use crate::core::constants::{EPHEMERAL_GIFT_WRAP_KIND, GIFT_WRAP_KIND};
use crate::core::error::{Error, Result};
use nostr_sdk::prelude::*;
pub async fn encrypt_nip44<T>(
signer: &T,
receiver_pubkey: &PublicKey,
plaintext: &str,
) -> Result<String>
where
T: NostrSigner,
{
signer
.nip44_encrypt(receiver_pubkey, plaintext)
.await
.map_err(|e| Error::Encryption(e.to_string()))
}
pub async fn decrypt_nip44<T>(
signer: &T,
sender_pubkey: &PublicKey,
ciphertext: &str,
) -> Result<String>
where
T: NostrSigner,
{
signer
.nip44_decrypt(sender_pubkey, ciphertext)
.await
.map_err(|e| Error::Decryption(e.to_string()))
}
pub async fn decrypt_gift_wrap_single_layer<T>(signer: &T, event: &Event) -> Result<String>
where
T: NostrSigner,
{
let sender_pubkey = event.pubkey;
decrypt_nip44(signer, &sender_pubkey, &event.content).await
}
pub async fn gift_wrap_single_layer<T>(
_signer: &T,
recipient: &PublicKey,
plaintext: &str,
) -> Result<Event>
where
T: NostrSigner,
{
let ephemeral = Keys::generate();
let encrypted = encrypt_nip44(&ephemeral, recipient, plaintext).await?;
let builder =
EventBuilder::new(Kind::Custom(GIFT_WRAP_KIND), encrypted).tag(Tag::public_key(*recipient));
builder
.sign_with_keys(&ephemeral)
.map_err(|e| Error::Encryption(e.to_string()))
}
pub async fn gift_wrap_single_layer_with_kind<T>(
_signer: &T,
recipient: &PublicKey,
plaintext: &str,
gift_wrap_kind: u16,
) -> Result<Event>
where
T: NostrSigner,
{
if gift_wrap_kind != GIFT_WRAP_KIND && gift_wrap_kind != EPHEMERAL_GIFT_WRAP_KIND {
return Err(Error::Encryption(format!(
"Unsupported gift-wrap kind for single-layer encryption: {gift_wrap_kind}"
)));
}
let ephemeral = Keys::generate();
let encrypted = encrypt_nip44(&ephemeral, recipient, plaintext).await?;
let builder =
EventBuilder::new(Kind::Custom(gift_wrap_kind), encrypted).tag(Tag::public_key(*recipient));
builder
.sign_with_keys(&ephemeral)
.map_err(|e| Error::Encryption(e.to_string()))
}
#[deprecated(note = "Use decrypt_gift_wrap_single_layer for ContextVM compatibility")]
pub async fn decrypt_gift_wrap(client: &Client, event: &Event) -> Result<UnsignedEvent> {
let unwrapped = client
.unwrap_gift_wrap(event)
.await
.map_err(|e| Error::Decryption(e.to_string()))?;
Ok(unwrapped.rumor)
}
#[deprecated(note = "Use gift_wrap_single_layer for ContextVM compatibility")]
pub async fn gift_wrap(
client: &Client,
recipient: &PublicKey,
rumor: UnsignedEvent,
) -> Result<EventId> {
let output = client
.gift_wrap(recipient, rumor, Vec::<Tag>::new())
.await
.map_err(|e| Error::Encryption(e.to_string()))?;
Ok(output.val)
}
#[cfg(test)]
mod tests {
use crate::core::constants::{EPHEMERAL_GIFT_WRAP_KIND, GIFT_WRAP_KIND};
use super::*;
#[tokio::test]
async fn test_nip44_roundtrip() {
let keys1 = Keys::generate();
let keys2 = Keys::generate();
let plaintext = "Hello, ContextVM!";
let ciphertext = encrypt_nip44(&keys1, &keys2.public_key(), plaintext)
.await
.unwrap();
let decrypted = decrypt_nip44(&keys2, &keys1.public_key(), &ciphertext)
.await
.unwrap();
assert_eq!(plaintext, decrypted);
}
async fn create_simple_gift_wrap(plaintext: &str, recipient: &PublicKey) -> (Event, Keys) {
let ephemeral = Keys::generate();
let encrypted = encrypt_nip44(&ephemeral, recipient, plaintext)
.await
.unwrap();
let builder = EventBuilder::new(Kind::from(GIFT_WRAP_KIND), encrypted)
.tag(Tag::public_key(*recipient));
let event = builder.sign_with_keys(&ephemeral).unwrap();
(event, ephemeral)
}
#[tokio::test]
async fn test_decrypt_js_style_gift_wrap() {
let client_keys = Keys::generate();
let server_keys = Keys::generate();
let mcp_content = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
let inner_event = EventBuilder::new(Kind::Custom(25910), mcp_content)
.tag(Tag::public_key(server_keys.public_key()))
.sign_with_keys(&client_keys)
.unwrap();
let inner_json = serde_json::to_string(&inner_event).unwrap();
let (gift_wrap, _ephemeral) =
create_simple_gift_wrap(&inner_json, &server_keys.public_key()).await;
assert_eq!(gift_wrap.kind, Kind::Custom(1059));
let decrypted = decrypt_gift_wrap_single_layer(&server_keys, &gift_wrap)
.await
.unwrap();
let parsed: Event = serde_json::from_str(&decrypted).unwrap();
assert_eq!(parsed.pubkey, client_keys.public_key());
assert_eq!(parsed.content, mcp_content);
}
#[tokio::test]
async fn test_gift_wrap_roundtrip_single_layer() {
let sender_keys = Keys::generate();
let recipient_keys = Keys::generate();
let mcp_content = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
let inner_event = EventBuilder::new(Kind::Custom(25910), mcp_content)
.tag(Tag::public_key(recipient_keys.public_key()))
.sign_with_keys(&sender_keys)
.unwrap();
let inner_json = serde_json::to_string(&inner_event).unwrap();
let gift_wrap_event =
gift_wrap_single_layer(&sender_keys, &recipient_keys.public_key(), &inner_json)
.await
.unwrap();
assert_eq!(gift_wrap_event.kind, Kind::Custom(1059));
let decrypted = decrypt_gift_wrap_single_layer(&recipient_keys, &gift_wrap_event)
.await
.unwrap();
let parsed: Event = serde_json::from_str(&decrypted).unwrap();
assert_eq!(parsed.pubkey, sender_keys.public_key());
assert_eq!(parsed.content, mcp_content);
}
#[tokio::test]
async fn test_gift_wrap_has_correct_tags() {
let sender_keys = Keys::generate();
let recipient_keys = Keys::generate();
let gift_wrap_event =
gift_wrap_single_layer(&sender_keys, &recipient_keys.public_key(), "test")
.await
.unwrap();
let p_tags: Vec<_> = gift_wrap_event
.tags
.iter()
.filter(|t| t.kind() == TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)))
.collect();
assert_eq!(p_tags.len(), 1);
let p_value = p_tags[0].clone().to_vec();
assert_eq!(p_value[1], recipient_keys.public_key().to_hex());
}
#[tokio::test]
async fn test_gift_wrap_uses_ephemeral_key() {
let sender_keys = Keys::generate();
let recipient_keys = Keys::generate();
let gift_wrap_event =
gift_wrap_single_layer(&sender_keys, &recipient_keys.public_key(), "test")
.await
.unwrap();
assert_ne!(gift_wrap_event.pubkey, sender_keys.public_key());
}
#[tokio::test]
async fn test_forged_inner_event_detected_by_verify() {
let real_sender = Keys::generate();
let impersonated = Keys::generate();
let recipient = Keys::generate();
let mcp_content = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
let inner_event = EventBuilder::new(Kind::Custom(25910), mcp_content)
.tag(Tag::public_key(recipient.public_key()))
.sign_with_keys(&real_sender)
.unwrap();
let mut forged_json: serde_json::Value = serde_json::to_value(&inner_event).unwrap();
forged_json["pubkey"] = serde_json::Value::String(impersonated.public_key().to_hex());
let forged_str = serde_json::to_string(&forged_json).unwrap();
let (gift_wrap, _) = create_simple_gift_wrap(&forged_str, &recipient.public_key()).await;
let decrypted = decrypt_gift_wrap_single_layer(&recipient, &gift_wrap)
.await
.unwrap();
let parsed: Event = serde_json::from_str(&decrypted).unwrap();
assert_eq!(parsed.pubkey, impersonated.public_key());
assert!(
parsed.verify().is_err(),
"forged inner event must fail signature verification"
);
}
#[tokio::test]
async fn test_ephemeral_gift_wrap_roundtrip_single_layer() {
let sender_keys = Keys::generate();
let recipient_keys = Keys::generate();
let mcp_content = r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
let inner_event = EventBuilder::new(Kind::Custom(25910), mcp_content)
.tag(Tag::public_key(recipient_keys.public_key()))
.sign_with_keys(&sender_keys)
.unwrap();
let inner_json = serde_json::to_string(&inner_event).unwrap();
let gift_wrap_event = gift_wrap_single_layer_with_kind(
&sender_keys,
&recipient_keys.public_key(),
&inner_json,
EPHEMERAL_GIFT_WRAP_KIND,
)
.await
.unwrap();
assert_eq!(gift_wrap_event.kind, Kind::Custom(EPHEMERAL_GIFT_WRAP_KIND));
let decrypted = decrypt_gift_wrap_single_layer(&recipient_keys, &gift_wrap_event)
.await
.unwrap();
let parsed: Event = serde_json::from_str(&decrypted).unwrap();
assert_eq!(parsed.pubkey, sender_keys.public_key());
assert_eq!(parsed.content, mcp_content);
}
#[tokio::test]
async fn test_invalid_gift_wrap_kind_rejected() {
let sender_keys = Keys::generate();
let recipient_keys = Keys::generate();
let error = gift_wrap_single_layer_with_kind(
&sender_keys,
&recipient_keys.public_key(),
"test",
4242,
)
.await
.unwrap_err();
assert!(
error.to_string().contains("Unsupported gift-wrap kind"),
"unexpected error: {error}"
);
}
}