mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-06 17:37:51 +08:00
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:
@@ -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
54
src/lib/base43.ts
Normal 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());
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user