🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add a fallback lib when in a non secure web crypto context

+226 -21
+3
src/components/auth.ts
··· 415 415 await this.checkAuth(); 416 416 window.dispatchEvent(new CustomEvent("auth-changed")); 417 417 } 418 + } catch (error) { 419 + // Catch crypto.subtle errors and other exceptions 420 + this.error = error instanceof Error ? error.message : "An error occurred"; 418 421 } finally { 419 422 this.isSubmitting = false; 420 423 }
+6
src/components/password-strength.ts
··· 139 139 return; 140 140 } 141 141 142 + // Skip if crypto.subtle is not available (non-HTTPS) 143 + if (!crypto.subtle) { 144 + this.hasChecked = true; 145 + return; 146 + } 147 + 142 148 this.isChecking = true; 143 149 this.isPwned = false; 144 150
+42 -21
src/lib/client-auth.ts
··· 7 7 const ITERATIONS = 1_000_000; // ~1-2 seconds on modern devices 8 8 9 9 /** 10 + * PBKDF2 implementation with fallback for non-secure contexts. 11 + */ 12 + async function pbkdf2( 13 + password: Uint8Array, 14 + salt: Uint8Array, 15 + iterations: number, 16 + ): Promise<Uint8Array> { 17 + if (crypto.subtle) { 18 + // Use native crypto.subtle when available (secure contexts) 19 + const keyMaterial = await crypto.subtle.importKey( 20 + "raw", 21 + password, 22 + { name: "PBKDF2" }, 23 + false, 24 + ["deriveBits"], 25 + ); 26 + 27 + const hashBuffer = await crypto.subtle.deriveBits( 28 + { 29 + name: "PBKDF2", 30 + salt, 31 + iterations, 32 + hash: "SHA-256", 33 + }, 34 + keyMaterial, 35 + 256, 36 + ); 37 + 38 + return new Uint8Array(hashBuffer); 39 + } 40 + 41 + // Fallback: lazy-load pure JS implementation for non-secure contexts 42 + const { pbkdf2Fallback } = await import("./crypto-fallback"); 43 + return pbkdf2Fallback(password, salt, iterations); 44 + } 45 + 46 + /** 10 47 * Hash password client-side using PBKDF2. 11 48 * @param password - Plaintext password 12 49 * @param email - Email address (used as salt) ··· 18 55 ): Promise<string> { 19 56 const encoder = new TextEncoder(); 20 57 21 - // Import password as key 22 - const keyMaterial = await crypto.subtle.importKey( 23 - "raw", 24 - encoder.encode(password), 25 - { name: "PBKDF2" }, 26 - false, 27 - ["deriveBits"], 28 - ); 29 - 30 58 // Use email as salt (deterministic, unique per user) 59 + const passwordBytes = encoder.encode(password); 31 60 const salt = encoder.encode(email.toLowerCase()); 32 61 33 - // Derive 256 bits using PBKDF2 34 - const hashBuffer = await crypto.subtle.deriveBits( 35 - { 36 - name: "PBKDF2", 37 - salt, 38 - iterations: ITERATIONS, 39 - hash: "SHA-256", 40 - }, 41 - keyMaterial, 42 - 256, // 256 bits = 32 bytes 43 - ); 62 + // Derive 256 bits using PBKDF2 (with fallback for non-secure contexts) 63 + const hashBuffer = await pbkdf2(passwordBytes, salt, ITERATIONS); 44 64 45 65 // Convert to hex string 46 - const hashArray = Array.from(new Uint8Array(hashBuffer)); 66 + const hashArray = Array.from(hashBuffer); 47 67 return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); 48 68 } 69 +
+175
src/lib/crypto-fallback.ts
··· 1 + /** 2 + * Pure JavaScript crypto implementations for non-secure contexts. 3 + * Only used when crypto.subtle is unavailable (HTTP non-localhost). 4 + */ 5 + 6 + /** 7 + * Pure JS SHA-256 implementation. 8 + */ 9 + export async function sha256(data: Uint8Array): Promise<Uint8Array> { 10 + const K = new Uint32Array([ 11 + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 12 + 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 13 + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 14 + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 15 + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 16 + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 17 + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 18 + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 19 + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 20 + 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 21 + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, 22 + ]); 23 + 24 + const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n)); 25 + const ch = (x: number, y: number, z: number) => (x & y) ^ (~x & z); 26 + const maj = (x: number, y: number, z: number) => 27 + (x & y) ^ (x & z) ^ (y & z); 28 + const s0 = (x: number) => rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22); 29 + const s1 = (x: number) => rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25); 30 + const g0 = (x: number) => rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3); 31 + const g1 = (x: number) => rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10); 32 + 33 + // Pad message 34 + const msgLen = data.length; 35 + const bitLen = msgLen * 8; 36 + const padLen = msgLen + 1 + ((119 - msgLen) % 64); 37 + const padded = new Uint8Array(padLen + 8); 38 + padded.set(data); 39 + padded[msgLen] = 0x80; 40 + new DataView(padded.buffer).setUint32(padLen + 4, bitLen, false); 41 + 42 + // Initialize hash 43 + let h0 = 0x6a09e667; 44 + let h1 = 0xbb67ae85; 45 + let h2 = 0x3c6ef372; 46 + let h3 = 0xa54ff53a; 47 + let h4 = 0x510e527f; 48 + let h5 = 0x9b05688c; 49 + let h6 = 0x1f83d9ab; 50 + let h7 = 0x5be0cd19; 51 + 52 + // Process blocks 53 + const w = new Uint32Array(64); 54 + for (let i = 0; i < padded.length; i += 64) { 55 + const view = new DataView(padded.buffer, i, 64); 56 + for (let j = 0; j < 16; j++) w[j] = view.getUint32(j * 4, false); 57 + for (let j = 16; j < 64; j++) 58 + w[j] = (g1(w[j - 2]) + w[j - 7] + g0(w[j - 15]) + w[j - 16]) | 0; 59 + 60 + let a = h0, 61 + b = h1, 62 + c = h2, 63 + d = h3, 64 + e = h4, 65 + f = h5, 66 + g = h6, 67 + h = h7; 68 + 69 + for (let j = 0; j < 64; j++) { 70 + const t1 = (h + s1(e) + ch(e, f, g) + K[j] + w[j]) | 0; 71 + const t2 = (s0(a) + maj(a, b, c)) | 0; 72 + h = g; 73 + g = f; 74 + f = e; 75 + e = (d + t1) | 0; 76 + d = c; 77 + c = b; 78 + b = a; 79 + a = (t1 + t2) | 0; 80 + } 81 + 82 + h0 = (h0 + a) | 0; 83 + h1 = (h1 + b) | 0; 84 + h2 = (h2 + c) | 0; 85 + h3 = (h3 + d) | 0; 86 + h4 = (h4 + e) | 0; 87 + h5 = (h5 + f) | 0; 88 + h6 = (h6 + g) | 0; 89 + h7 = (h7 + h) | 0; 90 + } 91 + 92 + const result = new Uint8Array(32); 93 + const view = new DataView(result.buffer); 94 + view.setUint32(0, h0, false); 95 + view.setUint32(4, h1, false); 96 + view.setUint32(8, h2, false); 97 + view.setUint32(12, h3, false); 98 + view.setUint32(16, h4, false); 99 + view.setUint32(20, h5, false); 100 + view.setUint32(24, h6, false); 101 + view.setUint32(28, h7, false); 102 + 103 + return result; 104 + } 105 + 106 + /** 107 + * HMAC-SHA256 using pure JS SHA-256. 108 + */ 109 + async function hmac(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> { 110 + const blockSize = 64; 111 + const opad = new Uint8Array(blockSize).fill(0x5c); 112 + const ipad = new Uint8Array(blockSize).fill(0x36); 113 + 114 + if (key.length > blockSize) { 115 + key = await sha256(key); 116 + } 117 + 118 + const keyPadded = new Uint8Array(blockSize); 119 + keyPadded.set(key); 120 + 121 + for (let i = 0; i < blockSize; i++) { 122 + opad[i] ^= keyPadded[i]; 123 + ipad[i] ^= keyPadded[i]; 124 + } 125 + 126 + const inner = new Uint8Array(blockSize + data.length); 127 + inner.set(ipad); 128 + inner.set(data, blockSize); 129 + 130 + const innerHash = await sha256(inner); 131 + 132 + const outer = new Uint8Array(blockSize + 32); 133 + outer.set(opad); 134 + outer.set(innerHash, blockSize); 135 + 136 + return sha256(outer); 137 + } 138 + 139 + /** 140 + * Pure JS PBKDF2-HMAC-SHA256 implementation. 141 + */ 142 + export async function pbkdf2Fallback( 143 + password: Uint8Array, 144 + salt: Uint8Array, 145 + iterations: number, 146 + ): Promise<Uint8Array> { 147 + const dkLen = 32; // 256 bits 148 + const hLen = 32; // SHA-256 output length 149 + const l = Math.ceil(dkLen / hLen); 150 + const r = dkLen - (l - 1) * hLen; 151 + 152 + const dk = new Uint8Array(dkLen); 153 + 154 + for (let i = 1; i <= l; i++) { 155 + const saltInt = new Uint8Array(salt.length + 4); 156 + saltInt.set(salt); 157 + new DataView(saltInt.buffer).setUint32(salt.length, i, false); 158 + 159 + let u = await hmac(password, saltInt); 160 + const t = new Uint8Array(u); 161 + 162 + for (let j = 1; j < iterations; j++) { 163 + u = await hmac(password, u); 164 + for (let k = 0; k < hLen; k++) { 165 + t[k] ^= u[k]; 166 + } 167 + } 168 + 169 + const offset = (i - 1) * hLen; 170 + const len = i === l ? r : hLen; 171 + dk.set(t.subarray(0, len), offset); 172 + } 173 + 174 + return dk; 175 + }