- 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>
112 lines
2.4 KiB
TypeScript
112 lines
2.4 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|