/** * End-to-End Encryption Manager * Provides E2E encryption using NaCl (TweetNaCl) */ import * as nacl from 'tweetnacl'; import * as util from 'tweetnacl-util'; export interface KeyPair { publicKey: string; secretKey: string; } export interface EncryptedMessage { ciphertext: string; nonce: string; ephemeralPublicKey: string; } export class CryptoManager { private keyPair: nacl.BoxKeyPair | null = null; /** * Generate a new key pair */ generateKeyPair(): KeyPair { this.keyPair = nacl.box.keyPair(); return { publicKey: util.encodeBase64(this.keyPair.publicKey), secretKey: util.encodeBase64(this.keyPair.secretKey), }; } /** * Load existing key pair */ loadKeyPair(keyPair: KeyPair): void { this.keyPair = { publicKey: util.decodeBase64(keyPair.publicKey), secretKey: util.decodeBase64(keyPair.secretKey), }; } /** * Encrypt a message for a recipient */ encrypt(message: string, recipientPublicKey: string): EncryptedMessage { if (!this.keyPair) { throw new Error('Key pair not initialized'); } // Generate ephemeral key pair for this message const ephemeralKeyPair = nacl.box.keyPair(); // Generate random nonce const nonce = nacl.randomBytes(nacl.box.nonceLength); // Convert message to Uint8Array const messageUint8 = util.decodeUTF8(message); // Decrypt recipient's public key const recipientPublicKeyUint8 = util.decodeBase64(recipientPublicKey); // Encrypt message const ciphertext = nacl.box( messageUint8, nonce, recipientPublicKeyUint8, ephemeralKeyPair.secretKey ); return { ciphertext: util.encodeBase64(ciphertext), nonce: util.encodeBase64(nonce), ephemeralPublicKey: util.encodeBase64(ephemeralKeyPair.publicKey), }; } /** * Decrypt a message */ decrypt(encryptedMessage: EncryptedMessage): string { if (!this.keyPair) { throw new Error('Key pair not initialized'); } // Decode components const ciphertext = util.decodeBase64(encryptedMessage.ciphertext); const nonce = util.decodeBase64(encryptedMessage.nonce); const ephemeralPublicKey = util.decodeBase64(encryptedMessage.ephemeralPublicKey); // Decrypt message const decrypted = nacl.box.open( ciphertext, nonce, ephemeralPublicKey, this.keyPair.secretKey ); if (!decrypted) { throw new Error('Failed to decrypt message'); } return util.encodeUTF8(decrypted); } /** * Generate a symmetric key for group encryption */ generateSymmetricKey(): string { const key = nacl.randomBytes(nacl.secretbox.keyLength); return util.encodeBase64(key); } /** * Encrypt with symmetric key (for group messages) */ encryptSymmetric(message: string, key: string): { ciphertext: string; nonce: string } { const keyUint8 = util.decodeBase64(key); const nonce = nacl.randomBytes(nacl.secretbox.nonceLength); const messageUint8 = util.decodeUTF8(message); const ciphertext = nacl.secretbox(messageUint8, nonce, keyUint8); return { ciphertext: util.encodeBase64(ciphertext), nonce: util.encodeBase64(nonce), }; } /** * Decrypt with symmetric key */ decryptSymmetric(ciphertext: string, nonce: string, key: string): string { const keyUint8 = util.decodeBase64(key); const nonceUint8 = util.decodeBase64(nonce); const ciphertextUint8 = util.decodeBase64(ciphertext); const decrypted = nacl.secretbox.open(ciphertextUint8, nonceUint8, keyUint8); if (!decrypted) { throw new Error('Failed to decrypt message'); } return util.encodeUTF8(decrypted); } /** * Hash a value (for password hashing, checksums, etc.) */ hash(value: string): string { const valueUint8 = util.decodeUTF8(value); const hash = nacl.hash(valueUint8); return util.encodeBase64(hash); } /** * Generate a random string (for tokens, IDs, etc.) */ generateRandomString(length: number = 32): string { const randomBytes = nacl.randomBytes(length); return util.encodeBase64(randomBytes); } /** * Sign a message */ sign(message: string): string { if (!this.keyPair) { throw new Error('Key pair not initialized'); } // Generate signing key pair from box key pair const signingKeyPair = nacl.sign.keyPair.fromSeed(this.keyPair.secretKey.slice(0, 32)); const messageUint8 = util.decodeUTF8(message); const signature = nacl.sign.detached(messageUint8, signingKeyPair.secretKey); return util.encodeBase64(signature); } /** * Verify a signature */ verify(message: string, signature: string, publicKey: string): boolean { const messageUint8 = util.decodeUTF8(message); const signatureUint8 = util.decodeBase64(signature); // Derive signing public key from box public key const boxPublicKey = util.decodeBase64(publicKey); // For simplicity, we'll use the first 32 bytes as signing key // In production, you'd want a separate signing key pair const signingPublicKey = boxPublicKey.slice(0, 32); return nacl.sign.detached.verify(messageUint8, signatureUint8, signingPublicKey); } /** * Store keys securely (localStorage with encryption) */ async storeKeys(password: string): Promise { if (!this.keyPair) { throw new Error('Key pair not initialized'); } // Derive a key from password const passwordHash = this.hash(password); const derivedKey = util.decodeBase64(passwordHash).slice(0, nacl.secretbox.keyLength); // Encrypt secret key const nonce = nacl.randomBytes(nacl.secretbox.nonceLength); const encryptedSecretKey = nacl.secretbox(this.keyPair.secretKey, nonce, derivedKey); // Store in localStorage localStorage.setItem('aethex_public_key', util.encodeBase64(this.keyPair.publicKey)); localStorage.setItem('aethex_encrypted_secret_key', util.encodeBase64(encryptedSecretKey)); localStorage.setItem('aethex_key_nonce', util.encodeBase64(nonce)); } /** * Load keys from storage */ async loadStoredKeys(password: string): Promise { const publicKeyStr = localStorage.getItem('aethex_public_key'); const encryptedSecretKeyStr = localStorage.getItem('aethex_encrypted_secret_key'); const nonceStr = localStorage.getItem('aethex_key_nonce'); if (!publicKeyStr || !encryptedSecretKeyStr || !nonceStr) { return null; } try { // Derive key from password const passwordHash = this.hash(password); const derivedKey = util.decodeBase64(passwordHash).slice(0, nacl.secretbox.keyLength); // Decrypt secret key const encryptedSecretKey = util.decodeBase64(encryptedSecretKeyStr); const nonce = util.decodeBase64(nonceStr); const secretKey = nacl.secretbox.open(encryptedSecretKey, nonce, derivedKey); if (!secretKey) { throw new Error('Failed to decrypt secret key'); } const publicKey = util.decodeBase64(publicKeyStr); this.keyPair = { publicKey, secretKey, }; return { publicKey: publicKeyStr, secretKey: util.encodeBase64(secretKey), }; } catch (error) { console.error('Failed to load keys:', error); return null; } } /** * Clear stored keys */ clearStoredKeys(): void { localStorage.removeItem('aethex_public_key'); localStorage.removeItem('aethex_encrypted_secret_key'); localStorage.removeItem('aethex_key_nonce'); this.keyPair = null; } }