fix(security): close rotateEncryptionKey crash window with in-process key rollback (DC-107) [glm-grade=A]
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

If the atomicWriteJSON of rotated credentials failed after rotateKey() had
already persisted+cached the new key, the on-disk key could no longer decrypt
the on-disk credentials.json (permanent loss on restart). Catch-path now
restores the old key via new cryptoUtils.restoreKey() (canonical atomic-write,
0600, hex-validated) while still holding the proper-lockfile. Double-failure
(rollback throws) is contained and the lock is still released. Hard-crash
mid-rollback is covered by the existing .bak startup fallback.

Judge: GLM-4.6 stand-in, round-1 grade A, 0 blocking, 1 LOW polish (folded).
URN urn:ump:z2sz3x6abtcffpqyde2l2ssq34vy47t5h2gbmkr3wtmfxwkerinq
Tests: 121 suites / 2785 green (6 consecutive runs pre-fold; suite re-run post-fold).
This commit is contained in:
Hermes committed 2026-08-23 04:40:41 -07:00
1 parent eb2bab7a96
commit 5efacd11e8
3 files changed
+83 -2

No files matched your search

@@ -176,6 +176,7 @@ class CredentialManager {
*/
async rotateEncryptionKey() {
let release;
let oldKey = null; // DC-107: hoisted so the catch can roll back
try {
log.info('cred', 'Starting encryption key rotation');
@@ -203,7 +204,7 @@ class CredentialManager {
}
// Generate new key (this replaces the cached key and saves to disk)
const { oldKey } = cryptoUtils.rotateKey();
({ oldKey } = cryptoUtils.rotateKey());
// Re-encrypt all credentials with the new key
const rotated = {};
@@ -225,6 +226,24 @@ class CredentialManager {
log.info('cred', 'Rotated credentials', { count: keys.length });
return true;
} catch (error) {
// DC-107: in-process rollback — the write above failed after rotateKey()
// already swapped the on-disk key and in-memory cache. Restore the old
// key so this process keeps running with a key that can still read the
// on-disk credentials.json. (Hard-crash window between rotateKey() and
// the atomic write is covered separately by the startup .bak fallback
// in crypto-utils.loadOrCreateKey.)
if (oldKey) {
try {
cryptoUtils.restoreKey(oldKey.toString('hex'));
log.warn('cred', 'Rolled back encryption key after write failure');
} catch (rollbackError) {
// If THIS write fails too (or we hard-crash mid-rollback), restart
// recovery is covered by the startup .bak fallback in
// crypto-utils.loadOrCreateKey (KEY_FILE+.bak still holds the old
// key from rotateKey's pre-swap save).
log.error('cred', rollbackError, { operation: 'rotate-rollback' });
}
}
log.error('cred', error, { operation: 'rotate' });
return false;
} finally {