feat(crypto): add AES-GCM encryption utilities

- Add arrayToBase64/base64ToArray conversion functions
- Add deriveKey for PBKDF2 key derivation
- Add encrypt/decrypt using AES-GCM
- Add generateMasterKey for random key generation
- Update setup.ts to use real Web Crypto API instead of mock
- Add comprehensive unit tests for all crypto functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-03-21 17:05:15 +08:00
parent 47a84f52a2
commit f070d9151e
3 changed files with 197 additions and 18 deletions

View File

@@ -0,0 +1,111 @@
/**
* Cryptographic utilities for secure storage
* Uses Web Crypto API for AES-GCM encryption
*/
const SALT = new TextEncoder().encode('zclaw-secure-storage-salt');
const ITERATIONS = 100000;
/**
* Convert Uint8Array to base64 string
*/
export function arrayToBase64(array: Uint8Array): string {
if (array.length === 0) return '';
let binary = '';
for (let i = 0; i < array.length; i++) {
binary += String.fromCharCode(array[i]);
}
return btoa(binary);
}
/**
* Convert base64 string to Uint8Array
*/
export function base64ToArray(base64: string): Uint8Array {
if (!base64) return new Uint8Array([]);
const binary = atob(base64);
const array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
array[i] = binary.charCodeAt(i);
}
return array;
}
/**
* Derive an encryption key from a master key
*/
export async function deriveKey(
masterKey: string,
salt: Uint8Array = SALT
): Promise<CryptoKey> {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(masterKey),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: ITERATIONS,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
/**
* Encrypt data using AES-GCM
*/
export async function encrypt(
plaintext: string,
key: CryptoKey
): Promise<{ iv: string; data: string }> {
const encoder = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoder.encode(plaintext)
);
return {
iv: arrayToBase64(iv),
data: arrayToBase64(new Uint8Array(encrypted)),
};
}
/**
* Decrypt data using AES-GCM
*/
export async function decrypt(
encrypted: { iv: string; data: string },
key: CryptoKey
): Promise<string> {
const decoder = new TextDecoder();
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: base64ToArray(encrypted.iv) },
key,
base64ToArray(encrypted.data)
);
return decoder.decode(decrypted);
}
/**
* Generate a random master key for encryption
*/
export function generateMasterKey(): string {
const array = crypto.getRandomValues(new Uint8Array(32));
return arrayToBase64(array);
}