Implement end-to-end encryption for response events with key tag

This commit is contained in:
complex 2025-04-09 15:10:03 +02:00
parent b232a80dbb
commit cb320765bb

@ -547,7 +547,7 @@ export class NostrHttpServer {
// Check rate limit
if (!this.checkRateLimit(event.pubkey)) {
await this.sendErrorResponse(event.id, 'Rate limit exceeded');
await this.sendErrorResponse(event.id, 'Rate limit exceeded', event.pubkey);
return;
}
@ -619,10 +619,10 @@ export class NostrHttpServer {
console.log('response', response);
// Send response
await this.sendResponseEvent(event.id, response);
await this.sendResponseEvent(event.id, response, event.pubkey);
} catch (error) {
console.error('Error processing event:', error);
await this.sendErrorResponse(event.id, error instanceof Error ? error.message : 'Unknown error');
await this.sendErrorResponse(event.id, error instanceof Error ? error.message : 'Unknown error', event.pubkey);
}
}
@ -821,16 +821,34 @@ ${responseBody}`;
* Send response event
* @param requestId The ID of the request event
* @param responseContent The response content to send
* @param senderPubkey The public key of the sender to encrypt the response to
*/
private async sendResponseEvent(requestId: string, responseContent: string): Promise<void> {
private async sendResponseEvent(requestId: string, responseContent: string, senderPubkey: string): Promise<void> {
try {
// Generate a random decryption key for AES-GCM encryption
const decryptionKey = crypto.randomBytes(32).toString('hex');
// Encrypt the response content using AES-GCM with the decryption key
const encryptedContent = await this.encryptWithAesGcm(responseContent, decryptionKey);
if (!encryptedContent) {
throw new Error('Failed to encrypt response content with AES-GCM');
}
// Encrypt the decryption key using NIP-44 to the sender's public key
const encryptedKey = await this.encryptContent(senderPubkey, decryptionKey);
if (!encryptedKey) {
throw new Error('Failed to encrypt decryption key with NIP-44');
}
const rawEvent = {
kind: 21121,
content: responseContent,
content: encryptedContent,
created_at: Math.floor(Date.now() / 1000),
pubkey: this.config.pubkey,
tags: [
['e', requestId],
['p', senderPubkey], // Add 'p' tag with the client's public key
['key', encryptedKey], // Add encrypted key tag for decryption
['expiration', (Math.floor(Date.now() / 1000) + 3600).toString()]
]
};
@ -851,19 +869,72 @@ ${responseBody}`;
}
}
/**
* Encrypt content using AES-GCM
* @param content The content to encrypt
* @param key The key to use for encryption
* @returns The encrypted content or null if encryption fails
*/
private async encryptWithAesGcm(content: string, key: string): Promise<string | null> {
try {
// Convert text to bytes
const dataBytes = new TextEncoder().encode(content);
// Create a key from the provided key (using SHA-256 hash)
const keyMaterial = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(key)
);
// Import the key
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'AES-GCM' },
false,
['encrypt']
);
// Generate random IV
const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt the data
const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
cryptoKey,
dataBytes
);
// Combine IV and encrypted data
const encryptedArray = new Uint8Array(iv.length + encryptedData.byteLength);
encryptedArray.set(iv);
encryptedArray.set(new Uint8Array(encryptedData), iv.length);
// Convert to base64
return Buffer.from(encryptedArray).toString('base64');
} catch (error) {
console.error('AES-GCM encryption error:', error);
return null;
}
}
/**
* Send error response event
* @param requestId The ID of the request event
* @param errorMessage The error message to send
* @param senderPubkey The public key of the sender to encrypt the response to
*/
private async sendErrorResponse(requestId: string, errorMessage: string): Promise<void> {
private async sendErrorResponse(requestId: string, errorMessage: string, senderPubkey: string): Promise<void> {
const errorResponse = `HTTP/1.1 500 Internal Server Error
Content-Type: text/plain
Content-Length: ${errorMessage.length}
${errorMessage}`;
await this.sendResponseEvent(requestId, errorResponse);
await this.sendResponseEvent(requestId, errorResponse, senderPubkey);
}
/**