mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-06 17:37:51 +08:00
- Replace BarcodeDetector with jsQR for raw binary byte access - BarcodeDetector forced UTF-8 decoding which corrupted binary data - jsQR's binaryData property preserves raw bytes without text conversion - Fix regex bug: use single backslash \x00 instead of \x00 for binary detection - Add debug logging for scan data inspection - QR generation already worked (Krux-compatible), only scanning was broken Resolves binary QR code scanning for 12/24-word CompactSeedQR format. Tested with Krux device - full bidirectional compatibility confirmed.
124 lines
3.3 KiB
TypeScript
124 lines
3.3 KiB
TypeScript
import * as bip39 from "bip39";
|
|
// Bun implements the Web Crypto API globally as `crypto`
|
|
|
|
const BASE43_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:";
|
|
|
|
// --- Helper: Base43 Decode ---
|
|
function base43Decode(str: string): Uint8Array {
|
|
let value = 0n;
|
|
const base = 43n;
|
|
|
|
for (const char of str) {
|
|
const index = BASE43_ALPHABET.indexOf(char);
|
|
if (index === -1) throw new Error(`Invalid Base43 char: ${char}`);
|
|
value = value * base + BigInt(index);
|
|
}
|
|
|
|
// Convert BigInt to Buffer/Uint8Array
|
|
let hex = value.toString(16);
|
|
if (hex.length % 2 !== 0) hex = '0' + hex;
|
|
return new Uint8Array(Buffer.from(hex, 'hex'));
|
|
}
|
|
|
|
// --- Main Decryption Function ---
|
|
async function decryptKruxKEF(kefData: string, passphrase: string) {
|
|
// 1. Decode Base43
|
|
const rawBytes = base43Decode(kefData);
|
|
|
|
// 2. Parse Envelope
|
|
let offset = 0;
|
|
|
|
// ID Length (1 byte)
|
|
const idLen = rawBytes[offset++];
|
|
|
|
// ID / Salt
|
|
const salt = rawBytes.slice(offset, offset + idLen);
|
|
offset += idLen;
|
|
|
|
// Version (1 byte)
|
|
const version = rawBytes[offset++];
|
|
|
|
// Iterations (3 bytes, Big-Endian)
|
|
const iterBytes = rawBytes.slice(offset, offset + 3);
|
|
const iterations = (iterBytes[0] << 16) | (iterBytes[1] << 8) | iterBytes[2];
|
|
offset += 3;
|
|
|
|
// Payload: [IV (12 bytes)] [Ciphertext (N)] [Tag (4 bytes)]
|
|
const payload = rawBytes.slice(offset);
|
|
const iv = payload.slice(0, 12);
|
|
const tagLength = 4;
|
|
const ciphertextWithTag = payload.slice(12);
|
|
|
|
console.log("--- Parsed KEF Data ---");
|
|
console.log(`Version: ${version}`);
|
|
console.log(`Iterations: ${iterations}`);
|
|
console.log(`Salt (hex): ${Buffer.from(salt).toString('hex')}`);
|
|
|
|
if (version !== 20) {
|
|
throw new Error("Only KEF Version 20 (AES-GCM) is supported.");
|
|
}
|
|
|
|
// 3. Derive Key (PBKDF2-HMAC-SHA256)
|
|
const keyMaterial = await crypto.subtle.importKey(
|
|
"raw",
|
|
new TextEncoder().encode(passphrase),
|
|
{ name: "PBKDF2" },
|
|
false,
|
|
["deriveKey"]
|
|
);
|
|
|
|
const key = await crypto.subtle.deriveKey(
|
|
{
|
|
name: "PBKDF2",
|
|
salt: salt,
|
|
iterations: iterations,
|
|
hash: "SHA-256",
|
|
},
|
|
keyMaterial,
|
|
{ name: "AES-GCM", length: 256 },
|
|
false,
|
|
["decrypt"]
|
|
);
|
|
|
|
// 4. Decrypt (AES-GCM)
|
|
try {
|
|
const decryptedBuffer = await crypto.subtle.decrypt(
|
|
{
|
|
name: "AES-GCM",
|
|
iv: iv,
|
|
tagLength: 32, // 4 bytes * 8
|
|
},
|
|
key,
|
|
ciphertextWithTag
|
|
);
|
|
|
|
return new Uint8Array(decryptedBuffer);
|
|
|
|
} catch (error) {
|
|
throw new Error(`Decryption failed: ${error}`);
|
|
}
|
|
}
|
|
|
|
// --- Run Test ---
|
|
const kefString = "1334+HGXM$F8PPOIRNHX0.R*:SBMHK$X88LX$*/Y417R/6S1ZQOB2LHM-L+4T1YQVU:B*CKGXONP7:Y/R-B*:$R8FK";
|
|
const passphrase = "aaa";
|
|
const expectedMnemonic = "differ release beauty fresh tortoise usage curtain spoil october town embrace ridge rough reject cabin snap glimpse enter book coach green lonely hundred mercy";
|
|
|
|
console.log(`\nDecrypting KEF String...`);
|
|
try {
|
|
const entropy = await decryptKruxKEF(kefString, passphrase);
|
|
const mnemonic = bip39.entropyToMnemonic(Buffer.from(entropy));
|
|
|
|
console.log("\n--- Result ---");
|
|
console.log(`Mnemonic: ${mnemonic}`);
|
|
|
|
if (mnemonic === expectedMnemonic) {
|
|
console.log("\n✅ SUCCESS: Mnemonic matches expected output.");
|
|
} else {
|
|
console.log("\n❌ FAIL: Mnemonic does not match.");
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|