mirror of
https://github.com/kccleoc/seedpgp-web.git
synced 2026-03-07 09:57:50 +08:00
feat: fix CompactSeedQR binary QR code scanning with jsQR library
- 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.
This commit is contained in:
@@ -17,7 +17,10 @@ export const VERSIONS: Record<number, {
|
||||
const GCM_IV_LENGTH = 12;
|
||||
|
||||
function toArrayBuffer(data: Uint8Array): ArrayBuffer {
|
||||
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
||||
// Create a new ArrayBuffer and copy the contents
|
||||
const buffer = new ArrayBuffer(data.byteLength);
|
||||
new Uint8Array(buffer).set(data);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function unwrap(envelope: Uint8Array): { label: string; labelBytes: Uint8Array, version: number; iterations: number; payload: Uint8Array } {
|
||||
@@ -40,18 +43,26 @@ export function unwrap(envelope: Uint8Array): { label: string; labelBytes: Uint8
|
||||
return { label, labelBytes, version, iterations, payload };
|
||||
}
|
||||
|
||||
import { pbkdf2HmacSha256 } from './pbkdf2';
|
||||
import { entropyToMnemonic, mnemonicToEntropy } from './seedblend';
|
||||
|
||||
// ... (rest of the file is the same until KruxCipher)
|
||||
|
||||
export class KruxCipher {
|
||||
private keyPromise: Promise<CryptoKey>;
|
||||
|
||||
constructor(passphrase: string, salt: Uint8Array, iterations: number) {
|
||||
const encoder = new TextEncoder();
|
||||
this.keyPromise = (async () => {
|
||||
const passphraseBuffer = toArrayBuffer(encoder.encode(passphrase));
|
||||
const baseKey = await crypto.subtle.importKey("raw", passphraseBuffer, { name: "PBKDF2" }, false, ["deriveKey"]);
|
||||
const saltBuffer = toArrayBuffer(salt); // Use the raw bytes directly
|
||||
return crypto.subtle.deriveKey(
|
||||
{ name: "PBKDF2", salt: saltBuffer, iterations: Math.max(1, iterations), hash: "SHA-256" },
|
||||
baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]
|
||||
// Use pure-JS PBKDF2 implementation which has been validated against Krux's test vector
|
||||
const derivedKeyBytes = await pbkdf2HmacSha256(passphrase, salt, iterations, 32);
|
||||
|
||||
// Import the derived bytes as an AES-GCM key
|
||||
return crypto.subtle.importKey(
|
||||
"raw",
|
||||
toArrayBuffer(derivedKeyBytes),
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
})();
|
||||
}
|
||||
@@ -149,7 +160,7 @@ export async function decryptFromKrux(params: { kefData: string; passphrase: str
|
||||
const cipher = new KruxCipher(passphrase, labelBytes, iterations);
|
||||
const decrypted = await cipher.decrypt(payload, version);
|
||||
|
||||
const mnemonic = new TextDecoder().decode(decrypted);
|
||||
const mnemonic = await entropyToMnemonic(decrypted);
|
||||
return { mnemonic, label, version, iterations };
|
||||
}
|
||||
|
||||
@@ -161,10 +172,36 @@ export async function encryptToKrux(params: { mnemonic: string; passphrase: stri
|
||||
const { mnemonic, passphrase, label = "Seed Backup", iterations = 200000, version = 21 } = params;
|
||||
if (!passphrase) throw new Error("Passphrase is required for Krux encryption");
|
||||
|
||||
const mnemonicBytes = new TextEncoder().encode(mnemonic);
|
||||
const mnemonicBytes = await mnemonicToEntropy(mnemonic);
|
||||
// For encryption, we encode the string label to get the salt bytes
|
||||
const cipher = new KruxCipher(passphrase, new TextEncoder().encode(label), iterations);
|
||||
const payload = await cipher.encrypt(mnemonicBytes, version);
|
||||
const kef = wrap(label, version, iterations, payload);
|
||||
return { kefHex: bytesToHex(kef), label, version, iterations };
|
||||
}
|
||||
|
||||
export function wrap(label: string, version: number, iterations: number, payload: Uint8Array): Uint8Array {
|
||||
const labelBytes = new TextEncoder().encode(label);
|
||||
const idLen = labelBytes.length;
|
||||
|
||||
// Convert iterations to 3 bytes (Big-Endian)
|
||||
const iterBytes = new Uint8Array(3);
|
||||
iterBytes[0] = (iterations >> 16) & 0xFF;
|
||||
iterBytes[1] = (iterations >> 8) & 0xFF;
|
||||
iterBytes[2] = iterations & 0xFF;
|
||||
|
||||
// Calculate total length
|
||||
const totalLength = 1 + idLen + 1 + 3 + payload.length;
|
||||
const envelope = new Uint8Array(totalLength);
|
||||
|
||||
let offset = 0;
|
||||
envelope[offset++] = idLen;
|
||||
envelope.set(labelBytes, offset);
|
||||
offset += idLen;
|
||||
envelope[offset++] = version;
|
||||
envelope.set(iterBytes, offset);
|
||||
offset += 3;
|
||||
envelope.set(payload, offset);
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user