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.
This commit is contained in:
LC mac
2026-02-04 13:41:20 +08:00
parent e8b0085689
commit e25cd9ebf9
4 changed files with 92 additions and 11 deletions

View File

@@ -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;
}

54
src/lib/base43.ts Normal file
View File

@@ -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<string, bigint>();
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());
}

View File

@@ -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);

View File

@@ -315,25 +315,36 @@ export async function decryptFromSeed(params: DecryptionParams): Promise<SeedPgp
/**
* Detect encryption mode from input text
*/
import { B43CHARS } from "./base43"; // Assuming B43CHARS is exported from base43.ts
// ... (other imports)
const BASE43_CHARS_ONLY_REGEX = new RegExp(`^[${B43CHARS.replace(/[\-\]\\]/g, '\\$&')}]+$`);
export function detectEncryptionMode(text: string): EncryptionMode {
const trimmed = text.trim();
// 1. Check for SEEDPGP1 format (most specific)
// 1. Definite PGP
if (trimmed.startsWith('SEEDPGP1:')) {
return 'pgp';
}
// 2. Check for Krux KEF format (hex, optional KEF: prefix)
// 2. Definite Krux (Hex)
const cleanedHex = trimmed.replace(/\s/g, '').replace(/^KEF:/i, '');
if (/^[0-9a-fA-F]+$/.test(cleanedHex) && cleanedHex.length > 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';
}