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
This commit is contained in:
parent
0b09b9a9c3
commit
c7601b2270
17 changed files with 12600 additions and 164 deletions
142
tests/unit/crypto.test.ts
Normal file
142
tests/unit/crypto.test.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue