import { nip04 } from 'nostr-tools' /** * Encryption service for article content * Uses AES-GCM for content encryption and NIP-04 for key encryption */ export interface EncryptedArticleContent { encryptedContent: string encryptedKey: string iv: string } export interface DecryptionKey { key: string iv: string } /** * Generate a random encryption key for AES-GCM */ function generateEncryptionKey(): string { const keyBytes = crypto.getRandomValues(new Uint8Array(32)) return Array.from(keyBytes) .map((b) => b.toString(16).padStart(2, '0')) .join('') } /** * Generate a random IV for AES-GCM */ function generateIV(): Uint8Array { return crypto.getRandomValues(new Uint8Array(12)) } /** * Convert hex string to ArrayBuffer */ function hexToArrayBuffer(hex: string): ArrayBuffer { const bytes = new Uint8Array(hex.length / 2) for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substr(i, 2), 16) } return bytes.buffer } /** * Convert ArrayBuffer to hex string */ function arrayBufferToHex(buffer: ArrayBuffer): string { const bytes = new Uint8Array(buffer) return Array.from(bytes) .map((b) => b.toString(16).padStart(2, '0')) .join('') } /** * Encrypt article content with AES-GCM * Returns encrypted content, IV, and the encryption key */ export async function encryptArticleContent(content: string): Promise<{ encryptedContent: string key: string iv: string }> { const key = generateEncryptionKey() const iv = generateIV() const keyBuffer = hexToArrayBuffer(key) const cryptoKey = await crypto.subtle.importKey( 'raw', keyBuffer, { name: 'AES-GCM' }, false, ['encrypt'] ) const encoder = new TextEncoder() const encodedContent = encoder.encode(content) const ivBuffer = iv.buffer instanceof ArrayBuffer ? iv.buffer : new ArrayBuffer(iv.byteLength) const ivView = new Uint8Array(ivBuffer, 0, iv.byteLength) ivView.set(iv) const encryptedBuffer = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: ivView, }, cryptoKey, encodedContent ) const encryptedContent = arrayBufferToHex(encryptedBuffer) const ivHex = arrayBufferToHex(ivView.buffer) return { encryptedContent, key, iv: ivHex, } } /** * Decrypt article content with AES-GCM using the provided key and IV */ export async function decryptArticleContent( encryptedContent: string, key: string, iv: string ): Promise { const keyBuffer = hexToArrayBuffer(key) const ivBuffer = hexToArrayBuffer(iv) const cryptoKey = await crypto.subtle.importKey( 'raw', keyBuffer, { name: 'AES-GCM' }, false, ['decrypt'] ) const encryptedBuffer = hexToArrayBuffer(encryptedContent) const decryptedBuffer = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: ivBuffer, }, cryptoKey, encryptedBuffer ) const decoder = new TextDecoder() return decoder.decode(decryptedBuffer) } /** * Encrypt the decryption key using NIP-04 (for storage in tags) * The key is encrypted with the author's public key */ export async function encryptDecryptionKey( key: string, iv: string, authorPrivateKey: string, authorPublicKey: string ): Promise { const keyData: DecryptionKey = { key, iv } const keyJson = JSON.stringify(keyData) const encryptedKey = await Promise.resolve(nip04.encrypt(authorPrivateKey, authorPublicKey, keyJson)) return encryptedKey } /** * Decrypt the decryption key from a private message */ export async function decryptDecryptionKey( encryptedKey: string, recipientPrivateKey: string, authorPublicKey: string ): Promise { const decryptedJson = await Promise.resolve(nip04.decrypt(recipientPrivateKey, authorPublicKey, encryptedKey)) const keyData = JSON.parse(decryptedJson) as DecryptionKey return keyData }