@@ -218,7 +218,10 @@ function deriveKey(masterKey: string, salt?: Buffer): Buffer {
218218 if ( salt ) {
219219 return pbkdf2Sync ( masterKey , salt , PBKDF2_ITERATIONS , 32 , 'sha256' ) ;
220220 }
221- // Legacy fallback — single SHA-256 (no brute-force resistance)
221+ // Legacy fallback — single SHA-256 (no brute-force resistance). Retained
222+ // ONLY to read pre-salt entries; those are re-encrypted with PBKDF2+salt on
223+ // first read via migrateLegacyEntryIfNeeded (CRYPTO-004), so this path is
224+ // self-healing and a no-salt entry should not persist past one read.
222225 return createHash ( 'sha256' ) . update ( masterKey ) . digest ( ) ;
223226}
224227
@@ -336,6 +339,25 @@ export class UserCredentialStore {
336339 return id ;
337340 }
338341
342+ /**
343+ * CRYPTO-004: lazily upgrade legacy (no-salt, single-SHA-256) entries to the
344+ * PBKDF2+salt scheme the first time they are successfully decrypted, so the
345+ * weak O(1) key derivation never survives a read. Non-fatal on failure — a
346+ * migration error must never break a credential read.
347+ */
348+ private async migrateLegacyEntryIfNeeded (
349+ entry : CredentialEntry ,
350+ plaintext : string
351+ ) : Promise < void > {
352+ if ( entry . salt ) return ;
353+ try {
354+ const { encrypted, iv, salt } = encryptValue ( plaintext , this . config . encryptionKey ) ;
355+ await this . backend . set ( { ...entry , encryptedValue : encrypted , iv, salt } ) ;
356+ } catch {
357+ // best-effort; keep serving the decrypted value
358+ }
359+ }
360+
339361 /**
340362 * Get a credential for use
341363 */
@@ -359,6 +381,9 @@ export class UserCredentialStore {
359381 entry . salt
360382 ) ;
361383
384+ // CRYPTO-004: upgrade legacy no-salt entries on read.
385+ await this . migrateLegacyEntryIfNeeded ( entry , value ) ;
386+
362387 // Update usage
363388 await this . updateUsage ( entry ) ;
364389
@@ -394,6 +419,9 @@ export class UserCredentialStore {
394419 entry . salt
395420 ) ;
396421
422+ // CRYPTO-004: upgrade legacy no-salt entries on read.
423+ await this . migrateLegacyEntryIfNeeded ( entry , value ) ;
424+
397425 // Update usage
398426 await this . updateUsage ( entry ) ;
399427
0 commit comments