- Remove @ts-expect-error comment from _unusedExtractTags - Remove eslint-disable comment from zapAggregation.ts and fix type properly - Remove unused _unusedExtractTags function - Remove unused decryptDecryptionKey import - Fix extractTagsFromEvent return type to include all optional properties explicitly - All errors fixed without disabling any controls
166 lines
3.8 KiB
TypeScript
166 lines
3.8 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
|
|
*/
|
|
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<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
|
|
}
|