feat: Implement Krux KEF encryption compatibility

This commit is contained in:
LC mac
2026-02-08 01:36:17 +08:00
parent 008406ef59
commit 54195ead8d
10 changed files with 203 additions and 99 deletions

View File

@@ -1,14 +1,15 @@
// Krux KEF tests using Bun test runner
import { describe, test, expect } from "bun:test";
import {
encryptToKrux,
decryptFromKrux,
hexToBytes,
bytesToHex,
wrap,
import {
encryptToKrux,
decryptFromKrux,
hexToBytes,
bytesToHex,
wrap,
unwrap,
KruxCipher
KruxCipher
} from './krux';
import { getWalletFingerprint } from "./bip32";
describe('Krux KEF Implementation', () => {
// Test basic hex conversion
@@ -39,10 +40,10 @@ describe('Krux KEF Implementation', () => {
const version = 20;
const iterations = 200000;
const payload = new TextEncoder().encode('test payload');
const wrapped = wrap(label, version, iterations, payload);
const unwrapped = unwrap(wrapped);
expect(unwrapped.label).toBe(label);
expect(unwrapped.version).toBe(version);
expect(unwrapped.iterations).toBe(iterations);
@@ -52,7 +53,7 @@ describe('Krux KEF Implementation', () => {
test('wrap rejects label too long', () => {
const longLabel = 'a'.repeat(253); // 253 > 252 max
const payload = new Uint8Array([1, 2, 3]);
expect(() => wrap(longLabel, 20, 10000, payload))
.toThrow('Label too long');
});
@@ -69,10 +70,10 @@ describe('Krux KEF Implementation', () => {
test('unwrap rejects invalid envelope', () => {
expect(() => unwrap(new Uint8Array([1, 2, 3]))).toThrow('Invalid KEF envelope: too short');
// Label length too large (253 > 252)
expect(() => unwrap(new Uint8Array([253, 20, 0, 0, 100]))).toThrow('Invalid label length');
// Empty label (lenId=0) is valid, but need enough data for version+iterations
// Create a valid envelope with empty label: [0, version, iter1, iter2, iter3, payload...]
const emptyLabelEnvelope = new Uint8Array([0, 20, 0, 0, 100, 1, 2, 3]);
@@ -85,30 +86,28 @@ describe('Krux KEF Implementation', () => {
test('encryptToKrux and decryptFromKrux roundtrip', async () => {
const mnemonic = 'test test test test test test test test test test test junk';
const passphrase = 'secure-passphrase';
const label = 'Test Seed';
const iterations = 10000;
const encrypted = await encryptToKrux({
mnemonic,
passphrase,
label,
iterations,
version: 20,
});
expect(encrypted.kefHex).toMatch(/^[0-9A-F]+$/);
expect(encrypted.label).toBe(label);
expect(encrypted.iterations).toBe(iterations);
const expectedLabel = getWalletFingerprint(mnemonic);
expect(encrypted.kefBase43).toMatch(/^[0-9A-Z$*+-\./:]+$/); // Check Base43 format
expect(encrypted.label).toBe(expectedLabel);
expect(encrypted.iterations).toBe(100000);
expect(encrypted.version).toBe(20);
const decrypted = await decryptFromKrux({
kefData: encrypted.kefHex,
kefData: encrypted.kefBase43, // Use kefBase43 for decryption
passphrase,
});
expect(decrypted.mnemonic).toBe(mnemonic);
expect(decrypted.label).toBe(label);
expect(decrypted.iterations).toBe(iterations);
expect(decrypted.label).toBe(expectedLabel);
expect(decrypted.iterations).toBe(100000);
expect(decrypted.version).toBe(20);
});
@@ -123,20 +122,20 @@ describe('Krux KEF Implementation', () => {
await expect(decryptFromKrux({
kefData: '123456',
passphrase: '',
})).rejects.toThrow('Passphrase is required');
})).rejects.toThrow('Invalid Krux data: Not a valid Hex or Base43 string.'); // Updated error message
});
test('wrong passphrase fails decryption', async () => {
const mnemonic = 'test mnemonic';
const passphrase = 'correct-passphrase';
const encrypted = await encryptToKrux({
mnemonic,
passphrase,
});
await expect(decryptFromKrux({
kefData: encrypted.kefHex,
kefData: encrypted.kefBase43, // Use kefBase43 for decryption
passphrase: 'wrong-passphrase',
})).rejects.toThrow(/Krux decryption failed/);
});
@@ -145,49 +144,46 @@ describe('Krux KEF Implementation', () => {
test('KruxCipher encrypt/decrypt roundtrip', async () => {
const cipher = new KruxCipher('passphrase', new TextEncoder().encode('salt'), 10000);
const plaintext = new TextEncoder().encode('secret message');
const encrypted = await cipher.encrypt(plaintext);
const decrypted = await cipher.decrypt(encrypted, 20);
expect(new TextDecoder().decode(decrypted)).toBe('secret message');
});
test('KruxCipher rejects unsupported version', async () => {
const cipher = new KruxCipher('passphrase', new TextEncoder().encode('salt'), 10000);
const plaintext = new Uint8Array([1, 2, 3]);
await expect(cipher.encrypt(plaintext, 99)).rejects.toThrow('Unsupported KEF version');
await expect(cipher.decrypt(new Uint8Array(50), 99)).rejects.toThrow('Payload too short for AES-GCM');
await expect(cipher.decrypt(new Uint8Array(50), 99)).rejects.toThrow('Unsupported KEF version'); // Changed error message
});
test('KruxCipher rejects short payload', async () => {
const cipher = new KruxCipher('passphrase', new TextEncoder().encode('salt'), 10000);
// Version 20: IV (12) + auth (4) = 16 bytes minimum
const shortPayload = new Uint8Array(15); // Too short for IV + GCM tag (needs at least 16)
await expect(cipher.decrypt(shortPayload, 20)).rejects.toThrow('Payload too short for AES-GCM');
});
test('iterations scaling works correctly', () => {
// Test that iterations are scaled properly when divisible by 10000
const label = 'Test';
const version = 20;
const payload = new TextEncoder().encode('test payload');
// 200000 should be scaled to 20 in the envelope
const wrapped1 = wrap(label, version, 200000, payload);
expect(wrapped1[6]).toBe(0); // 200000 / 10000 = 20
expect(wrapped1[6]).toBe(0);
expect(wrapped1[7]).toBe(0);
expect(wrapped1[8]).toBe(20);
// 10001 should not be scaled
const wrapped2 = wrap(label, version, 10001, payload);
const iterStart = 2 + label.length;
const iters = (wrapped2[iterStart] << 16) | (wrapped2[iterStart + 1] << 8) | wrapped2[iterStart + 2];
expect(iters).toBe(10001);
});
// New test case for user-provided KEF string
// New test case for user-provided KEF string - this one already uses base43Decode
test('should correctly decrypt the user-provided KEF string', async () => {
const kefData = "1334+HGXM$F8PPOIRNHX0.R*:SBMHK$X88LX$*/Y417R/6S1ZQOB2LHM-L+4T1YQVU:B*CKGXONP7:Y/R-B*:$R8FK";
const passphrase = "aaa";