use crate::event::{sign_event, NostrEvent, UnsignedEvent};
use crate::keys::generate_keypair;
use crate::nip04;
use crate::nip44;
use k256::schnorr::SigningKey;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::Zeroize;
const TIMESTAMP_JITTER_SECS: u32 = 172_800;
const KIND_RUMOR: u64 = 14;
const KIND_SEAL: u64 = 13;
const KIND_GIFT_WRAP: u64 = 1059;
#[derive(Debug, Error)]
pub enum GiftWrapError {
#[error("serialization error: {0}")]
Serialization(String),
#[error("encryption error: {0}")]
Encryption(String),
#[error("decryption error: {0}")]
Decryption(String),
#[error("invalid event kind: expected {expected}, got {actual}")]
InvalidKind {
expected: u64,
actual: u64,
},
#[error("invalid public key: {0}")]
InvalidPubkey(String),
#[error("key error: {0}")]
KeyError(String),
#[error("parse error: {0}")]
ParseError(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnwrappedGift {
pub sender_pubkey: String,
pub rumor: UnsignedEvent,
pub seal: NostrEvent,
}
fn now_secs() -> u64 {
#[cfg(target_arch = "wasm32")]
{
(js_sys::Date::now() / 1000.0) as u64
}
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock before UNIX epoch")
.as_secs()
}
}
fn randomized_timestamp() -> u64 {
let now = now_secs();
let mut rand_bytes = [0u8; 5];
getrandom::getrandom(&mut rand_bytes).expect("getrandom for timestamp jitter");
let offset_raw =
u32::from_le_bytes([rand_bytes[0], rand_bytes[1], rand_bytes[2], rand_bytes[3]]);
let offset = (offset_raw % TIMESTAMP_JITTER_SECS) as u64;
let add = rand_bytes[4] & 1 == 0;
if add {
now.saturating_add(offset)
} else {
now.saturating_sub(offset)
}
}
fn hex_to_32(hex_str: &str) -> Result<[u8; 32], GiftWrapError> {
let bytes = hex::decode(hex_str)
.map_err(|e| GiftWrapError::InvalidPubkey(format!("hex decode: {e}")))?;
if bytes.len() != 32 {
return Err(GiftWrapError::InvalidPubkey(format!(
"expected 32 bytes, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
}
pub fn create_rumor(sender_pubkey: &str, recipient_pubkey: &str, content: &str) -> UnsignedEvent {
UnsignedEvent {
pubkey: sender_pubkey.to_string(),
created_at: now_secs(),
kind: KIND_RUMOR,
tags: vec![vec!["p".to_string(), recipient_pubkey.to_string()]],
content: content.to_string(),
}
}
pub fn seal_rumor(
rumor: &UnsignedEvent,
sender_sk: &[u8; 32],
recipient_pk: &[u8; 32],
) -> Result<NostrEvent, GiftWrapError> {
let rumor_json =
serde_json::to_string(rumor).map_err(|e| GiftWrapError::Serialization(e.to_string()))?;
let encrypted = nip44::encrypt(sender_sk, recipient_pk, &rumor_json)
.map_err(|e| GiftWrapError::Encryption(e.to_string()))?;
let signing_key = SigningKey::from_bytes(sender_sk)
.map_err(|e| GiftWrapError::KeyError(format!("invalid sender secret key: {e}")))?;
let sender_pubkey = hex::encode(signing_key.verifying_key().to_bytes());
let unsigned_seal = UnsignedEvent {
pubkey: sender_pubkey,
created_at: randomized_timestamp(),
kind: KIND_SEAL,
tags: vec![],
content: encrypted,
};
sign_event(unsigned_seal, &signing_key)
.map_err(|e| GiftWrapError::KeyError(format!("seal signing failed: {e}")))
}
pub fn wrap_seal(seal: &NostrEvent, recipient_pubkey: &str) -> Result<NostrEvent, GiftWrapError> {
let throwaway = generate_keypair()
.map_err(|e| GiftWrapError::KeyError(format!("throwaway keypair generation: {e}")))?;
let throwaway_sk_bytes = *throwaway.secret.as_bytes();
let throwaway_pubkey = throwaway.public.to_hex();
let seal_json =
serde_json::to_string(seal).map_err(|e| GiftWrapError::Serialization(e.to_string()))?;
let recipient_pk_bytes = hex_to_32(recipient_pubkey)?;
let encrypted = nip44::encrypt(&throwaway_sk_bytes, &recipient_pk_bytes, &seal_json)
.map_err(|e| GiftWrapError::Encryption(e.to_string()))?;
let unsigned_wrap = UnsignedEvent {
pubkey: throwaway_pubkey,
created_at: randomized_timestamp(),
kind: KIND_GIFT_WRAP,
tags: vec![vec!["p".to_string(), recipient_pubkey.to_string()]],
content: encrypted,
};
let throwaway_signing_key = SigningKey::from_bytes(&throwaway_sk_bytes)
.map_err(|e| GiftWrapError::KeyError(format!("throwaway signing key: {e}")))?;
let wrapped = sign_event(unsigned_wrap, &throwaway_signing_key)
.map_err(|e| GiftWrapError::KeyError(format!("gift wrap signing failed: {e}")))?;
let mut sk_to_zeroize = throwaway_sk_bytes;
sk_to_zeroize.zeroize();
Ok(wrapped)
}
pub fn gift_wrap(
sender_sk: &[u8; 32],
sender_pubkey: &str,
recipient_pubkey: &str,
content: &str,
) -> Result<NostrEvent, GiftWrapError> {
let recipient_pk_bytes = hex_to_32(recipient_pubkey)?;
let rumor = create_rumor(sender_pubkey, recipient_pubkey, content);
let seal = seal_rumor(&rumor, sender_sk, &recipient_pk_bytes)?;
wrap_seal(&seal, recipient_pubkey)
}
pub fn unwrap_gift(
gift: &NostrEvent,
recipient_sk: &[u8; 32],
) -> Result<UnwrappedGift, GiftWrapError> {
if gift.kind != KIND_GIFT_WRAP {
return Err(GiftWrapError::InvalidKind {
expected: KIND_GIFT_WRAP,
actual: gift.kind,
});
}
let throwaway_pk_bytes = hex_to_32(&gift.pubkey)?;
let seal_json = nip44::decrypt(recipient_sk, &throwaway_pk_bytes, &gift.content)
.map_err(|e| GiftWrapError::Decryption(format!("gift wrap decryption: {e}")))?;
let seal: NostrEvent = serde_json::from_str(&seal_json)
.map_err(|e| GiftWrapError::ParseError(format!("seal JSON parse: {e}")))?;
if seal.kind != KIND_SEAL {
return Err(GiftWrapError::InvalidKind {
expected: KIND_SEAL,
actual: seal.kind,
});
}
let sender_pk_bytes = hex_to_32(&seal.pubkey)?;
let rumor_json = nip44::decrypt(recipient_sk, &sender_pk_bytes, &seal.content)
.map_err(|e| GiftWrapError::Decryption(format!("seal decryption: {e}")))?;
let rumor: UnsignedEvent = serde_json::from_str(&rumor_json)
.map_err(|e| GiftWrapError::ParseError(format!("rumor JSON parse: {e}")))?;
if rumor.kind != KIND_RUMOR {
return Err(GiftWrapError::InvalidKind {
expected: KIND_RUMOR,
actual: rumor.kind,
});
}
Ok(UnwrappedGift {
sender_pubkey: seal.pubkey.clone(),
rumor,
seal,
})
}
const KIND_ENCRYPTED_DM: u64 = 4;
pub fn process_kind4_event(
event: &NostrEvent,
recipient_sk: &[u8; 32],
) -> Result<String, GiftWrapError> {
if event.kind != KIND_ENCRYPTED_DM {
return Err(GiftWrapError::InvalidKind {
expected: KIND_ENCRYPTED_DM,
actual: event.kind,
});
}
nip04::nip04_decrypt(recipient_sk, &event.pubkey, &event.content)
.map_err(|e| GiftWrapError::Decryption(format!("NIP-04 decryption: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::generate_keypair as gen_kp;
fn test_keypair() -> ([u8; 32], String) {
let kp = gen_kp().unwrap();
let sk = *kp.secret.as_bytes();
let pk = kp.public.to_hex();
(sk, pk)
}
#[test]
fn create_rumor_has_correct_structure() {
let sender_pk = "aa".repeat(32);
let recipient_pk = "bb".repeat(32);
let rumor = create_rumor(&sender_pk, &recipient_pk, "hello");
assert_eq!(rumor.kind, KIND_RUMOR);
assert_eq!(rumor.pubkey, sender_pk);
assert_eq!(rumor.content, "hello");
assert_eq!(rumor.tags.len(), 1);
assert_eq!(rumor.tags[0], vec!["p", &recipient_pk]);
assert!(rumor.created_at > 0);
}
#[test]
fn seal_rumor_produces_kind_13() {
let (sender_sk, sender_pk) = test_keypair();
let (_, recipient_pk) = test_keypair();
let recipient_pk_bytes = hex_to_32(&recipient_pk).unwrap();
let rumor = create_rumor(&sender_pk, &recipient_pk, "sealed message");
let seal = seal_rumor(&rumor, &sender_sk, &recipient_pk_bytes).unwrap();
assert_eq!(seal.kind, KIND_SEAL);
assert_eq!(seal.pubkey, sender_pk);
assert!(seal.tags.is_empty());
assert!(!seal.content.is_empty());
assert!(!seal.id.is_empty());
assert!(!seal.sig.is_empty());
}
#[test]
fn seal_has_randomized_timestamp() {
let (sender_sk, sender_pk) = test_keypair();
let (_, recipient_pk) = test_keypair();
let recipient_pk_bytes = hex_to_32(&recipient_pk).unwrap();
let rumor = create_rumor(&sender_pk, &recipient_pk, "timing test");
let seal1 = seal_rumor(&rumor, &sender_sk, &recipient_pk_bytes).unwrap();
let seal2 = seal_rumor(&rumor, &sender_sk, &recipient_pk_bytes).unwrap();
let now = now_secs();
let jitter = TIMESTAMP_JITTER_SECS as u64;
assert!(seal1.created_at >= now.saturating_sub(jitter));
assert!(seal1.created_at <= now.saturating_add(jitter));
assert!(seal2.created_at >= now.saturating_sub(jitter));
assert!(seal2.created_at <= now.saturating_add(jitter));
}
#[test]
fn wrap_seal_produces_kind_1059() {
let (sender_sk, sender_pk) = test_keypair();
let (_, recipient_pk) = test_keypair();
let recipient_pk_bytes = hex_to_32(&recipient_pk).unwrap();
let rumor = create_rumor(&sender_pk, &recipient_pk, "wrapped message");
let seal = seal_rumor(&rumor, &sender_sk, &recipient_pk_bytes).unwrap();
let wrapped = wrap_seal(&seal, &recipient_pk).unwrap();
assert_eq!(wrapped.kind, KIND_GIFT_WRAP);
assert_ne!(wrapped.pubkey, sender_pk);
assert_eq!(wrapped.tags.len(), 1);
assert_eq!(wrapped.tags[0][0], "p");
assert_eq!(wrapped.tags[0][1], recipient_pk);
assert!(!wrapped.content.is_empty());
}
#[test]
fn gift_wrap_roundtrip() {
let (sender_sk, sender_pk) = test_keypair();
let (recipient_sk, recipient_pk) = test_keypair();
let content = "Hello from NIP-59 gift wrap!";
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, content).unwrap();
assert_eq!(wrapped.kind, KIND_GIFT_WRAP);
let unwrapped = unwrap_gift(&wrapped, &recipient_sk).unwrap();
assert_eq!(unwrapped.sender_pubkey, sender_pk);
assert_eq!(unwrapped.rumor.content, content);
assert_eq!(unwrapped.rumor.kind, KIND_RUMOR);
assert_eq!(unwrapped.seal.kind, KIND_SEAL);
}
#[test]
fn gift_wrap_roundtrip_unicode() {
let (sender_sk, sender_pk) = test_keypair();
let (recipient_sk, recipient_pk) = test_keypair();
let content = "Nostr DM with unicode: 日本語テスト 🎁";
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, content).unwrap();
let unwrapped = unwrap_gift(&wrapped, &recipient_sk).unwrap();
assert_eq!(unwrapped.rumor.content, content);
}
#[test]
fn gift_wrap_roundtrip_long_message() {
let (sender_sk, sender_pk) = test_keypair();
let (recipient_sk, recipient_pk) = test_keypair();
let content = "A".repeat(10000);
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, &content).unwrap();
let unwrapped = unwrap_gift(&wrapped, &recipient_sk).unwrap();
assert_eq!(unwrapped.rumor.content, content);
}
#[test]
fn unwrap_with_wrong_key_fails() {
let (sender_sk, sender_pk) = test_keypair();
let (_, recipient_pk) = test_keypair();
let (wrong_sk, _) = test_keypair();
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, "secret").unwrap();
let result = unwrap_gift(&wrapped, &wrong_sk);
assert!(result.is_err());
assert!(
matches!(result, Err(GiftWrapError::Decryption(_))),
"expected Decryption error, got: {:?}",
result
);
}
#[test]
fn unwrap_rejects_wrong_outer_kind() {
let fake_event = NostrEvent {
id: "00".repeat(32),
pubkey: "aa".repeat(32),
created_at: 1700000000,
kind: 1, tags: vec![],
content: String::new(),
sig: "00".repeat(64),
};
let (recipient_sk, _) = test_keypair();
let result = unwrap_gift(&fake_event, &recipient_sk);
assert!(matches!(
result,
Err(GiftWrapError::InvalidKind {
expected: KIND_GIFT_WRAP,
actual: 1
})
));
}
#[test]
fn gift_wrap_sender_pubkey_matches() {
let (sender_sk, sender_pk) = test_keypair();
let (recipient_sk, recipient_pk) = test_keypair();
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, "identity test").unwrap();
let unwrapped = unwrap_gift(&wrapped, &recipient_sk).unwrap();
assert_eq!(unwrapped.sender_pubkey, sender_pk);
assert_eq!(unwrapped.rumor.pubkey, sender_pk);
}
#[test]
fn gift_wrap_recipient_tag_present() {
let (sender_sk, sender_pk) = test_keypair();
let (_, recipient_pk) = test_keypair();
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, "tag test").unwrap();
let p_tags: Vec<_> = wrapped.tags.iter().filter(|t| t[0] == "p").collect();
assert_eq!(p_tags.len(), 1);
assert_eq!(p_tags[0][1], recipient_pk);
}
#[test]
fn seal_has_no_tags() {
let (sender_sk, sender_pk) = test_keypair();
let (recipient_sk, recipient_pk) = test_keypair();
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, "no tags test").unwrap();
let unwrapped = unwrap_gift(&wrapped, &recipient_sk).unwrap();
assert!(unwrapped.seal.tags.is_empty());
}
#[test]
fn rumor_has_p_tag() {
let (sender_sk, sender_pk) = test_keypair();
let (recipient_sk, recipient_pk) = test_keypair();
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, "p tag test").unwrap();
let unwrapped = unwrap_gift(&wrapped, &recipient_sk).unwrap();
let p_tags: Vec<_> = unwrapped
.rumor
.tags
.iter()
.filter(|t| t[0] == "p")
.collect();
assert_eq!(p_tags.len(), 1);
assert_eq!(p_tags[0][1], recipient_pk);
}
#[test]
fn outer_pubkey_is_throwaway() {
let (sender_sk, sender_pk) = test_keypair();
let (_, recipient_pk) = test_keypair();
let wrapped1 = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, "throwaway 1").unwrap();
let wrapped2 = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, "throwaway 2").unwrap();
assert_ne!(wrapped1.pubkey, wrapped2.pubkey);
assert_ne!(wrapped1.pubkey, sender_pk);
assert_ne!(wrapped2.pubkey, sender_pk);
}
#[test]
fn gift_wrap_event_verifies() {
let (sender_sk, sender_pk) = test_keypair();
let (_, recipient_pk) = test_keypair();
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, "verify test").unwrap();
assert!(crate::event::verify_event(&wrapped));
}
#[test]
fn seal_event_verifies() {
let (sender_sk, sender_pk) = test_keypair();
let (recipient_sk, recipient_pk) = test_keypair();
let wrapped = gift_wrap(&sender_sk, &sender_pk, &recipient_pk, "seal verify test").unwrap();
let unwrapped = unwrap_gift(&wrapped, &recipient_sk).unwrap();
assert!(crate::event::verify_event(&unwrapped.seal));
}
}