Implement end-to-end encryption for response events with key tag
This commit is contained in:
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user