story-research-zapwall/lib/articleEncryption.ts

163 lines
4.0 KiB
TypeScript

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
*/
async function prepareEncryptionKey(key: string): Promise<CryptoKey> {
const keyBuffer = hexToArrayBuffer(key)
return crypto.subtle.importKey('raw', keyBuffer, { name: 'AES-GCM' }, false, ['encrypt'])
}
function prepareIV(iv: Uint8Array): { view: Uint8Array; buffer: ArrayBuffer } {
const ivBuffer = new ArrayBuffer(iv.byteLength)
const ivView = new Uint8Array(ivBuffer)
// Copy bytes from original IV
for (let i = 0; i < iv.length; i++) {
ivView[i] = iv[i] ?? 0
}
return { view: ivView, buffer: ivBuffer }
}
export async function encryptArticleContent(content: string): Promise<{
encryptedContent: string
key: string
iv: string
}> {
const key = generateEncryptionKey()
const iv = generateIV()
const cryptoKey = await prepareEncryptionKey(key)
const encoder = new TextEncoder()
const encodedContent = encoder.encode(content)
const { view: ivView, buffer: ivBuffer } = prepareIV(iv)
const encryptedBuffer = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: ivView as Uint8Array<ArrayBuffer> },
cryptoKey,
encodedContent
)
return {
encryptedContent: arrayBufferToHex(encryptedBuffer),
key,
iv: arrayBufferToHex(ivBuffer),
}
}
/**
* Decrypt article content with AES-GCM using the provided key and IV
*/
export async function decryptArticleContent(
encryptedContent: string,
key: string,
iv: string
): Promise<string> {
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<string> {
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<DecryptionKey> {
const decryptedJson = await Promise.resolve(nip04.decrypt(recipientPrivateKey, authorPublicKey, encryptedKey))
const keyData = JSON.parse(decryptedJson) as DecryptionKey
return keyData
}