diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 764fff3..770c316 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,28 +1,32 @@ const APP_SALT = "tryskills.sh-v1"; const ITERATIONS = 100_000; -function textToBuffer(text: string): ArrayBuffer { - return new TextEncoder().encode(text).buffer as ArrayBuffer; +// Helpers return Uint8Array (not SharedArrayBuffer-backed) so they +// satisfy WebCrypto BufferSource in both Node and jsdom realms. + +function textToBytes(text: string): Uint8Array { + return new Uint8Array(new TextEncoder().encode(text)); } -function bufferToHex(buffer: ArrayBuffer): string { - return Array.from(new Uint8Array(buffer)) +function bytesToHex(bytes: Uint8Array | ArrayBuffer): string { + const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes); + return Array.from(view) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } -function hexToBuffer(hex: string): ArrayBuffer { +function hexToBytes(hex: string): Uint8Array { const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } - return bytes.buffer as ArrayBuffer; + return bytes; } export async function deriveKey(userId: string): Promise { const keyMaterial = await crypto.subtle.importKey( "raw", - textToBuffer(userId), + textToBytes(userId), "PBKDF2", false, ["deriveKey"], @@ -31,7 +35,7 @@ export async function deriveKey(userId: string): Promise { return crypto.subtle.deriveKey( { name: "PBKDF2", - salt: textToBuffer(APP_SALT), + salt: textToBytes(APP_SALT), iterations: ITERATIONS, hash: "SHA-256", }, @@ -46,9 +50,8 @@ export async function encrypt( plaintext: string, key: CryptoKey, ): Promise<{ ciphertext: string; iv: string }> { - const ivArray = crypto.getRandomValues(new Uint8Array(12)); - const iv = ivArray.buffer as ArrayBuffer; - const encoded = textToBuffer(plaintext); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const encoded = textToBytes(plaintext); const encrypted = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, @@ -57,8 +60,8 @@ export async function encrypt( ); return { - ciphertext: bufferToHex(encrypted), - iv: bufferToHex(iv), + ciphertext: bytesToHex(encrypted), + iv: bytesToHex(iv), }; } @@ -68,9 +71,9 @@ export async function decrypt( key: CryptoKey, ): Promise { const decrypted = await crypto.subtle.decrypt( - { name: "AES-GCM", iv: hexToBuffer(iv) }, + { name: "AES-GCM", iv: hexToBytes(iv) }, key, - hexToBuffer(ciphertext), + hexToBytes(ciphertext), ); return new TextDecoder().decode(decrypted);