mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +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 {
|
try {
|
||||||
let mnemonic: string;
|
let mnemonic: string;
|
||||||
if (entry.inputType === 'krux') {
|
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
|
} else { // seedpgp
|
||||||
mnemonic = (await decryptFromSeed({ frameText: entry.rawInput, messagePassword: entry.passwordInput, mode: 'pgp' })).w;
|
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: {
|
export async function decryptFromKrux(params: {
|
||||||
kefHex: string;
|
kefData: string;
|
||||||
passphrase: string;
|
passphrase: string;
|
||||||
}): Promise<{ mnemonic: string; label: string; version: number; iterations: number }> {
|
}): Promise<{ mnemonic: string; label: string; version: number; iterations: number }> {
|
||||||
if (!params.passphrase) {
|
if (!params.passphrase) {
|
||||||
throw new Error("Passphrase is required for Krux decryption");
|
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 { label, version, iterations, payload } = unwrap(bytes);
|
||||||
const cipher = new KruxCipher(params.passphrase, label, iterations);
|
const cipher = new KruxCipher(params.passphrase, label, iterations);
|
||||||
const decrypted = await cipher.decrypt(payload, version);
|
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
|
* 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 {
|
export function detectEncryptionMode(text: string): EncryptionMode {
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
|
|
||||||
// 1. Check for SEEDPGP1 format (most specific)
|
// 1. Definite PGP
|
||||||
if (trimmed.startsWith('SEEDPGP1:')) {
|
if (trimmed.startsWith('SEEDPGP1:')) {
|
||||||
return 'pgp';
|
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, '');
|
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';
|
return 'krux';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. If it contains spaces, it's almost certainly a plain text mnemonic
|
// 3. Likely a plain text mnemonic
|
||||||
if (trimmed.includes(' ')) {
|
if (trimmed.includes(' ')) {
|
||||||
return 'text';
|
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';
|
return 'text';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user