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:
parent
f1903e1e61
commit
5c7cc4c6aa
8 changed files with 224 additions and 18 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue