mod filter;
mod shared_key;
mod unwrap;
mod wrap;
pub use filter::{chat_filter, CHAT_DEFAULT_LOOKBACK_SECS};
pub use shared_key::SharedKey;
pub use unwrap::{unwrap_chat_message, ChatMessage};
pub use wrap::wrap_chat_message;
#[cfg(test)]
mod tests {
use super::*;
use nostr_sdk::nips::nip44;
use nostr_sdk::prelude::*;
fn shared_pair() -> (Keys, Keys, SharedKey, SharedKey) {
let alice = Keys::generate();
let bob = Keys::generate();
let alice_shared = SharedKey::derive(alice.secret_key(), &bob.public_key()).unwrap();
let bob_shared = SharedKey::derive(bob.secret_key(), &alice.public_key()).unwrap();
(alice, bob, alice_shared, bob_shared)
}
#[tokio::test]
async fn wrap_and_unwrap_roundtrip() {
let (alice, _bob, alice_shared, bob_shared) = shared_pair();
let body = "hello from alice";
let event = wrap_chat_message(&alice, &alice_shared.public_key(), body)
.await
.expect("wrap");
assert_eq!(event.kind, Kind::GiftWrap);
assert!(event
.tags
.public_keys()
.any(|pk| *pk == alice_shared.public_key()));
let decoded = unwrap_chat_message(bob_shared.keys(), &event)
.await
.expect("unwrap");
assert_eq!(decoded.content, body);
assert_eq!(decoded.sender, alice.public_key());
}
#[tokio::test]
async fn unwrap_with_wrong_shared_key_fails() {
let (alice, _bob, alice_shared, _bob_shared) = shared_pair();
let intruder = Keys::generate();
let intruder_shared =
SharedKey::derive(intruder.secret_key(), &Keys::generate().public_key()).unwrap();
let event = wrap_chat_message(&alice, &alice_shared.public_key(), "for bob only")
.await
.expect("wrap");
let err = unwrap_chat_message(intruder_shared.keys(), &event)
.await
.expect_err("must not decrypt with foreign shared key");
assert!(matches!(
err,
crate::error::MostroError::MostroInternalErr(_)
));
}
#[tokio::test]
async fn unwrap_tampered_event_fails() {
let (_alice, _bob, alice_shared, bob_shared) = shared_pair();
let impostor = Keys::generate();
let inner = EventBuilder::text_note("forged")
.build(impostor.public_key())
.sign(&impostor)
.await
.unwrap();
let mut json: serde_json::Value = serde_json::from_str(&inner.as_json()).unwrap();
json["content"] = serde_json::Value::String("tampered".to_string());
let tampered_inner = json.to_string();
let ephemeral = Keys::generate();
let encrypted = nip44::encrypt(
ephemeral.secret_key(),
&alice_shared.public_key(),
tampered_inner,
nip44::Version::V2,
)
.unwrap();
let event = EventBuilder::new(Kind::GiftWrap, encrypted)
.tag(Tag::public_key(alice_shared.public_key()))
.sign_with_keys(&ephemeral)
.unwrap();
let err = unwrap_chat_message(bob_shared.keys(), &event)
.await
.expect_err("tampered inner must not verify");
assert!(matches!(
err,
crate::error::MostroError::MostroInternalErr(_)
));
}
#[tokio::test]
async fn unwrap_rejects_non_giftwrap_event() {
let alice = Keys::generate();
let other = Keys::generate();
let shared = SharedKey::derive(alice.secret_key(), &other.public_key()).unwrap();
let bogus = EventBuilder::text_note("not a wrap")
.build(alice.public_key())
.sign(&alice)
.await
.unwrap();
let err = unwrap_chat_message(shared.keys(), &bogus)
.await
.expect_err("non-giftwrap must error");
assert!(matches!(
err,
crate::error::MostroError::MostroInternalErr(_)
));
}
}