273 lines
7.6 KiB
TypeScript
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;
|
|
}
|
|
}
|