diff --git a/src/lib/krux.ts b/src/lib/krux.ts index 91aad99..c224987 100644 --- a/src/lib/krux.ts +++ b/src/lib/krux.ts @@ -56,6 +56,34 @@ 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: ${version}`); @@ -101,6 +129,21 @@ 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");