learning_ai_common_plat/packages/client-encrypt/src/aes-gcm.test.ts
saravanakumardb1 1bce981f43 feat(client-encrypt): create @bytelyst/client-encrypt — Web Crypto API encryption
- AES-256-GCM via SubtleCrypto (browsers + React Native with polyfill)
- Wire-compatible EncryptedField with @bytelyst/field-encrypt (server) and
  BLFieldEncrypt (Swift/Kotlin native SDKs)
- encryptField, decryptField, generateKey, keyFromHex, keyToHex
- PBKDF2 key derivation (600k iterations per OWASP 2023)
- isEncryptedField type guard, toHex/fromHex helpers
- 22 Vitest tests, all passing
- Add Web Crypto globals to root ESLint config
2026-03-21 11:15:27 -07:00

182 lines
6.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
encryptField,
decryptField,
generateKey,
keyFromHex,
keyToHex,
deriveKey,
} from './aes-gcm.js';
import { isEncryptedField } from './guards.js';
import { toHex, fromHex } from './hex.js';
describe('encryptField / decryptField', () => {
it('roundtrip', async () => {
const key = await generateKey();
const encrypted = await encryptField('Hello, World!', key, 'dek_test');
const decrypted = await decryptField(encrypted, key);
expect(decrypted).toBe('Hello, World!');
});
it('empty string', async () => {
const key = await generateKey();
const encrypted = await encryptField('', key, 'dek_test');
const decrypted = await decryptField(encrypted, key);
expect(decrypted).toBe('');
});
it('unicode', async () => {
const key = await generateKey();
const text = 'こんにちは世界 🌍 مرحبا Ñoño';
const encrypted = await encryptField(text, key, 'dek_test');
const decrypted = await decryptField(encrypted, key);
expect(decrypted).toBe(text);
});
it('large payload', async () => {
const key = await generateKey();
const text = 'A'.repeat(100_000);
const encrypted = await encryptField(text, key, 'dek_test');
const decrypted = await decryptField(encrypted, key);
expect(decrypted).toBe(text);
});
});
describe('EncryptedField structure', () => {
it('has correct sentinel fields', async () => {
const key = await generateKey();
const encrypted = await encryptField('test', key, 'dek_test');
expect(encrypted.__encrypted).toBe(true);
expect(encrypted.v).toBe(1);
expect(encrypted.alg).toBe('aes-256-gcm');
expect(encrypted.dekId).toBe('dek_test');
});
it('has correct hex lengths', async () => {
const key = await generateKey();
const encrypted = await encryptField('test', key, 'dek_test');
expect(encrypted.iv.length).toBe(24); // 12 bytes = 24 hex
expect(encrypted.tag.length).toBe(32); // 16 bytes = 32 hex
expect(encrypted.ct.length).toBeGreaterThan(0);
});
it('unique IVs per encryption', async () => {
const key = await generateKey();
const a = await encryptField('same', key, 'dek_test');
const b = await encryptField('same', key, 'dek_test');
expect(a.iv).not.toBe(b.iv);
expect(a.ct).not.toBe(b.ct);
});
});
describe('AAD (Additional Authenticated Data)', () => {
it('roundtrip with AAD', async () => {
const key = await generateKey();
const encrypted = await encryptField('secret', key, 'dek_test', 'user:ctx');
const decrypted = await decryptField(encrypted, key, 'user:ctx');
expect(decrypted).toBe('secret');
});
it('wrong AAD fails', async () => {
const key = await generateKey();
const encrypted = await encryptField('secret', key, 'dek_test', 'correct');
await expect(decryptField(encrypted, key, 'wrong')).rejects.toThrow();
});
it('missing AAD fails', async () => {
const key = await generateKey();
const encrypted = await encryptField('secret', key, 'dek_test', 'some-aad');
await expect(decryptField(encrypted, key)).rejects.toThrow();
});
});
describe('wrong key', () => {
it('decrypt with wrong key fails', async () => {
const key = await generateKey();
const wrongKey = await generateKey();
const encrypted = await encryptField('secret', key, 'dek_test');
await expect(decryptField(encrypted, wrongKey)).rejects.toThrow();
});
});
describe('keyFromHex / keyToHex', () => {
it('roundtrip', async () => {
const key = await generateKey();
const hex = await keyToHex(key);
expect(hex.length).toBe(64); // 32 bytes = 64 hex chars
const restored = await keyFromHex(hex);
const encrypted = await encryptField('test', key, 'dek_test');
const decrypted = await decryptField(encrypted, restored);
expect(decrypted).toBe('test');
});
it('rejects invalid length', async () => {
await expect(keyFromHex('aabb')).rejects.toThrow('32-byte key');
});
});
describe('deriveKey', () => {
it('derives consistent key from passphrase + salt', async () => {
const salt = new Uint8Array(16);
globalThis.crypto.getRandomValues(salt);
const key1 = await deriveKey('my-passphrase', salt, 1000, true);
const key2 = await deriveKey('my-passphrase', salt, 1000, true);
const hex1 = await keyToHex(key1);
const hex2 = await keyToHex(key2);
expect(hex1).toBe(hex2);
});
it('different passphrases produce different keys', async () => {
const salt = new Uint8Array(16);
globalThis.crypto.getRandomValues(salt);
const key1 = await deriveKey('pass-1', salt, 1000, true);
const key2 = await deriveKey('pass-2', salt, 1000, true);
const hex1 = await keyToHex(key1);
const hex2 = await keyToHex(key2);
expect(hex1).not.toBe(hex2);
});
it('derived key can encrypt/decrypt', async () => {
const salt = new Uint8Array(16);
globalThis.crypto.getRandomValues(salt);
const key = await deriveKey('test', salt, 1000, true);
const encrypted = await encryptField('hello', key, 'dek_test');
const decrypted = await decryptField(encrypted, key);
expect(decrypted).toBe('hello');
});
});
describe('isEncryptedField', () => {
it('true for valid EncryptedField', async () => {
const key = await generateKey();
const encrypted = await encryptField('test', key, 'dek_test');
expect(isEncryptedField(encrypted)).toBe(true);
});
it('false for plain string', () => {
expect(isEncryptedField('just a string')).toBe(false);
});
it('false for null', () => {
expect(isEncryptedField(null)).toBe(false);
});
it('false for incomplete object', () => {
expect(isEncryptedField({ __encrypted: true, v: 1 })).toBe(false);
});
});
describe('hex utilities', () => {
it('toHex / fromHex roundtrip', () => {
const bytes = new Uint8Array([0x00, 0x0f, 0xff, 0xab, 0xcd]);
const hex = toHex(bytes);
expect(hex).toBe('000fffabcd');
const restored = fromHex(hex);
expect(restored).toEqual(bytes);
});
it('fromHex rejects odd length', () => {
expect(() => fromHex('a')).toThrow('even length');
});
});