~james/core

core/web/src/lib/server/sealed.ts -rw-r--r-- 1.6 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const enc = new TextEncoder();

const b64url = (x: ArrayBuffer | Uint8Array) => {
	const bytes = x instanceof Uint8Array ? x : new Uint8Array(x);
	let raw = "";
	for (const byte of bytes) raw += String.fromCharCode(byte);
	return btoa(raw).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
};

const unb64url = (x: string) => {
	const raw = atob(
		x.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - (x.length % 4)) % 4)
	);
	return Uint8Array.from(raw, (c) => c.charCodeAt(0));
};

const keyFor = async (secret: string) =>
	crypto.subtle.importKey(
		"raw",
		await crypto.subtle.digest("SHA-256", enc.encode(secret)),
		"AES-GCM",
		false,
		["encrypt", "decrypt"]
	);

export const sha256Base64Url = async (value: string) =>
	b64url(await crypto.subtle.digest("SHA-256", enc.encode(value)));

export const seal = async (value: unknown, secret: string) => {
	if (!secret) throw new Error("session secret is not configured");
	const iv = crypto.getRandomValues(new Uint8Array(12));
	const body = await crypto.subtle.encrypt(
		{ name: "AES-GCM", iv },
		await keyFor(secret),
		enc.encode(JSON.stringify(value))
	);
	return `${b64url(iv)}.${b64url(body)}`;
};

export const open = async <T>(value: string | undefined, secret: string): Promise<T | null> => {
	if (!value || !secret) return null;
	try {
		const [iv, body] = value.split(".");
		if (!iv || !body) return null;
		const plain = await crypto.subtle.decrypt(
			{ name: "AES-GCM", iv: unb64url(iv) },
			await keyFor(secret),
			unb64url(body)
		);
		return JSON.parse(new TextDecoder().decode(plain)) as T;
	} catch {
		return null;
	}
};