Fix WASM decrypt: store SPK secret, pass to decrypt_wire_message

Root cause: WASM was regenerating random pre-keys on every call to
decrypt_wire_message, instead of using the SPK that was registered
with the server. CLI sender encrypts to the registered SPK, but
WASM was trying to decrypt with a different random key.

Fix:
- WasmIdentity now stores spk_secret_bytes internally
- SPK secret persisted to localStorage as 'wz-spk'
- On load: restored from localStorage, not regenerated
- bundle_bytes() uses stored SPK secret (cached, deterministic)
- decrypt_wire_message() takes spk_secret_hex parameter
- Web UI passes stored SPK to all decrypt calls

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 08:52:44 +04:00
parent ab296df825
commit 99da095a0f
4 changed files with 106 additions and 35 deletions

View File

@@ -177,10 +177,25 @@ async function initWasm() {
wasmReady = true;
}
let mySpkSecretHex = '';
function initIdentityFromSeed(hexSeed) {
wasmIdentity = WasmIdentity.from_hex_seed(hexSeed);
myFingerprint = wasmIdentity.fingerprint();
mySeedHex = wasmIdentity.seed_hex();
// Restore or generate SPK secret
const savedSpk = localStorage.getItem('wz-spk');
if (savedSpk) {
wasmIdentity.set_spk_secret_hex(savedSpk);
mySpkSecretHex = savedSpk;
dbg('Restored SPK secret from localStorage');
} else {
mySpkSecretHex = wasmIdentity.spk_secret_hex();
localStorage.setItem('wz-spk', mySpkSecretHex);
dbg('Generated new SPK secret');
}
localStorage.setItem('wz-seed', mySeedHex);
localStorage.setItem('wz-fp', myFingerprint);
}
@@ -189,8 +204,11 @@ function generateNewIdentity() {
wasmIdentity = new WasmIdentity();
myFingerprint = wasmIdentity.fingerprint();
mySeedHex = wasmIdentity.seed_hex();
mySpkSecretHex = wasmIdentity.spk_secret_hex();
localStorage.setItem('wz-seed', mySeedHex);
localStorage.setItem('wz-fp', myFingerprint);
localStorage.setItem('wz-spk', mySpkSecretHex);
dbg('New identity, SPK secret stored');
}
function loadSavedIdentity() {
@@ -198,7 +216,7 @@ function loadSavedIdentity() {
if (!saved) { dbg('No saved seed'); return false; }
try {
initIdentityFromSeed(saved);
dbg('Loaded identity:', myFingerprint);
dbg('Loaded identity:', myFingerprint, 'has SPK:', !!mySpkSecretHex);
return true;
} catch(e) {
dbg('Failed to load identity:', e);
@@ -295,7 +313,7 @@ async function pollMessages() {
let decrypted = false;
try {
dbg('Trying decrypt as KeyExchange (no session)...');
const resultStr = decrypt_wire_message(mySeedHex, bytes, null);
const resultStr = decrypt_wire_message(mySeedHex, mySpkSecretHex, bytes, null);
const result = JSON.parse(resultStr);
dbg('Decrypted!', result.new_session ? 'new session' : 'existing', 'from:', result.sender);
@@ -323,7 +341,7 @@ async function pollMessages() {
for (const [senderFP, sessData] of Object.entries(sessions)) {
try {
dbg('Trying session for', senderFP);
const resultStr = decrypt_wire_message(mySeedHex, bytes, sessData.data);
const resultStr = decrypt_wire_message(mySeedHex, mySpkSecretHex, bytes, sessData.data);
const result = JSON.parse(resultStr);
dbg('Decrypted with session', senderFP, ':', result.text.slice(0, 30));