public
imalexnord
read
Slot
Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.
Languages
Repository composition by tracked source files.
TypeScript
79%
CSS
19%
SQL
2%
HTML
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/slot.git
ssh://[email protected]:2222/user/imalexnord/slot.git
Trace
worker/crypto.ts
Trace helps you understand code history line by line. See who changed each line, when it changed, and which commit introduced it.
Author
Date
Commit
Line
Code
1
import type { Env } from './types';
3
const encoder = new TextEncoder();
4
const decoder = new TextDecoder();
6
function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
7
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer;
8
}
10
function base64Url(bytes: Uint8Array): string {
11
let binary = '';
12
for (const byte of bytes) binary += String.fromCharCode(byte);
13
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
14
}
16
function fromBase64(value: string): Uint8Array {
17
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
18
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
19
const binary = atob(padded);
20
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
21
}
23
async function hmacKey(secret: string): Promise<CryptoKey> {
24
return crypto.subtle.importKey(
25
'raw',
26
encoder.encode(secret),
27
{ name: 'HMAC', hash: 'SHA-256' },
28
false,
29
['sign', 'verify'],
30
);
31
}
33
export async function signSession(env: Env, email: string): Promise<string> {
34
const payload = base64Url(encoder.encode(JSON.stringify({ email, exp: Date.now() + 7 * 86400_000 })));
35
const signature = new Uint8Array(
36
await crypto.subtle.sign('HMAC', await hmacKey(env.SESSION_SECRET), encoder.encode(payload)),
37
);
38
return `${payload}.${base64Url(signature)}`;
39
}
41
export async function verifySession(env: Env, token: string | null): Promise<string | null> {
42
if (!token) return null;
43
const [payload, signature] = token.split('.');
44
if (!payload || !signature) return null;
45
const ok = await crypto.subtle.verify(
46
'HMAC',
47
await hmacKey(env.SESSION_SECRET),
48
toArrayBuffer(fromBase64(signature)),
49
encoder.encode(payload),
50
);
51
if (!ok) return null;
53
try {
54
const parsed = JSON.parse(decoder.decode(fromBase64(payload))) as { email?: string; exp?: number };
55
if (!parsed.email || !parsed.exp || parsed.exp < Date.now()) return null;
56
return parsed.email;
57
} catch {
58
return null;
59
}
60
}
62
async function encryptionKey(env: Env): Promise<CryptoKey> {
63
const raw = fromBase64(env.TOKEN_ENCRYPTION_KEY);
64
if (raw.byteLength !== 32) throw new Error('TOKEN_ENCRYPTION_KEY must decode to 32 bytes');
65
return crypto.subtle.importKey('raw', toArrayBuffer(raw), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
66
}
68
export async function encryptToken(env: Env, value: string): Promise<string> {
69
const iv = crypto.getRandomValues(new Uint8Array(12));
70
const ciphertext = new Uint8Array(
71
await crypto.subtle.encrypt({ name: 'AES-GCM', iv: toArrayBuffer(iv) }, await encryptionKey(env), encoder.encode(value)),
72
);
73
return `${base64Url(iv)}.${base64Url(ciphertext)}`;
74
}
76
export async function decryptToken(env: Env, value: string): Promise<string> {
77
const [ivText, ciphertextText] = value.split('.');
78
if (!ivText || !ciphertextText) throw new Error('Invalid encrypted token');
79
const plaintext = await crypto.subtle.decrypt(
80
{ name: 'AES-GCM', iv: toArrayBuffer(fromBase64(ivText)) },
81
await encryptionKey(env),
82
toArrayBuffer(fromBase64(ciphertextText)),
83
);
84
return decoder.decode(plaintext);
85
}
88
export function randomUrlToken(bytes = 32): string {
89
return base64Url(crypto.getRandomValues(new Uint8Array(bytes)));
90
}
92
export async function sha256Base64Url(value: string): Promise<string> {
93
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(value)));
94
return base64Url(digest);
95
}
97
export function getCookie(request: Request, key: string): string | null {
98
const cookie = request.headers.get('Cookie') ?? '';
99
for (const part of cookie.split(';')) {
100
const [name, ...rest] = part.trim().split('=');
101
if (name === key) return decodeURIComponent(rest.join('='));
102
}
103
return null;
104
}
106
export function sessionCookie(value: string, maxAge = 7 * 86400, secure = true): string {
107
const securePart = secure ? '; Secure' : '';
108
return `meet_session=${encodeURIComponent(value)}; Path=/; HttpOnly${securePart}; SameSite=Lax; Max-Age=${maxAge}`;
109
}
111
export function clearSessionCookie(secure = true): string {
112
const securePart = secure ? '; Secure' : '';
113
return `meet_session=; Path=/; HttpOnly${securePart}; SameSite=Lax; Max-Age=0`;
114
}