From 1f887725fb9a4219be7b91d419f9ac0eefe48c7e Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 27 Jun 2026 07:02:47 -0700 Subject: [PATCH] DC-019: fix flaky backup-manager tamper test (authTag byte corruption) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- dashcaddy-api/__tests__/backup-manager.test.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/dashcaddy-api/__tests__/backup-manager.test.js b/dashcaddy-api/__tests__/backup-manager.test.js index 425ee5a..6f122b2 100644 --- a/dashcaddy-api/__tests__/backup-manager.test.js +++ b/dashcaddy-api/__tests__/backup-manager.test.js @@ -184,9 +184,16 @@ describe('BackupManager — backup/restore lifecycle', () => { it('rejects tampered data (auth tag mismatch)', async () => { const data = Buffer.from('test'); const encrypted = await backupManager.encryptBackup(data, testKey); - // Corrupt the first character of the IV - const str = encrypted.toString(); - const tampered = Buffer.from('X' + str.substring(1)); + // Corrupt the authTag so the GCM integrity check is guaranteed to fail. + // The format is iv:authTag:ciphertext (all base64). We flip all bits of + // 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)) .rejects.toThrow(); });