import * as Effect from "effect/Effect"; export const encode = (bytes: Uint8Array) => btoa(String.fromCharCode(...bytes)) .replaceAll("+", "-") .replaceAll("/", "_") .replaceAll("=", ""); export const decode = (value: string) => Uint8Array.from(atob(value.replaceAll("-", "+").replaceAll("_", "/")), (c) => c.charCodeAt(0), ); export const random = () => encode(crypto.getRandomValues(new Uint8Array(32))); export const hash = (value: string) => Effect.promise(() => crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)), ).pipe(Effect.map((bytes) => encode(new Uint8Array(bytes)))); export const opaque = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9_-]{43}$/.test(value); function key(secret: string) { return Effect.gen(function* () { return yield* Effect.promise(() => crypto.subtle.importKey("raw", decode(secret), "AES-GCM", false, [ "encrypt", "decrypt", ]), ); }); } export function encrypt(secret: string, value: unknown, binding: string) { return Effect.gen(function* () { const iv = crypto.getRandomValues(new Uint8Array(12)); const cryptoKey = yield* key(secret); const ciphertext = yield* Effect.promise(() => crypto.subtle.encrypt( { name: "AES-GCM", iv, additionalData: new TextEncoder().encode(binding), }, cryptoKey, new TextEncoder().encode(JSON.stringify(value)), ), ); return { iv: encode(iv), ciphertext: encode(new Uint8Array(ciphertext)) }; }); } export function decrypt( secret: string, sealed: { iv: string; ciphertext: string }, binding: string, ) { return Effect.gen(function* () { const cryptoKey = yield* key(secret); const bytes = yield* Effect.promise(() => crypto.subtle.decrypt( { name: "AES-GCM", iv: decode(sealed.iv), additionalData: new TextEncoder().encode(binding), }, cryptoKey, decode(sealed.ciphertext), ), ); return JSON.parse(new TextDecoder().decode(bytes)) as T; }); }