diff --git a/src/lib/krux.ts b/src/lib/krux.ts index cc72413..91aad99 100644 --- a/src/lib/krux.ts +++ b/src/lib/krux.ts @@ -3,13 +3,13 @@ import * as pako from 'pako'; import { base43Decode } from './base43'; -// KEF version definitions (matches Python reference) +// KEF version definitions, ported from kef.py export const VERSIONS: Record = { - // Versions from kef.py - only GCM modes are relevant for our WebCrypto implementation + // We only implement the GCM versions as they are the only ones compatible with WebCrypto 20: { name: "AES-GCM", compress: false, auth: 4 }, 21: { name: "AES-GCM +c", compress: true, auth: 4 }, }; @@ -17,32 +17,7 @@ export const VERSIONS: Record> 16) & 0xff; - itersBytes[1] = (scaled >> 8) & 0xff; - itersBytes[2] = scaled & 0xff; - } else { - if (!(10000 < iterations && iterations < 2**24)) throw new Error("Iterations out of range"); - itersBytes[0] = (iterations >> 16) & 0xff; - itersBytes[1] = (iterations >> 8) & 0xff; - itersBytes[2] = iterations & 0xff; - } - return new Uint8Array([...lenId, ...labelBytes, ...versionByte, ...itersBytes, ...payload]); + return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); } export function unwrap(envelope: Uint8Array): { label: string; version: number; iterations: number; payload: Uint8Array } { @@ -54,6 +29,9 @@ export function unwrap(envelope: Uint8Array): { label: string; version: number; const labelBytes = envelope.subarray(1, 1 + lenId); const label = new TextDecoder().decode(labelBytes); const version = envelope[1 + lenId]; + if (!VERSIONS[version]) { + throw new Error(`Unsupported KEF version: ${version}`); + } const iterStart = 2 + lenId; let iters = (envelope[iterStart] << 16) | (envelope[iterStart + 1] << 8) | envelope[iterStart + 2]; @@ -78,37 +56,9 @@ export class KruxCipher { })(); } - async encrypt(plaintext: Uint8Array, version = 20, iv?: Uint8Array): Promise { - const v = VERSIONS[version]; - if (!v) throw new Error(`Unsupported KEF version: ${version}`); - - let dataToEncrypt = plaintext; - if (v.compress) { - dataToEncrypt = pako.deflate(plaintext); - } - - let ivBytes = iv ? new Uint8Array(iv) : crypto.getRandomValues(new Uint8Array(GCM_IV_LENGTH)); - - const key = await this.keyPromise; - const plaintextBuffer = toArrayBuffer(dataToEncrypt); - const ivBuffer = toArrayBuffer(ivBytes); - const tagLengthBits = v.auth * 8; - - const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv: ivBuffer, tagLength: tagLengthBits }, key, plaintextBuffer); - const encryptedBytes = new Uint8Array(encrypted); - const ciphertext = encryptedBytes.slice(0, encryptedBytes.length - v.auth); - const tag = encryptedBytes.slice(encryptedBytes.length - v.auth); - - const combined = new Uint8Array(ivBytes.length + ciphertext.length + tag.length); - combined.set(ivBytes, 0); - combined.set(ciphertext, ivBytes.length); - combined.set(tag, ivBytes.length + ciphertext.length); - return combined; - } - async decrypt(payload: Uint8Array, version: number): Promise { const v = VERSIONS[version]; - if (!v) throw new Error(`Unsupported KEF version: A${version}`); + if (!v) throw new Error(`Unsupported KEF version: ${version}`); if (payload.length < GCM_IV_LENGTH + v.auth) throw new Error("Payload too short for AES-GCM"); const iv = payload.slice(0, GCM_IV_LENGTH); @@ -127,6 +77,8 @@ export class KruxCipher { ); let decrypted = new Uint8Array(decryptedBuffer); + + // Decompress if the version requires it if (v.compress) { decrypted = pako.inflate(decrypted); } @@ -149,21 +101,6 @@ export function hexToBytes(hex: string): Uint8Array { return bytes; } -export function bytesToHex(bytes: Uint8Array): string { - return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('').toUpperCase(); -} - -export async function encryptToKrux(params: { mnemonic: string; passphrase: string; label?: string; iterations?: number; version?: number; }): Promise<{ kefHex: string; label: string; version: number; iterations: number }> { - const { mnemonic, passphrase, label = "Seed Backup", iterations = 200000, version = 21 } = params; - if (!passphrase) throw new Error("Passphrase is required for Krux encryption"); - - const mnemonicBytes = new TextEncoder().encode(mnemonic); - const cipher = new KruxCipher(passphrase, label, iterations); - const payload = await cipher.encrypt(mnemonicBytes, version); - const kef = wrap(label, version, iterations, payload); - return { kefHex: bytesToHex(kef), label, version, iterations }; -} - export async function decryptFromKrux(params: { kefData: string; passphrase: string; }): Promise<{ mnemonic: string; label: string; version: number; iterations: number }> { const { kefData, passphrase } = params; if (!passphrase) throw new Error("Passphrase is required for Krux decryption"); @@ -185,4 +122,4 @@ export async function decryptFromKrux(params: { kefData: string; passphrase: str const mnemonic = new TextDecoder().decode(decrypted); return { mnemonic, label, version, iterations }; -} +} \ No newline at end of file