Skip to main content

contextvm_sdk/encryption/
mod.rs

1//! Encryption and gift wrapping for ContextVM.
2//!
3//! Provides NIP-44 encryption/decryption and NIP-59 gift wrapping.
4//! The actual gift wrapping is done via nostr-sdk's Client for full NIP-59 compliance.
5
6use crate::core::constants::{EPHEMERAL_GIFT_WRAP_KIND, GIFT_WRAP_KIND};
7use crate::core::error::{Error, Result};
8use nostr_sdk::prelude::*;
9
10/// Encrypt a message using NIP-44.
11pub async fn encrypt_nip44<T>(
12    signer: &T,
13    receiver_pubkey: &PublicKey,
14    plaintext: &str,
15) -> Result<String>
16where
17    T: NostrSigner,
18{
19    signer
20        .nip44_encrypt(receiver_pubkey, plaintext)
21        .await
22        .map_err(|e| Error::Encryption(e.to_string()))
23}
24
25/// Decrypt a message using NIP-44.
26pub async fn decrypt_nip44<T>(
27    signer: &T,
28    sender_pubkey: &PublicKey,
29    ciphertext: &str,
30) -> Result<String>
31where
32    T: NostrSigner,
33{
34    signer
35        .nip44_decrypt(sender_pubkey, ciphertext)
36        .await
37        .map_err(|e| Error::Decryption(e.to_string()))
38}
39
40/// Decrypt a single-layer NIP-44 gift wrap (kind 1059)
41pub async fn decrypt_gift_wrap_single_layer<T>(signer: &T, event: &Event) -> Result<String>
42where
43    T: NostrSigner,
44{
45    let sender_pubkey = event.pubkey;
46    decrypt_nip44(signer, &sender_pubkey, &event.content).await
47}
48
49/// Create a single-layer NIP-44 gift wrap (kind 1059)
50pub async fn gift_wrap_single_layer<T>(
51    _signer: &T,
52    recipient: &PublicKey,
53    plaintext: &str,
54) -> Result<Event>
55where
56    T: NostrSigner,
57{
58    let ephemeral = Keys::generate();
59
60    let encrypted = encrypt_nip44(&ephemeral, recipient, plaintext).await?;
61
62    let builder =
63        EventBuilder::new(Kind::Custom(GIFT_WRAP_KIND), encrypted).tag(Tag::public_key(*recipient));
64
65    builder
66        .sign_with_keys(&ephemeral)
67        .map_err(|e| Error::Encryption(e.to_string()))
68}
69
70/// Create a single-layer NIP-44 gift wrap using the provided outer event kind.
71///
72/// Only ContextVM's supported persistent (`1059`) and ephemeral (`21059`) gift-wrap
73/// kinds are accepted here.
74pub async fn gift_wrap_single_layer_with_kind<T>(
75    _signer: &T,
76    recipient: &PublicKey,
77    plaintext: &str,
78    gift_wrap_kind: u16,
79) -> Result<Event>
80where
81    T: NostrSigner,
82{
83    if gift_wrap_kind != GIFT_WRAP_KIND && gift_wrap_kind != EPHEMERAL_GIFT_WRAP_KIND {
84        return Err(Error::Encryption(format!(
85            "Unsupported gift-wrap kind for single-layer encryption: {gift_wrap_kind}"
86        )));
87    }
88
89    let ephemeral = Keys::generate();
90
91    let encrypted = encrypt_nip44(&ephemeral, recipient, plaintext).await?;
92
93    let builder =
94        EventBuilder::new(Kind::Custom(gift_wrap_kind), encrypted).tag(Tag::public_key(*recipient));
95
96    builder
97        .sign_with_keys(&ephemeral)
98        .map_err(|e| Error::Encryption(e.to_string()))
99}
100
101// Legacy NIP-59 functions kept for reference but deprecated.
102
103/// Decrypt a full NIP-59 gift-wrapped event using the Client.
104///
105/// **Deprecated**: Use `decrypt_gift_wrap_single_layer` for ContextVM interop.
106/// This expects the full NIP-59 two-layer scheme (gift wrap → seal → rumor).
107#[deprecated(note = "Use decrypt_gift_wrap_single_layer for ContextVM compatibility")]
108pub async fn decrypt_gift_wrap(client: &Client, event: &Event) -> Result<UnsignedEvent> {
109    let unwrapped = client
110        .unwrap_gift_wrap(event)
111        .await
112        .map_err(|e| Error::Decryption(e.to_string()))?;
113    Ok(unwrapped.rumor)
114}
115
116/// Create and publish a full NIP-59 gift-wrapped event.
117///
118/// **Deprecated**: Use `gift_wrap_single_layer` for ContextVM compatibility.
119#[deprecated(note = "Use gift_wrap_single_layer for ContextVM compatibility")]
120pub async fn gift_wrap(
121    client: &Client,
122    recipient: &PublicKey,
123    rumor: UnsignedEvent,
124) -> Result<EventId> {
125    let output = client
126        .gift_wrap(recipient, rumor, Vec::<Tag>::new())
127        .await
128        .map_err(|e| Error::Encryption(e.to_string()))?;
129    Ok(output.val)
130}
131
132#[cfg(test)]
133mod tests {
134    use crate::core::constants::{EPHEMERAL_GIFT_WRAP_KIND, GIFT_WRAP_KIND};
135
136    use super::*;
137
138    #[tokio::test]
139    async fn test_nip44_roundtrip() {
140        let keys1 = Keys::generate();
141        let keys2 = Keys::generate();
142
143        let plaintext = "Hello, ContextVM!";
144
145        let ciphertext = encrypt_nip44(&keys1, &keys2.public_key(), plaintext)
146            .await
147            .unwrap();
148
149        let decrypted = decrypt_nip44(&keys2, &keys1.public_key(), &ciphertext)
150            .await
151            .unwrap();
152
153        assert_eq!(plaintext, decrypted);
154    }
155
156    /// Create a gift wrap event the same way the JS/TS SDK does:
157    /// single-layer NIP-44 encryption with an ephemeral key.
158    ///
159    /// JS SDK `encryptMessage`:
160    ///   1. Generate ephemeral keypair
161    ///   2. NIP-44 encrypt the plaintext using ephemeral_secret + recipient_pubkey
162    ///   3. Build kind 1059 event with encrypted content, `p` tag = recipient
163    ///   4. Sign with ephemeral key
164    async fn create_simple_gift_wrap(plaintext: &str, recipient: &PublicKey) -> (Event, Keys) {
165        let ephemeral = Keys::generate();
166
167        // Single-layer NIP-44 encrypt
168        let encrypted = encrypt_nip44(&ephemeral, recipient, plaintext)
169            .await
170            .unwrap();
171
172        // Build kind 1059 event
173        let builder = EventBuilder::new(Kind::from(GIFT_WRAP_KIND), encrypted)
174            .tag(Tag::public_key(*recipient));
175
176        let event = builder.sign_with_keys(&ephemeral).unwrap();
177        (event, ephemeral)
178    }
179
180    #[tokio::test]
181    async fn test_decrypt_js_style_gift_wrap() {
182        // Simulates exactly what the JS SDK does:
183        // 1. Create a signed Nostr event containing the MCP message
184        // 2. JSON.stringify that event
185        // 3. Encrypt that JSON string in a gift wrap
186        let client_keys = Keys::generate();
187        let server_keys = Keys::generate();
188
189        let mcp_content = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
190
191        // Step 1: JS SDK creates a signed event (kind 25910 = CTXVM_MESSAGES_KIND)
192        let inner_event = EventBuilder::new(Kind::Custom(25910), mcp_content)
193            .tag(Tag::public_key(server_keys.public_key()))
194            .sign_with_keys(&client_keys)
195            .unwrap();
196
197        // Step 2: JSON.stringify the signed event
198        let inner_json = serde_json::to_string(&inner_event).unwrap();
199
200        // Step 3: Encrypt as a gift wrap
201        let (gift_wrap, _ephemeral) =
202            create_simple_gift_wrap(&inner_json, &server_keys.public_key()).await;
203
204        assert_eq!(gift_wrap.kind, Kind::Custom(1059));
205
206        // Decrypt using our function — should get back the inner event JSON
207        let decrypted = decrypt_gift_wrap_single_layer(&server_keys, &gift_wrap)
208            .await
209            .unwrap();
210
211        // Parse the decrypted JSON as a Nostr event
212        let parsed: Event = serde_json::from_str(&decrypted).unwrap();
213        assert_eq!(parsed.pubkey, client_keys.public_key());
214        assert_eq!(parsed.content, mcp_content);
215    }
216
217    #[tokio::test]
218    async fn test_gift_wrap_roundtrip_single_layer() {
219        let sender_keys = Keys::generate();
220        let recipient_keys = Keys::generate();
221
222        // Simulate the full flow: create inner event, stringify, gift wrap, decrypt
223        let mcp_content = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
224        let inner_event = EventBuilder::new(Kind::Custom(25910), mcp_content)
225            .tag(Tag::public_key(recipient_keys.public_key()))
226            .sign_with_keys(&sender_keys)
227            .unwrap();
228        let inner_json = serde_json::to_string(&inner_event).unwrap();
229
230        // Encrypt (Rust SDK sending)
231        let gift_wrap_event =
232            gift_wrap_single_layer(&sender_keys, &recipient_keys.public_key(), &inner_json)
233                .await
234                .unwrap();
235
236        assert_eq!(gift_wrap_event.kind, Kind::Custom(1059));
237
238        // Decrypt
239        let decrypted = decrypt_gift_wrap_single_layer(&recipient_keys, &gift_wrap_event)
240            .await
241            .unwrap();
242
243        let parsed: Event = serde_json::from_str(&decrypted).unwrap();
244        assert_eq!(parsed.pubkey, sender_keys.public_key());
245        assert_eq!(parsed.content, mcp_content);
246    }
247
248    #[tokio::test]
249    async fn test_gift_wrap_has_correct_tags() {
250        let sender_keys = Keys::generate();
251        let recipient_keys = Keys::generate();
252
253        let gift_wrap_event =
254            gift_wrap_single_layer(&sender_keys, &recipient_keys.public_key(), "test")
255                .await
256                .unwrap();
257
258        // Should have a p tag pointing to the recipient
259        let p_tags: Vec<_> = gift_wrap_event
260            .tags
261            .iter()
262            .filter(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)))
263            .collect();
264        assert_eq!(p_tags.len(), 1);
265
266        let p_value = p_tags[0].clone().to_vec();
267        assert_eq!(p_value[1], recipient_keys.public_key().to_hex());
268    }
269
270    #[tokio::test]
271    async fn test_gift_wrap_uses_ephemeral_key() {
272        let sender_keys = Keys::generate();
273        let recipient_keys = Keys::generate();
274
275        let gift_wrap_event =
276            gift_wrap_single_layer(&sender_keys, &recipient_keys.public_key(), "test")
277                .await
278                .unwrap();
279
280        // The gift wrap event should NOT be signed by the sender's key
281        // (it uses an ephemeral key, like the JS SDK)
282        assert_ne!(gift_wrap_event.pubkey, sender_keys.public_key());
283    }
284
285    /// Regression: gift-wrapped inner events with a tampered pubkey must be
286    /// caught by `Event::verify()`.
287    #[tokio::test]
288    async fn test_forged_inner_event_detected_by_verify() {
289        let real_sender = Keys::generate();
290        let impersonated = Keys::generate();
291        let recipient = Keys::generate();
292
293        let mcp_content = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
294
295        // Step 1: build a legitimately signed inner event
296        let inner_event = EventBuilder::new(Kind::Custom(25910), mcp_content)
297            .tag(Tag::public_key(recipient.public_key()))
298            .sign_with_keys(&real_sender)
299            .unwrap();
300
301        // Step 2: tamper the pubkey (keep original, now-invalid, signature)
302        let mut forged_json: serde_json::Value = serde_json::to_value(&inner_event).unwrap();
303        forged_json["pubkey"] = serde_json::Value::String(impersonated.public_key().to_hex());
304        let forged_str = serde_json::to_string(&forged_json).unwrap();
305
306        // Step 3: gift-wrap the forged payload
307        let (gift_wrap, _) = create_simple_gift_wrap(&forged_str, &recipient.public_key()).await;
308
309        // Decrypt + parse both succeed — the forgery is syntactically valid
310        let decrypted = decrypt_gift_wrap_single_layer(&recipient, &gift_wrap)
311            .await
312            .unwrap();
313        let parsed: Event = serde_json::from_str(&decrypted).unwrap();
314        assert_eq!(parsed.pubkey, impersonated.public_key());
315
316        // Signature verification catches the tampered pubkey
317        assert!(
318            parsed.verify().is_err(),
319            "forged inner event must fail signature verification"
320        );
321    }
322
323    #[tokio::test]
324    async fn test_ephemeral_gift_wrap_roundtrip_single_layer() {
325        let sender_keys = Keys::generate();
326        let recipient_keys = Keys::generate();
327
328        let mcp_content = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
329        let inner_event = EventBuilder::new(Kind::Custom(25910), mcp_content)
330            .tag(Tag::public_key(recipient_keys.public_key()))
331            .sign_with_keys(&sender_keys)
332            .unwrap();
333        let inner_json = serde_json::to_string(&inner_event).unwrap();
334
335        let gift_wrap_event = gift_wrap_single_layer_with_kind(
336            &sender_keys,
337            &recipient_keys.public_key(),
338            &inner_json,
339            EPHEMERAL_GIFT_WRAP_KIND,
340        )
341        .await
342        .unwrap();
343
344        assert_eq!(gift_wrap_event.kind, Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND));
345
346        let decrypted = decrypt_gift_wrap_single_layer(&recipient_keys, &gift_wrap_event)
347            .await
348            .unwrap();
349        let parsed: Event = serde_json::from_str(&decrypted).unwrap();
350        assert_eq!(parsed.pubkey, sender_keys.public_key());
351        assert_eq!(parsed.content, mcp_content);
352    }
353
354    #[tokio::test]
355    async fn test_invalid_gift_wrap_kind_rejected() {
356        let sender_keys = Keys::generate();
357        let recipient_keys = Keys::generate();
358
359        let error = gift_wrap_single_layer_with_kind(
360            &sender_keys,
361            &recipient_keys.public_key(),
362            "test",
363            4242,
364        )
365        .await
366        .unwrap_err();
367
368        assert!(
369            error.to_string().contains("Unsupported gift-wrap kind"),
370            "unexpected error: {error}"
371        );
372    }
373}