返回 DeepSeek-Reasonix
crypto.ts
根目录 / workers / accounts / src / auth / crypto.ts
1 // Password hashing (PBKDF2-HMAC-SHA256) and token helpers, built on the Web
2 // Crypto API available in the Workers runtime. Ported from the crash-report
3 // worker and hardened: tokens are stored hashed, peppered with a server secret.
4 import { PBKDF2_ITERATIONS } from "../config";
5
6 const encoder = new TextEncoder();
7
8 function toBase64(bytes: Uint8Array): string {
9 let s = "";
10 for (const byte of bytes) s += String.fromCharCode(byte);
11 return btoa(s);
12 }
13
14 function fromBase64(s: string): Uint8Array {
15 const bin = atob(s);
16 const out = new Uint8Array(bin.length);
17 for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
18 return out;
19 }
20
21 function toHex(bytes: Uint8Array): string {
22 let s = "";
23 for (const byte of bytes) s += byte.toString(16).padStart(2, "0");
24 return s;
25 }
26
27 async function deriveBits(password: string, salt: Uint8Array, iterations: number): Promise<Uint8Array> {
28 const key = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveBits"]);
29 const bits = await crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations, hash: "SHA-256" }, key, 256);
30 return new Uint8Array(bits);
31 }
32
33 // Stored as `pbkdf2$<iters>$<salt-b64>$<hash-b64>` so the work factor travels
34 // with the hash and can be raised without invalidating older passwords.
35 export async function hashPassword(password: string): Promise<string> {
36 const salt = crypto.getRandomValues(new Uint8Array(16));
37 const hash = await deriveBits(password, salt, PBKDF2_ITERATIONS);
38 return `pbkdf2$${PBKDF2_ITERATIONS}$${toBase64(salt)}$${toBase64(hash)}`;
39 }
40
41 export async function verifyPassword(password: string, stored: string | null): Promise<boolean> {
42 if (!stored) return false;
43 const [scheme, iters, saltB64, hashB64] = stored.split("$");
44 if (scheme !== "pbkdf2" || !iters || !saltB64 || !hashB64) return false;
45 const got = await deriveBits(password, fromBase64(saltB64), Number(iters));
46 const want = fromBase64(hashB64);
47 return got.byteLength === want.byteLength && crypto.subtle.timingSafeEqual(got, want);
48 }
49
50 export async function sha256Hex(input: string): Promise<string> {
51 const digest = await crypto.subtle.digest("SHA-256", encoder.encode(input));
52 return toHex(new Uint8Array(digest));
53 }
54
55 // 256 bits of entropy as hex — used for session cookies and email links.
56 export function generateToken(): string {
57 return toHex(crypto.getRandomValues(new Uint8Array(32)));
58 }
59
60 // Look-up key for a token: only the peppered hash is ever stored, so a DB read
61 // can neither resurrect a session nor redeem an email link.
62 export function hashToken(pepper: string, token: string): Promise<string> {
63 return sha256Hex(`${pepper}:${token}`);
64 }
65
66 // Crockford base32 (no I/L/O/U) so a spoken or hand-typed device code has no
67 // ambiguous characters.
68 const USER_CODE_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
69
70 // A canonical 8-char user code for the device-authorization flow. 40 bits of
71 // entropy; the pending window is tiny and short-lived and approval is
72 // auth-gated + rate-limited, so guessing is impractical. `byte & 31` is unbiased
73 // because 256 is an exact multiple of the 32-symbol alphabet.
74 export function generateUserCode(): string {
75 const bytes = crypto.getRandomValues(new Uint8Array(8));
76 let s = "";
77 for (const byte of bytes) s += USER_CODE_ALPHABET[byte & 31];
78 return s;
79 }
80
81 // A short random suffix for handle generation. Hex-encodes crypto bytes (a
82 // bijection — no modulo, so no bias) and trims to length. Hex digits are all
83 // valid handle characters.
84 export function randomSuffix(len: number): string {
85 const bytes = crypto.getRandomValues(new Uint8Array(Math.ceil(len / 2)));
86 let s = "";
87 for (const byte of bytes) s += byte.toString(16).padStart(2, "0");
88 return s.slice(0, len);
89 }
90
90 lines TYPESCRIPT