AeThex-Connect/packages/core/crypto/CryptoManager.ts

273 lines
7.6 KiB
TypeScript

/**
* 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<void> {
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<KeyPair | null> {
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;
}
}