- Add packages/auth/src/seal.ts: shared AES-256-GCM seal/unseal using BETTER_AUTH_SECRET - Export seal/unseal from @mosaicstack/auth index - Refactor provider-credentials.service.ts to import seal/unseal from @mosaicstack/auth - Add apps/gateway/src/federation/peer-key.util.ts: sealClientKey/unsealClientKey wrappers - Add peer-key.spec.ts with 5 vitest tests (round-trip, non-determinism, at-rest, tamper, missing secret) - Document key rotation deferred procedure in docs/federation/SETUP.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
64 lines
2.3 KiB
TypeScript
64 lines
2.3 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { sealClientKey, unsealClientKey } from '../peer-key.util.js';
|
|
|
|
const TEST_SECRET = 'test-secret-for-peer-key-unit-tests-only';
|
|
|
|
const TEST_PEM = `-----BEGIN PRIVATE KEY-----
|
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7o4qne60TB3wo
|
|
pCOW8QqstpxEBpnFo37JxLYEJbpE3gUlJajsHv9UWRQ7m5B7n+MBXwTCQqMEY8Wl
|
|
kHv9tGgz1YGwzBjNKxPJXE6pPTXQ1Oa0VB9l3qHdqF5HtZoJzE0c6dO8HJ5YUVL
|
|
-----END PRIVATE KEY-----`;
|
|
|
|
let savedSecret: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
savedSecret = process.env['BETTER_AUTH_SECRET'];
|
|
process.env['BETTER_AUTH_SECRET'] = TEST_SECRET;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (savedSecret === undefined) {
|
|
delete process.env['BETTER_AUTH_SECRET'];
|
|
} else {
|
|
process.env['BETTER_AUTH_SECRET'] = savedSecret;
|
|
}
|
|
});
|
|
|
|
describe('peer-key seal/unseal', () => {
|
|
it('round-trip: unsealClientKey(sealClientKey(pem)) returns original pem', () => {
|
|
const sealed = sealClientKey(TEST_PEM);
|
|
const roundTripped = unsealClientKey(sealed);
|
|
expect(roundTripped).toBe(TEST_PEM);
|
|
});
|
|
|
|
it('non-determinism: sealClientKey produces different ciphertext each call', () => {
|
|
const sealed1 = sealClientKey(TEST_PEM);
|
|
const sealed2 = sealClientKey(TEST_PEM);
|
|
expect(sealed1).not.toBe(sealed2);
|
|
});
|
|
|
|
it('at-rest: sealed output does not contain plaintext PEM content', () => {
|
|
const sealed = sealClientKey(TEST_PEM);
|
|
expect(sealed).not.toContain('PRIVATE KEY');
|
|
expect(sealed).not.toContain(
|
|
'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7o4qne60TB3wo',
|
|
);
|
|
});
|
|
|
|
it('tamper: flipping a byte in the sealed payload causes unseal to throw', () => {
|
|
const sealed = sealClientKey(TEST_PEM);
|
|
const buf = Buffer.from(sealed, 'base64');
|
|
// Flip a byte in the middle of the buffer (past IV and authTag)
|
|
const midpoint = Math.floor(buf.length / 2);
|
|
buf[midpoint] = buf[midpoint]! ^ 0xff;
|
|
const tampered = buf.toString('base64');
|
|
expect(() => unsealClientKey(tampered)).toThrow();
|
|
});
|
|
|
|
it('missing secret: unsealClientKey throws when BETTER_AUTH_SECRET is unset', () => {
|
|
const sealed = sealClientKey(TEST_PEM);
|
|
delete process.env['BETTER_AUTH_SECRET'];
|
|
expect(() => unsealClientKey(sealed)).toThrow('BETTER_AUTH_SECRET is not set');
|
|
});
|
|
});
|