5fa72ee8d048
fix: per-entry AES-GCM key isolation — HKDF info = entryId + 'sync-entry-v1' instead of shared salt (v3.6.97)
- time
- 2026-05-22 18:30 UTC · 5 months ago
- machine
- koh2
- parent
- 3f266d2afda8
↓ peach-browser-5fa72ee8.tar.gzatlas at this save
files changed1 file
src/background/chirp-sync-vault-adapter.ts+10 −7src/background/chirp-sync-vault-adapter.ts+10 −7
@@ -50,7 +50,9 @@5050 SignedDeviceManifest,5151 } from "../shared/sync-types";5252 53-const ENTRY_KEY_SALT = new TextEncoder().encode("peach-sync-entry-v1");53+// Each entry uses a unique AES-GCM key: HKDF(masterKey, info = entryId + "sync-entry-v1").54+// The info binding prevents a ciphertext from one entry slot being replayed into another.55+const ENTRY_KEY_INFO_SUFFIX = "sync-entry-v1";5456 const ENTRY_AAD = "peach-sync-entry-v1";5557 5658 export class VaultLockedError extends Error {@@ -109,12 +111,12 @@109111 async getEncryptedEntries(entryIds) {110112 requireUnlocked();111113 const masterKey = requireMasterKey();112- const entryKey = await deriveEntryKey(masterKey);113114 const out: Record<string, string> = {};114115 for (const id of entryIds) {115116 const entry = getEntryById(id);116117 if (!entry) continue;117- out[id] = await encryptEntry(entryKey, entry);118+ const key = await deriveEntryKey(masterKey, id);119+ out[id] = await encryptEntry(key, entry);118120 }119121 return out;120122 },@@ -122,10 +124,10 @@122124 async applyEntries(entries) {123125 requireUnlocked();124126 const masterKey = requireMasterKey();125- const entryKey = await deriveEntryKey(masterKey);126127 const decrypted: VaultEntry[] = [];127128 for (const [id, envelope] of Object.entries(entries)) {128- const entry = await decryptEntryOrNull(entryKey, envelope);129+ const key = await deriveEntryKey(masterKey, id);130+ const entry = await decryptEntryOrNull(key, envelope);129131 if (!entry) continue;130132 // The map key is the authority on id — refuse mismatched envelopes131133 // so a swapped envelope can't poison a different entry slot.@@ -254,8 +256,9 @@254256 return hexToBytes(hex);255257 }256258 257-async function deriveEntryKey(masterKey: Uint8Array): Promise<CryptoKey> {258- const raw = new HKDF(SHA256, masterKey, ENTRY_KEY_SALT, new Uint8Array(0)).expand(32);259+async function deriveEntryKey(masterKey: Uint8Array, entryId: string): Promise<CryptoKey> {260+ const info = new TextEncoder().encode(entryId + ENTRY_KEY_INFO_SUFFIX);261+ const raw = new HKDF(SHA256, masterKey, new Uint8Array(0), info).expand(32);259262 return crypto.subtle.importKey(260263 "raw",261264 raw.buffer as ArrayBuffer,