nomyo-js/tests/unit/crypto.test.ts

215 lines
8.7 KiB
TypeScript
Raw Normal View History

/**
* Unit tests for crypto primitives (AES-256-GCM, RSA-OAEP)
*/
import { AESEncryption } from '../../src/core/crypto/encryption';
import { RSAOperations } from '../../src/core/crypto/rsa';
import { arrayBufferToBase64, base64ToArrayBuffer, stringToArrayBuffer, arrayBufferToString } from '../../src/core/crypto/utils';
describe('AESEncryption', () => {
let aes: AESEncryption;
beforeEach(() => {
aes = new AESEncryption();
});
test('generateKey produces 256-bit key', async () => {
const key = await aes.generateKey();
expect(key.type).toBe('secret');
expect((key.algorithm as AesKeyAlgorithm).length).toBe(256);
});
test('encrypt/decrypt roundtrip', async () => {
const key = await aes.generateKey();
const plaintext = stringToArrayBuffer('hello secure world');
const { ciphertext, nonce } = await aes.encrypt(plaintext, key);
expect(ciphertext.byteLength).toBeGreaterThan(0);
expect(nonce.byteLength).toBe(12);
// Web Crypto appends 16-byte tag — decrypt should succeed
const decrypted = await aes.decrypt(ciphertext, nonce, key);
expect(arrayBufferToString(decrypted)).toBe('hello secure world');
});
test('decrypt fails with wrong key', async () => {
const key1 = await aes.generateKey();
const key2 = await aes.generateKey();
const { ciphertext, nonce } = await aes.encrypt(stringToArrayBuffer('secret'), key1);
await expect(aes.decrypt(ciphertext, nonce, key2)).rejects.toThrow();
});
test('decrypt with wrong key throws generic message (no internal details)', async () => {
const key1 = await aes.generateKey();
const key2 = await aes.generateKey();
const { ciphertext, nonce } = await aes.encrypt(stringToArrayBuffer('secret'), key1);
await expect(aes.decrypt(ciphertext, nonce, key2))
.rejects.toThrow('AES-GCM decryption failed');
try {
await aes.decrypt(ciphertext, nonce, key2);
} catch (e) {
expect((e as Error).message).toBe('AES-GCM decryption failed');
}
});
test('exportKey / importKey roundtrip', async () => {
const key = await aes.generateKey();
const exported = await aes.exportKey(key);
expect(exported.byteLength).toBe(32); // 256-bit
const imported = await aes.importKey(exported);
const plaintext = stringToArrayBuffer('roundtrip test');
const { ciphertext, nonce } = await aes.encrypt(plaintext, imported);
const decrypted = await aes.decrypt(ciphertext, nonce, imported);
expect(arrayBufferToString(decrypted)).toBe('roundtrip test');
});
test('GCM tag split/join compatibility with Python format', async () => {
// Simulate what SecureCompletionClient.performEncryption does:
// split the 16-byte tag from Web Crypto output, then re-join for decrypt
const key = await aes.generateKey();
const plaintext = stringToArrayBuffer('tag split test');
const { ciphertext, nonce } = await aes.encrypt(plaintext, key);
const TAG_LENGTH = 16;
const ciphertextBytes = new Uint8Array(ciphertext);
const ciphertextOnly = ciphertextBytes.slice(0, ciphertextBytes.length - TAG_LENGTH);
const tag = ciphertextBytes.slice(ciphertextBytes.length - TAG_LENGTH);
// Re-join before decrypt (as in decryptResponse)
const combined = new Uint8Array(ciphertextOnly.length + tag.length);
combined.set(ciphertextOnly, 0);
combined.set(tag, ciphertextOnly.length);
const decrypted = await aes.decrypt(combined.buffer, nonce, key);
expect(arrayBufferToString(decrypted)).toBe('tag split test');
});
});
describe('RSAOperations', () => {
let rsa: RSAOperations;
beforeEach(() => {
rsa = new RSAOperations();
});
test('generateKeyPair produces usable 2048-bit keys', async () => {
const kp = await rsa.generateKeyPair(2048);
expect(kp.publicKey.type).toBe('public');
expect(kp.privateKey.type).toBe('private');
expect((kp.publicKey.algorithm as RsaHashedKeyAlgorithm).modulusLength).toBe(2048);
}, 30000);
test('encrypt/decrypt AES key roundtrip', async () => {
const kp = await rsa.generateKeyPair(2048);
const aes = new AESEncryption();
const aesKey = await aes.generateKey();
const aesKeyBytes = await aes.exportKey(aesKey);
const encrypted = await rsa.encryptKey(aesKeyBytes, kp.publicKey);
const decrypted = await rsa.decryptKey(encrypted, kp.privateKey);
expect(arrayBufferToBase64(decrypted)).toBe(arrayBufferToBase64(aesKeyBytes));
}, 30000);
test('exportPublicKey / importPublicKey roundtrip', async () => {
const kp = await rsa.generateKeyPair(2048);
const pem = await rsa.exportPublicKey(kp.publicKey);
expect(pem).toContain('-----BEGIN PUBLIC KEY-----');
const imported = await rsa.importPublicKey(pem);
expect(imported.type).toBe('public');
}, 30000);
test('exportPrivateKey / importPrivateKey roundtrip (no password)', async () => {
const kp = await rsa.generateKeyPair(2048);
const pem = await rsa.exportPrivateKey(kp.privateKey);
expect(pem).toContain('-----BEGIN PRIVATE KEY-----');
const imported = await rsa.importPrivateKey(pem);
expect(imported.type).toBe('private');
}, 30000);
test('importPrivateKey fails with wrong password', async () => {
const kp = await rsa.generateKeyPair(2048);
const pem = await rsa.exportPrivateKey(kp.privateKey, 'correct-password');
await expect(rsa.importPrivateKey(pem, 'wrong-password')).rejects.toThrow();
}, 30000);
test('decryptKey with wrong private key throws generic message', async () => {
const kp1 = await rsa.generateKeyPair(2048);
const kp2 = await rsa.generateKeyPair(2048);
const aes = new AESEncryption();
const aesKey = await aes.generateKey();
const aesKeyBytes = await aes.exportKey(aesKey);
const encrypted = await rsa.encryptKey(aesKeyBytes, kp1.publicKey);
await expect(rsa.decryptKey(encrypted, kp2.privateKey))
.rejects.toThrow('RSA key decryption failed');
try {
await rsa.decryptKey(encrypted, kp2.privateKey);
} catch (e) {
// Must not contain internal engine error details
expect((e as Error).message).toBe('RSA key decryption failed');
}
}, 30000);
});
describe('KeyManager password validation', () => {
test('generateKeys rejects password shorter than 8 characters', async () => {
const { KeyManager } = await import('../../src/core/crypto/keys');
const km = new KeyManager();
await expect(km.generateKeys({ keySize: 2048, password: 'short' }))
.rejects.toThrow('at least 8 characters');
}, 30000);
test('generateKeys rejects empty password', async () => {
const { KeyManager } = await import('../../src/core/crypto/keys');
const km = new KeyManager();
await expect(km.generateKeys({ keySize: 2048, password: '' }))
.rejects.toThrow('at least 8 characters');
}, 30000);
test('generateKeys accepts password of exactly 8 characters', async () => {
const { KeyManager } = await import('../../src/core/crypto/keys');
const km = new KeyManager();
await expect(km.generateKeys({ keySize: 2048, password: '12345678' }))
.resolves.toBeUndefined();
}, 30000);
test('generateKeys accepts undefined password (no encryption)', async () => {
const { KeyManager } = await import('../../src/core/crypto/keys');
const km = new KeyManager();
await expect(km.generateKeys({ keySize: 2048 }))
.resolves.toBeUndefined();
}, 30000);
test('zeroKeys clears key references', async () => {
const { KeyManager } = await import('../../src/core/crypto/keys');
const km = new KeyManager();
await km.generateKeys({ keySize: 2048 });
expect(km.hasKeys()).toBe(true);
km.zeroKeys();
expect(km.hasKeys()).toBe(false);
}, 30000);
});
describe('Base64 utilities', () => {
test('arrayBufferToBase64 / base64ToArrayBuffer roundtrip', () => {
const original = new Uint8Array([0, 1, 127, 128, 255]);
const b64 = arrayBufferToBase64(original.buffer);
const restored = new Uint8Array(base64ToArrayBuffer(b64));
expect(Array.from(restored)).toEqual([0, 1, 127, 128, 255]);
});
test('stringToArrayBuffer / arrayBufferToString roundtrip', () => {
const text = 'Hello, 世界! 🔐';
const buf = stringToArrayBuffer(text);
expect(arrayBufferToString(buf)).toBe(text);
});
});