sync: detect orphaned blobs (pepper rotation) + fix AESGCM arg order

Adds an 8-byte HKDF fingerprint of the current pepper to portfolio_sync
rows. On fetch, a mismatch surfaces as 410 Gone (distinct from genuine
GCM corruption → 500), and the UI silently cleans up the dead row and
shows a soft "please re-import" notice instead of a confusing PIN
re-prompt. Legacy rows (pepper_fp NULL) are probed optimistically and
backfilled on success.

Also fixes a latent bug in unwrap(): AESGCM.decrypt args were swapped
(ct, nonce instead of nonce, ct), so restore-from-cloud always failed
even when the pepper was correct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-25 12:49:11 +02:00
parent f1903e1e61
commit 5c7cc4c6aa
8 changed files with 224 additions and 18 deletions

View file

@ -216,7 +216,23 @@
if (r.status === 402) return { exists: false, paid: false };
if (!r.ok) throw new Error('sync status: HTTP ' + r.status);
const body = await r.json();
return { exists: !!body.exists, updated_at: body.updated_at, paid: true };
return {
exists: !!body.exists,
orphaned: !!body.orphaned,
updated_at: body.updated_at,
paid: true,
};
}
// Thrown by pullSync when the server reports the stored blob is
// wrapped with a different server key (pepper rotation). Distinct
// from BadPinError so the UI can swap the restore form for a
// re-upload CTA instead of asking again for the PIN.
class StaleBlobError extends Error {
constructor(msg) {
super(msg || 'Stored portfolio cannot be decrypted with the current server key.');
this.name = 'StaleBlobError';
}
}
async function pushSync(pie, pin) {
@ -245,6 +261,10 @@
headers: { 'Accept': 'application/json' },
});
if (r.status === 404) return null;
if (r.status === 410) {
const body = await r.json().catch(() => ({}));
throw new StaleBlobError(body.detail);
}
if (!r.ok) {
const body = await r.json().catch(() => ({}));
// 429 → server already throttling; bubble the message up unchanged.
@ -273,6 +293,7 @@
disableSync,
clearCachedKey,
BadPinError,
StaleBlobError,
// Exposed for tests / debugging:
_packBlob: packBlob,
_unpackBlob: unpackBlob,