163 lines
4.0 KiB
TypeScript
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
|
|
}
|