nomyo-js/tests/unit/crypto.test.ts
alpha-nerd-nomyo c7601b2270 fix:
- AES GCM protocol mismatch
- better, granular error handling
- UUID now uses crypto.randomUUID()
- added native mlock addon to improve security
- ZeroBuffer uses explicit_bzero now
- fixed imports

feat:
-  added unit tests
2026-03-04 11:30:44 +01:00

142 lines
5.7 KiB
TypeScript

/**
* 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('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);
});
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);
});
});