This commit is contained in:
n 2025-04-10 12:58:28 +01:00
parent a00417685c
commit 12f1258f3e
2 changed files with 179 additions and 36 deletions

@ -4,6 +4,7 @@
import { NostrEvent } from '../relay';
import * as nostrTools from 'nostr-tools';
import { encryptWithNostrTools, encryptWithWebCrypto } from '../utils/crypto-utils';
/**
* Service for managing NIP-21121 HTTP response events
@ -40,21 +41,42 @@ export class Nostr21121Service {
// Extract private key
let privateKeyStr = typeof serverPrivateKey === 'string' ? serverPrivateKey : '';
if (typeof serverPrivateKey === 'string' && serverPrivateKey.startsWith('nsec')) {
try {
// This would normally decode the nsec, but we'll just simulate it
console.log('Would decode nsec to hex');
privateKeyStr = serverPrivateKey;
} catch (e) {
console.error('Error decoding nsec:', e);
return null;
console.log('Using nsec key for signing');
} else {
console.log('Using hex or bytes key for signing');
}
// Convert the private key to bytes if needed
let privateKeyBytes: Uint8Array;
// Handle different private key formats
if (typeof serverPrivateKey === 'string') {
if (serverPrivateKey.startsWith('nsec')) {
try {
// Decode nsec
const decoded = nostrTools.nip19.decode(serverPrivateKey);
privateKeyBytes = decoded.data as Uint8Array;
console.log('Successfully decoded nsec key');
} catch (e) {
console.error('Error decoding nsec:', e);
return null;
}
} else {
// Convert hex string to bytes
privateKeyBytes = new Uint8Array(
serverPrivateKey.match(/.{1,2}/g)?.map(byte => parseInt(byte, 16)) || []
);
}
} else {
// Already bytes
privateKeyBytes = serverPrivateKey;
}
// Get the public key from the private key
const pubKey = nostrTools.getPublicKey(privateKeyStr as any);
const pubKey = nostrTools.getPublicKey(privateKeyBytes);
console.log(`Using pubkey: ${pubKey.substring(0, 8)}...`);
// Check if we need to encrypt the response
let finalContent = responseContent;
// Initialize tags array
let tags: string[][] = [];
// Always add reference to the request event
@ -64,42 +86,77 @@ export class Nostr21121Service {
// Add kind reference
tags.push(['k', '21120']);
// Check if the original event has a p tag (recipient)
const pTag = requestEvent.tags.find(tag => tag[0] === 'p');
if (pTag && pTag[1]) {
// This would be encrypted in a real implementation
console.log('Would encrypt content for recipient:', pTag[1]);
// Add p tag to reference the recipient
tags.push(['p', pTag[1], '']);
// Get the pubkey of the request creator (client) for encryption
const clientPubkey = requestEvent.pubkey;
if (!clientPubkey) {
console.warn("No client pubkey found in request event, encryption will not be possible");
}
// Create the event
const eventBody = {
// Generate a random encryption key for symmetric encryption
const encryptionKey = generateRandomKey(32);
console.log("Generated random encryption key for symmetric encryption");
// Encrypt the content with WebCrypto (symmetric encryption)
let encryptedContent: string;
try {
encryptedContent = await encryptWithWebCrypto(responseContent, encryptionKey);
console.log("Successfully encrypted HTTP content with symmetric encryption");
} catch (error) {
console.error("Symmetric encryption failed:", error);
return null;
}
// Encrypt the encryption key with NIP-44 using nostr-tools
let encryptedKey: string = "";
if (clientPubkey) {
try {
// Encrypt the encryption key using NIP-44 from nostr-tools
console.log(`Encrypting key for client pubkey: ${clientPubkey.substring(0, 8)}...`);
// We use privateKeyBytes here since we have it in the correct format
encryptedKey = await encryptWithNostrTools(encryptionKey, privateKeyBytes, clientPubkey);
console.log("Successfully encrypted the symmetric key with NIP-44");
// Add p tag to reference the recipient
tags.push(['p', clientPubkey, '']);
// Add encrypted key as a tag
tags.push(['encrypted_key', encryptedKey]);
} catch (encryptError) {
console.error("Failed to encrypt symmetric key:", encryptError);
// We'll continue with just the symmetric encryption, but decryption won't work properly
console.warn("Proceeding without encrypted key - decryption will not be possible");
}
}
// Create the unsigned event
const unsignedEvent = {
kind: 21121,
created_at: Math.floor(Date.now() / 1000),
tags: tags,
content: finalContent,
content: encryptedContent, // This is now the symmetrically encrypted content
pubkey: pubKey
};
// Compute the event ID (hash)
const id = nostrTools.getEventHash(eventBody);
// Sign the event (simplified)
const sig = 'simulated_signature_for_demo';
// Create the complete signed event
const event: NostrEvent = {
...eventBody,
id,
sig
// Sign the event properly using nostr-tools
const event = nostrTools.finalizeEvent(unsignedEvent, privateKeyBytes);
console.log(`Event created and signed successfully. ID: ${event.id.substring(0, 8)}...`);
// The event is already properly signed
};
// Simulate publishing to relay
console.log(`Would publish event to relay: ${relayUrl}`);
console.log('Event:', event);
// Publish to relay
console.log(`Publishing event to relay: ${relayUrl}`);
try {
const relayPool = this.relayService.getRelayPool();
if (relayPool) {
await relayPool.publish([relayUrl], event);
console.log('Event published successfully to relay');
}
} catch (publishError) {
console.error('Error publishing event to relay:', publishError);
// Even if publishing fails, we'll return the created event
}
return event;
} catch (error) {
@ -118,4 +175,17 @@ export class Nostr21121Service {
console.log(`Would search for response to ${requestEventId} on ${relayUrl}`);
return Promise.resolve(null);
}
}
/**
* Generate a random key for symmetric encryption
* @param length Length of the key in bytes
* @returns A random key as a hex string
*/
function generateRandomKey(length: number): string {
const randomBytes = new Uint8Array(length);
crypto.getRandomValues(randomBytes);
return Array.from(randomBytes)
.map(byte => byte.toString(16).padStart(2, '0'))
.join('');
}

@ -316,4 +316,77 @@ export function decryptKeyWithNostrTools(ciphertext: string, serverNsec: string,
reject(new Error(`NIP-44 decryption with nostr-tools failed: ${error instanceof Error ? error.message : String(error)}`));
}
});
}
/**
* Encrypt data using NIP-44 with nostr-tools library directly
* This is specifically for server-side encryption to the client/requester
*
* @param plaintext The message to encrypt
* @param serverNsec Server's private key in nsec format
* @param clientPubkey Client's public key for conversation key derivation (the pubkey of the 21120 request creator)
* @returns Promise resolving to NIP-44 encrypted message
*/
export function encryptWithNostrTools(plaintext: string, serverNsec: string, clientPubkey: string): Promise<string> {
return new Promise<string>(async (resolve, reject) => {
try {
if (!serverNsec.startsWith('nsec')) {
reject(new Error("Server key must be in nsec format for encryption"));
return;
}
// Import nostr-tools library
const nostrTools = await import('nostr-tools');
// Check if NIP-44 implementation is available in nostr-tools
if (!nostrTools.nip44) {
reject(new Error("NIP-44 not available in nostr-tools - this may need to be implemented"));
return;
}
// Decode the nsec to get the raw private key
const decoded = nostrTools.nip19.decode(serverNsec);
if (decoded.type !== 'nsec') {
reject(new Error(`Not a private key: decoded type is ${decoded.type}`));
return;
}
// Get the private key bytes
const privateKeyBytes = decoded.data as Uint8Array;
// Convert client pubkey from npub format if necessary
let clientPubkeyHex = clientPubkey;
if (clientPubkey.startsWith('npub')) {
try {
const decodedClient = nostrTools.nip19.decode(clientPubkey);
clientPubkeyHex = decodedClient.data as string;
} catch (error) {
reject(new Error(`Invalid npub format: ${error instanceof Error ? error.message : String(error)}`));
return;
}
}
console.log("Deriving conversation key between server and client for encryption");
// Get conversation key using the server's private key and client's public key
const conversationKey = await nostrTools.nip44.getConversationKey(
privateKeyBytes, // Server private key as bytes
clientPubkeyHex // Client public key as hex string (21120 request creator)
);
console.log("Generated conversation key for encryption");
// Encrypt the message using NIP-44 with the derived conversation key
const encryptedMessage = await nostrTools.nip44.encrypt(
plaintext, // The plaintext message (HTTP response)
conversationKey // The conversation key we derived
);
console.log("Successfully encrypted message with NIP-44 conversation key");
resolve(encryptedMessage);
} catch (error) {
console.error("Error in encryptWithNostrTools:", error);
reject(new Error(`NIP-44 encryption with nostr-tools failed: ${error instanceof Error ? error.message : String(error)}`));
}
});
}