src/lib/auth.ts 2.2 K raw
1
const encoder = new TextEncoder();
2
3
export 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
22
export 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
31
export 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
49
export 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
78
function arrayBufferToHex(buffer: ArrayBuffer): string {
79
	return Array.from(new Uint8Array(buffer))
80
		.map((b) => b.toString(16).padStart(2, "0"))
81
		.join("");
82
}
83
84
function 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
}