From e25cd9ebf926a91529f6112f86f3ec2d84fc3b64 Mon Sep 17 00:00:00 2001 From: LC mac Date: Wed, 4 Feb 2026 13:41:20 +0800 Subject: [PATCH] fix(krux): add Base43 decoding for encrypted QR codes Implements support for Base43-encoded QR codes generated by Krux devices, resolving a bug where they were misidentified as invalid text. - Adds a new `lib/base43.ts` module with a decoder ported from the official Krux Python implementation. - Updates `detectEncryptionMode` to use the Base43 alphabet for more accurate `'krux'` format detection. - Modifies `decryptFromKrux` to be robust, attempting to decode input as Hex first and falling back to Base43. - This allows the Seed Blender to correctly parse and trigger the decryption flow for both Hex and Base43-encoded Krux QR codes. --- src/components/SeedBlender.tsx | 2 +- src/lib/base43.ts | 54 ++++++++++++++++++++++++++++++++++ src/lib/krux.ts | 22 ++++++++++++-- src/lib/seedpgp.ts | 25 +++++++++++----- 4 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 src/lib/base43.ts diff --git a/src/components/SeedBlender.tsx b/src/components/SeedBlender.tsx index d8e4dbc..1c3855b 100644 --- a/src/components/SeedBlender.tsx +++ b/src/components/SeedBlender.tsx @@ -174,7 +174,7 @@ export function SeedBlender({ onDirtyStateChange, setMnemonicForBackup, requestT try { let mnemonic: string; if (entry.inputType === 'krux') { - mnemonic = (await decryptFromKrux({ kefHex: entry.rawInput, passphrase: entry.passwordInput })).mnemonic; + mnemonic = (await decryptFromKrux({ kefData: entry.rawInput, passphrase: entry.passwordInput })).mnemonic; } else { // seedpgp mnemonic = (await decryptFromSeed({ frameText: entry.rawInput, messagePassword: entry.passwordInput, mode: 'pgp' })).w; } diff --git a/src/lib/base43.ts b/src/lib/base43.ts new file mode 100644 index 0000000..09f864a --- /dev/null +++ b/src/lib/base43.ts @@ -0,0 +1,54 @@ +/** + * @file Base43 encoding/decoding, ported from Krux's pure_python_base_decode. + */ + +export const B43CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*+-./:"; +const B43_MAP = new Map(); +for (let i = 0; i < B43CHARS.length; i++) { + B43_MAP.set(B43CHARS[i], BigInt(i)); +} + +/** + * Decodes a Base43 string into bytes. + * This is a direct port of the pure_python_base_decode function from Krux. + * @param v The Base43 encoded string. + * @returns The decoded bytes as a Uint8Array. + */ +export function base43Decode(v: string): Uint8Array { + if (typeof v !== 'string') { + throw new TypeError("Invalid value, expected string"); + } + if (v === "") { + return new Uint8Array(0); + } + + let longValue = 0n; + let powerOfBase = 1n; + const base = 43n; + + for (let i = v.length - 1; i >= 0; i--) { + const char = v[i]; + const digit = B43_MAP.get(char); + if (digit === undefined) { + throw new Error(`forbidden character ${char} for base 43`); + } + longValue += digit * powerOfBase; + powerOfBase *= base; + } + + const result: number[] = []; + while (longValue >= 256) { + result.push(Number(longValue % 256n)); + longValue /= 256n; + } + if (longValue > 0) { + result.push(Number(longValue)); + } + + // Pad with leading zeros + for (let i = 0; i < v.length && v[i] === B43CHARS[0]; i++) { + result.push(0); + } + + return new Uint8Array(result.reverse()); +} diff --git a/src/lib/krux.ts b/src/lib/krux.ts index 6f834e8..7310634 100644 --- a/src/lib/krux.ts +++ b/src/lib/krux.ts @@ -310,18 +310,34 @@ export async function encryptToKrux(params: { }; } +import { base43Decode } from './base43'; + +// ... (rest of the file until decryptFromKrux) + /** - * Decrypt KEF hex to mnemonic + * Decrypt KEF data (Hex or Base43) to mnemonic */ export async function decryptFromKrux(params: { - kefHex: string; + kefData: string; passphrase: string; }): Promise<{ mnemonic: string; label: string; version: number; iterations: number }> { if (!params.passphrase) { throw new Error("Passphrase is required for Krux decryption"); } - const bytes = hexToBytes(params.kefHex); + let bytes: Uint8Array; + try { + // First, try to decode as hex + bytes = hexToBytes(params.kefData); + } catch (e) { + // If hex fails, try to decode as Base43 + try { + bytes = base43Decode(params.kefData); + } catch (e2) { + throw new Error("Invalid Krux data: Not a valid Hex or Base43 string."); + } + } + const { label, version, iterations, payload } = unwrap(bytes); const cipher = new KruxCipher(params.passphrase, label, iterations); const decrypted = await cipher.decrypt(payload, version); diff --git a/src/lib/seedpgp.ts b/src/lib/seedpgp.ts index 4c04995..762b137 100644 --- a/src/lib/seedpgp.ts +++ b/src/lib/seedpgp.ts @@ -315,25 +315,36 @@ export async function decryptFromSeed(params: DecryptionParams): Promise 10) { // check for hex and minimum length + if (/^[0-9a-fA-F]{10,}$/.test(cleanedHex)) { return 'krux'; } - - // 3. If it contains spaces, it's almost certainly a plain text mnemonic + + // 3. Likely a plain text mnemonic if (trimmed.includes(' ')) { return 'text'; } - - // 4. Default to text for anything else. The component validation will handle it. + + // 4. Heuristic: If it looks like Base43, assume it's a Krux payload + if (BASE43_CHARS_ONLY_REGEX.test(trimmed)) { + return 'krux'; + } + + // 5. Default to text, which will then fail validation in the component. return 'text'; }