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