My personal photography website
steve.phot
portfolio
photography
svelte
sveltekit
1const encoder = new TextEncoder();
2
3export async function hashPassword(
4 password: string,
5 secret: string,
6): Promise<string> {
7 const key = await crypto.subtle.importKey(
8 "raw",
9 encoder.encode(secret),
10 { name: "HMAC", hash: "SHA-256" },
11 false,
12 ["sign"],
13 );
14 const signature = await crypto.subtle.sign(
15 "HMAC",
16 key,
17 encoder.encode(password),
18 );
19 return arrayBufferToHex(signature);
20}
21
22export async function verifyPassword(
23 password: string,
24 hash: string,
25 secret: string,
26): Promise<boolean> {
27 const computed = await hashPassword(password, secret);
28 return timingSafeEqual(computed, hash);
29}
30
31export async function createSession(secret: string): Promise<string> {
32 const sessionId = crypto.randomUUID();
33 const timestamp = Date.now().toString();
34 const data = `${sessionId}.${timestamp}`;
35
36 const key = await crypto.subtle.importKey(
37 "raw",
38 encoder.encode(secret),
39 { name: "HMAC", hash: "SHA-256" },
40 false,
41 ["sign"],
42 );
43 const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
44 const sig = arrayBufferToHex(signature);
45
46 return `${data}.${sig}`;
47}
48
49export async function verifySession(
50 token: string,
51 secret: string,
52): Promise<boolean> {
53 const parts = token.split(".");
54 if (parts.length !== 3) return false;
55
56 const [sessionId, timestamp, providedSig] = parts;
57 const data = `${sessionId}.${timestamp}`;
58
59 // Check if session is expired (24 hours)
60 const sessionTime = parseInt(timestamp, 10);
61 if (isNaN(sessionTime) || Date.now() - sessionTime > 24 * 60 * 60 * 1000) {
62 return false;
63 }
64
65 const key = await crypto.subtle.importKey(
66 "raw",
67 encoder.encode(secret),
68 { name: "HMAC", hash: "SHA-256" },
69 false,
70 ["sign"],
71 );
72 const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(data));
73 const expectedSig = arrayBufferToHex(signature);
74
75 return timingSafeEqual(providedSig, expectedSig);
76}
77
78function arrayBufferToHex(buffer: ArrayBuffer): string {
79 return Array.from(new Uint8Array(buffer))
80 .map((b) => b.toString(16).padStart(2, "0"))
81 .join("");
82}
83
84function timingSafeEqual(a: string, b: string): boolean {
85 if (a.length !== b.length) return false;
86 let result = 0;
87 for (let i = 0; i < a.length; i++) {
88 result |= a.charCodeAt(i) ^ b.charCodeAt(i);
89 }
90 return result === 0;
91}