DC-019: fix flaky backup-manager tamper test (authTag byte corruption)

The 'rejects tampered data (auth tag mismatch)' test corrupted the
encrypted blob by replacing its first base64 char with 'X'. When the
random 16-byte IV's first base64 char was already 'X' (~1/64 chance),
the replacement was a no-op and decryption succeeded — causing the test
to flake ~1.6% of runs.

Fix: parse the iv:authTag:ciphertext format, XOR the first authTag byte
with 0xFF (guaranteed to change the value), reassemble. This reliably
triggers the AES-256-GCM integrity failure every time.

Verified: 30/30 isolated runs + 8/8 full-suite runs (1036/1036), zero
failures. The production encryptBackup/decryptBackup (AES-256-GCM)
code is correct and unchanged.
This commit is contained in:
Hermes
2026-06-27 07:02:47 -07:00
parent c1ac0baa5e
commit 1f887725fb
+10 -3
View File
@@ -184,9 +184,16 @@ describe('BackupManager — backup/restore lifecycle', () => {
it('rejects tampered data (auth tag mismatch)', async () => { it('rejects tampered data (auth tag mismatch)', async () => {
const data = Buffer.from('test'); const data = Buffer.from('test');
const encrypted = await backupManager.encryptBackup(data, testKey); const encrypted = await backupManager.encryptBackup(data, testKey);
// Corrupt the first character of the IV // Corrupt the authTag so the GCM integrity check is guaranteed to fail.
const str = encrypted.toString(); // The format is iv:authTag:ciphertext (all base64). We flip all bits of
const tampered = Buffer.from('X' + str.substring(1)); // the first authTag byte — XOR with 0xFF always changes the value, so
// this can never be a no-op (unlike replacing a base64 char with a fixed
// char, which collides ~1/64 of the time when that char already matches).
const parts = encrypted.toString().split(':');
const authTagBuf = Buffer.from(parts[1], 'base64');
authTagBuf[0] ^= 0xFF;
parts[1] = authTagBuf.toString('base64');
const tampered = Buffer.from(parts.join(':'));
await expect(backupManager.decryptBackup(tampered, testKey)) await expect(backupManager.decryptBackup(tampered, testKey))
.rejects.toThrow(); .rejects.toThrow();
}); });