Files
pn-new-crm/tests/unit/services/backup-bundle-encryption.test.ts

76 lines
2.6 KiB
TypeScript
Raw Normal View History

/**
* Unit test for opt-in backup-bundle encryption
* (`src/lib/services/backup-destinations/bundle-encryption.ts`).
*
* Contract: AES-256-GCM streaming encryption with a scrypt-derived key.
* - round-trips arbitrary bytes (small + multi-chunk),
* - rejects the wrong passphrase (GCM auth failure),
* - rejects tampered ciphertext (GCM auth failure).
*/
import { createHash, randomBytes } from 'node:crypto';
import { mkdtempSync, readFileSync, rmSync, writeFileSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
decryptFileToFile,
encryptFileToFile,
} from '@/lib/services/backup-destinations/bundle-encryption';
function sha(p: string): string {
return createHash('sha256').update(readFileSync(p)).digest('hex');
}
describe('backup bundle encryption', () => {
let dir: string;
beforeEach(() => {
dir = mkdtempSync(path.join(tmpdir(), 'pn-enc-'));
});
afterEach(() => {
rmSync(dir, { recursive: true, force: true });
});
it('round-trips a multi-chunk file with the correct passphrase', async () => {
const plain = path.join(dir, 'bundle.tar');
// ~400 KB of pseudo-random bytes → multiple cipher chunks.
writeFileSync(plain, randomBytes(400 * 1024));
const enc = path.join(dir, 'bundle.tar.enc');
const dec = path.join(dir, 'bundle.roundtrip.tar');
await encryptFileToFile(plain, enc, 'correct horse battery staple');
// Ciphertext must differ from plaintext and not be empty.
expect(statSync(enc).size).toBeGreaterThan(0);
expect(sha(enc)).not.toBe(sha(plain));
await decryptFileToFile(enc, dec, 'correct horse battery staple');
expect(sha(dec)).toBe(sha(plain));
});
it('rejects the wrong passphrase', async () => {
const plain = path.join(dir, 'b.tar');
writeFileSync(plain, Buffer.from('top secret contract bytes'));
const enc = path.join(dir, 'b.tar.enc');
await encryptFileToFile(plain, enc, 'right-pass');
await expect(decryptFileToFile(enc, path.join(dir, 'out.tar'), 'WRONG-pass')).rejects.toThrow();
});
it('rejects tampered ciphertext', async () => {
const plain = path.join(dir, 'c.tar');
writeFileSync(plain, randomBytes(64 * 1024));
const enc = path.join(dir, 'c.tar.enc');
await encryptFileToFile(plain, enc, 'pw');
// Flip a byte in the middle of the ciphertext region.
const buf = readFileSync(enc);
const mid = Math.floor(buf.length / 2);
buf[mid] = (buf[mid] ?? 0) ^ 0xff;
writeFileSync(enc, buf);
await expect(decryptFileToFile(enc, path.join(dir, 'out.tar'), 'pw')).rejects.toThrow();
});
});