mod group;
mod invite;
mod manager;
mod sender_key;
pub use group::*;
pub use invite::*;
pub use manager::*;
pub use sender_key::*;
use crate::Nip44;
use base64::engine::{Engine as _, general_purpose};
use nostro2_traits::{NostrKeypair, SignerError, hex::Hexable as _};
use std::collections::BTreeMap;
use zeroize::Zeroize;
pub const MAX_SKIP: usize = 1000;
pub const MESSAGE_EVENT_KIND: u32 = 1060;
const HEADER_TAG: &str = "header";
#[derive(Debug)]
pub enum Nip104Error {
CannotSendYet,
SessionNotReady,
UnexpectedSender,
TooManySkippedMessages,
InvalidHeader,
InvalidInvite(String),
UnknownPeer(String),
Signer(SignerError),
Nip44(crate::Nip44Error),
Json(String),
Base64(base64::DecodeError),
}
impl std::fmt::Display for Nip104Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CannotSendYet => f.write_str("session cannot send yet"),
Self::SessionNotReady => f.write_str("session not ready: missing key material"),
Self::UnexpectedSender => f.write_str("envelope sender matches no known chain"),
Self::TooManySkippedMessages => f.write_str("too many skipped messages"),
Self::InvalidHeader => f.write_str("could not decrypt message header"),
Self::InvalidInvite(e) => write!(f, "invalid invite: {e}"),
Self::UnknownPeer(p) => write!(f, "no session with peer {p}"),
Self::Signer(e) => write!(f, "signer error: {e}"),
Self::Nip44(e) => write!(f, "nip-44 error: {e}"),
Self::Json(e) => write!(f, "json error: {e}"),
Self::Base64(e) => write!(f, "base64 error: {e}"),
}
}
}
impl std::error::Error for Nip104Error {}
impl From<SignerError> for Nip104Error {
fn from(e: SignerError) -> Self {
Self::Signer(e)
}
}
impl From<crate::Nip44Error> for Nip104Error {
fn from(e: crate::Nip44Error) -> Self {
Self::Nip44(e)
}
}
impl From<bourne::Error> for Nip104Error {
fn from(e: bourne::Error) -> Self {
Self::Json(format!("{e:?}"))
}
}
impl From<base64::DecodeError> for Nip104Error {
fn from(e: base64::DecodeError) -> Self {
Self::Base64(e)
}
}
type Result<T> = std::result::Result<T, Nip104Error>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyPairBytes {
pub public_key: String,
pub private_key: String,
}
impl KeyPairBytes {
fn secret_bytes(&self) -> Result<[u8; 32]> {
let mut buf = [0_u8; 32];
nostro2_traits::hex::FromHex::decode_hex_to_slice(self.private_key.as_str(), &mut buf)
.map_err(|_| Nip104Error::Signer(SignerError::InvalidPublicKey))?;
Ok(buf)
}
fn from_secret<K: NostrKeypair>(secret: &[u8; 32]) -> Result<Self> {
let kp = K::from_secret_bytes(secret)?;
Ok(Self {
public_key: kp.public_key(),
private_key: secret.to_hex(),
})
}
}
impl Drop for KeyPairBytes {
fn drop(&mut self) {
self.private_key.zeroize();
}
}
bourne::json! {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Header {
number: u32,
#[bourne(rename = "previousChainLength")]
previous_chain_length: u32,
#[bourne(rename = "nextPublicKey")]
next_public_key: String,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SkippedKeysEntry {
pub message_keys: BTreeMap<u32, String>,
}
impl Drop for SkippedKeysEntry {
fn drop(&mut self) {
for key in self.message_keys.values_mut() {
key.zeroize();
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionState {
pub root_key: String,
pub their_current_nostr_public_key: Option<String>,
pub their_next_nostr_public_key: Option<String>,
pub our_previous_nostr_key: Option<KeyPairBytes>,
pub our_current_nostr_key: Option<KeyPairBytes>,
pub our_next_nostr_key: KeyPairBytes,
pub receiving_chain_key: Option<String>,
pub sending_chain_key: Option<String>,
pub sending_chain_message_number: u32,
pub receiving_chain_message_number: u32,
pub previous_sending_chain_message_count: u32,
pub skipped_keys: BTreeMap<String, SkippedKeysEntry>,
}
impl Drop for SessionState {
fn drop(&mut self) {
self.root_key.zeroize();
if let Some(k) = self.receiving_chain_key.as_mut() {
k.zeroize();
}
if let Some(k) = self.sending_chain_key.as_mut() {
k.zeroize();
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageEnvelope {
pub sender: String,
pub encrypted_header: String,
pub ciphertext: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::redundant_pub_crate)]
pub(crate) enum HeaderTarget {
Current,
Next,
Previous,
}
#[derive(Debug, Clone)]
pub struct Session<K: NostrKeypair> {
pub state: SessionState,
_marker: std::marker::PhantomData<fn() -> K>,
}
impl<K: NostrKeypair> Session<K> {
#[must_use]
pub fn from_state(state: SessionState) -> Self {
Self {
state,
_marker: std::marker::PhantomData,
}
}
pub fn new_initiator(
their_ephemeral_pubkey: &[u8; 32],
our_secret: &[u8; 32],
shared_secret: &[u8; 32],
) -> Result<Self> {
Self::init(their_ephemeral_pubkey, our_secret, true, shared_secret)
}
pub fn new_responder(
their_ephemeral_pubkey: &[u8; 32],
our_secret: &[u8; 32],
shared_secret: &[u8; 32],
) -> Result<Self> {
Self::init(their_ephemeral_pubkey, our_secret, false, shared_secret)
}
fn init(
their_ephemeral_pubkey: &[u8; 32],
our_secret: &[u8; 32],
is_initiator: bool,
shared_secret: &[u8; 32],
) -> Result<Self> {
let our_current = KeyPairBytes::from_secret::<K>(our_secret)?;
let their_next_hex = their_ephemeral_pubkey.to_hex();
let state = if is_initiator {
let our_next_secret = K::generate().secret_bytes();
let our_next = KeyPairBytes::from_secret::<K>(&our_next_secret)?;
let conv = K::derive_conv_key(&our_next_secret, their_ephemeral_pubkey)?;
let outs = K::kdf(shared_secret, &conv, 2);
SessionState {
root_key: outs[0].to_hex(),
their_current_nostr_public_key: None,
their_next_nostr_public_key: Some(their_next_hex),
our_previous_nostr_key: None,
our_current_nostr_key: Some(our_current),
our_next_nostr_key: our_next,
receiving_chain_key: None,
sending_chain_key: Some(outs[1].to_hex()),
sending_chain_message_number: 0,
receiving_chain_message_number: 0,
previous_sending_chain_message_count: 0,
skipped_keys: BTreeMap::new(),
}
} else {
SessionState {
root_key: shared_secret.to_hex(),
their_current_nostr_public_key: None,
their_next_nostr_public_key: Some(their_next_hex),
our_previous_nostr_key: None,
our_current_nostr_key: None,
our_next_nostr_key: our_current,
receiving_chain_key: None,
sending_chain_key: None,
sending_chain_message_number: 0,
receiving_chain_message_number: 0,
previous_sending_chain_message_count: 0,
skipped_keys: BTreeMap::new(),
}
};
Ok(Self::from_state(state))
}
#[must_use]
pub const fn can_send(&self) -> bool {
self.state.their_next_nostr_public_key.is_some()
&& self.state.our_current_nostr_key.is_some()
}
fn matches_sender(&self, sender: &str) -> bool {
self.state.their_current_nostr_public_key.as_deref() == Some(sender)
|| self.state.their_next_nostr_public_key.as_deref() == Some(sender)
|| self.state.skipped_keys.contains_key(sender)
}
#[must_use]
pub fn accepted_senders(&self) -> Vec<String> {
let mut out = Vec::with_capacity(2 + self.state.skipped_keys.len());
if let Some(cur) = self.state.their_current_nostr_public_key.as_deref() {
out.push(cur.to_owned());
}
if let Some(next) = self.state.their_next_nostr_public_key.as_deref()
&& Some(next) != self.state.their_current_nostr_public_key.as_deref()
{
out.push(next.to_owned());
}
for k in self.state.skipped_keys.keys() {
if Some(k.as_str()) != self.state.their_current_nostr_public_key.as_deref()
&& Some(k.as_str()) != self.state.their_next_nostr_public_key.as_deref()
{
out.push(k.clone());
}
}
out
}
pub fn plan_send(&self, payload: &[u8]) -> Result<(SessionState, MessageEnvelope)> {
if !self.can_send() {
return Err(Nip104Error::CannotSendYet);
}
let mut next = self.state.clone();
let (header, ciphertext) = K::ratchet_encrypt(&mut next, payload)?;
let our_current = self
.state
.our_current_nostr_key
.as_ref()
.ok_or(Nip104Error::SessionNotReady)?;
let their_next = self
.state
.their_next_nostr_public_key
.as_deref()
.ok_or(Nip104Error::SessionNotReady)?;
let our_kp = K::from_secret_bytes(&our_current.secret_bytes()?)?;
let header_json = bourne::to_string(&header)?;
let encrypted_header = our_kp
.nip_44_encrypt(&header_json, their_next)?
.into_owned();
Ok((
next,
MessageEnvelope {
sender: our_current.public_key.clone(),
encrypted_header,
ciphertext,
},
))
}
pub fn plan_receive(&self, envelope: &MessageEnvelope) -> Result<(SessionState, Vec<u8>)> {
if !self.matches_sender(&envelope.sender) {
return Err(Nip104Error::UnexpectedSender);
}
let mut next = self.state.clone();
let previous_chain_sender = next
.their_current_nostr_public_key
.clone()
.or_else(|| next.their_next_nostr_public_key.clone());
let (header, target) =
K::decrypt_header(&next, &envelope.encrypted_header, &envelope.sender)?;
let should_ratchet = target == HeaderTarget::Next;
if should_ratchet
&& next.their_next_nostr_public_key.as_ref() != Some(&header.next_public_key)
{
next.their_current_nostr_public_key = next.their_next_nostr_public_key.take();
next.their_next_nostr_public_key = Some(header.next_public_key.clone());
}
if should_ratchet {
if next.receiving_chain_key.is_some() {
let skipped_sender = previous_chain_sender.ok_or(Nip104Error::SessionNotReady)?;
K::skip_message_keys(&mut next, header.previous_chain_length, &skipped_sender)?;
}
K::ratchet_step(&mut next)?;
}
let payload =
K::ratchet_decrypt(&mut next, &header, &envelope.ciphertext, &envelope.sender)?;
Ok((next, payload))
}
pub fn apply(&mut self, next: SessionState) {
self.state = next;
}
pub fn plan_send_event(
&self,
payload: &[u8],
created_at: i64,
) -> Result<(SessionState, nostro2::NostrNote)> {
let (next, envelope) = self.plan_send(payload)?;
let our_current = self
.state
.our_current_nostr_key
.as_ref()
.ok_or(Nip104Error::SessionNotReady)?;
let signer = K::from_secret_bytes(&our_current.secret_bytes()?)?;
let event = envelope.to_event(&signer, created_at)?;
Ok((next, event))
}
pub fn plan_receive_event(
&self,
event: &nostro2::NostrNote,
) -> Result<(SessionState, Vec<u8>)> {
let envelope = MessageEnvelope::from_event(event)?;
self.plan_receive(&envelope)
}
}
impl MessageEnvelope {
pub fn to_event<S: nostro2::NostrSigner>(
&self,
signer: &S,
created_at: i64,
) -> Result<nostro2::NostrNote> {
let mut tags = nostro2::NostrTags::new();
tags.add_custom_tag(HEADER_TAG, &self.encrypted_header);
let mut note = nostro2::NostrNote {
kind: MESSAGE_EVENT_KIND,
content: self.ciphertext.clone(),
created_at,
tags,
..Default::default()
};
note.sign_with(signer)
.map_err(|_| Nip104Error::Signer(SignerError::InvalidSignature))?;
Ok(note)
}
pub fn from_event(event: &nostro2::NostrNote) -> Result<Self> {
use nostro2::NostrEvent as _;
if event.kind != MESSAGE_EVENT_KIND {
return Err(Nip104Error::InvalidHeader);
}
if !event.verify() {
return Err(Nip104Error::InvalidHeader);
}
let encrypted_header = event
.tags
.find_tags_ref(HEADER_TAG)
.into_iter()
.next()
.ok_or(Nip104Error::InvalidHeader)?
.to_owned();
Ok(Self {
sender: event.pubkey.clone(),
encrypted_header,
ciphertext: event.content.clone(),
})
}
}
#[allow(clippy::redundant_pub_crate)]
pub(crate) trait Nip104Crypto: NostrKeypair + Sized {
fn kdf(input1: &[u8], input2: &[u8], num_outputs: usize) -> Vec<[u8; 32]> {
let hk = hkdf::Hkdf::<sha2::Sha256>::new(Some(input2), input1);
let mut outputs = Vec::with_capacity(num_outputs);
for i in 1..=num_outputs {
let mut okm = [0_u8; 32];
hk.expand(&[u8::try_from(i).unwrap_or(u8::MAX)], &mut okm)
.expect("32 bytes is a valid HKDF length");
outputs.push(okm);
}
outputs
}
fn decode_hex_32(s: &str) -> Result<[u8; 32]> {
let mut buf = [0_u8; 32];
nostro2_traits::hex::FromHex::decode_hex_to_slice(s, &mut buf)
.map_err(|_| Nip104Error::Signer(SignerError::InvalidPublicKey))?;
Ok(buf)
}
fn derive_conv_key(sk: &[u8; 32], pk: &[u8; 32]) -> Result<[u8; 32]> {
let kp = Self::from_secret_bytes(sk)?;
let shared = kp.ecdh_x(pk)?;
let conv = <Self as Nip44>::conversation_key_v2(zeroize::Zeroizing::new(shared))?;
Ok(*conv)
}
fn encrypt_with_message_key(message_key: &[u8; 32], plaintext: &[u8]) -> Result<String> {
let nonce = Self::generate_nonce_32();
Ok(<Self as Nip44>::encrypt_v2(message_key, &nonce, plaintext)?)
}
fn decrypt_with_message_key(message_key: &[u8; 32], ciphertext_b64: &str) -> Result<Vec<u8>> {
let decoded = general_purpose::STANDARD.decode(ciphertext_b64)?;
let s = <Self as Nip44>::decrypt_v2_bytes(message_key, &decoded)?;
Ok(s)
}
fn ratchet_encrypt(state: &mut SessionState, plaintext: &[u8]) -> Result<(Header, String)> {
let sending_chain_key = Self::decode_hex_32(
state
.sending_chain_key
.as_deref()
.ok_or(Nip104Error::SessionNotReady)?,
)?;
let outs = Self::kdf(&sending_chain_key, &[1_u8], 2);
state.sending_chain_key = Some(outs[0].to_hex());
let message_key = outs[1];
let header = Header {
number: state.sending_chain_message_number,
next_public_key: state.our_next_nostr_key.public_key.clone(),
previous_chain_length: state.previous_sending_chain_message_count,
};
state.sending_chain_message_number += 1;
let ciphertext = Self::encrypt_with_message_key(&message_key, plaintext)?;
Ok((header, ciphertext))
}
fn ratchet_decrypt(
state: &mut SessionState,
header: &Header,
ciphertext: &str,
sender: &str,
) -> Result<Vec<u8>> {
if let Some(pt) = Self::try_skipped_message_keys(state, header, ciphertext, sender)? {
return Ok(pt);
}
if state.receiving_chain_key.is_none() {
return Err(Nip104Error::SessionNotReady);
}
Self::skip_message_keys(state, header.number, sender)?;
let receiving_chain_key = Self::decode_hex_32(
state
.receiving_chain_key
.as_deref()
.ok_or(Nip104Error::SessionNotReady)?,
)?;
let outs = Self::kdf(&receiving_chain_key, &[1_u8], 2);
state.receiving_chain_key = Some(outs[0].to_hex());
let message_key = outs[1];
state.receiving_chain_message_number += 1;
Self::decrypt_with_message_key(&message_key, ciphertext)
}
fn ratchet_step(state: &mut SessionState) -> Result<()> {
state.previous_sending_chain_message_count = state.sending_chain_message_number;
state.sending_chain_message_number = 0;
state.receiving_chain_message_number = 0;
let their_next = state
.their_next_nostr_public_key
.as_deref()
.ok_or(Nip104Error::SessionNotReady)?;
let their_next_bytes = Self::decode_hex_32(their_next)?;
let root_key = Self::decode_hex_32(&state.root_key)?;
let conv1 =
Self::derive_conv_key(&state.our_next_nostr_key.secret_bytes()?, &their_next_bytes)?;
let outs1 = Self::kdf(&root_key, &conv1, 2);
state.receiving_chain_key = Some(outs1[1].to_hex());
state.our_previous_nostr_key = state.our_current_nostr_key.take();
state.our_current_nostr_key = Some(state.our_next_nostr_key.clone());
let our_next_secret = Self::generate().secret_bytes();
state.our_next_nostr_key = KeyPairBytes::from_secret::<Self>(&our_next_secret)?;
let conv2 = Self::derive_conv_key(&our_next_secret, &their_next_bytes)?;
let outs2 = Self::kdf(&outs1[0], &conv2, 2);
state.root_key = outs2[0].to_hex();
state.sending_chain_key = Some(outs2[1].to_hex());
Ok(())
}
fn skip_message_keys(state: &mut SessionState, until: u32, sender: &str) -> Result<()> {
if until <= state.receiving_chain_message_number {
return Ok(());
}
if (until - state.receiving_chain_message_number) as usize > MAX_SKIP {
return Err(Nip104Error::TooManySkippedMessages);
}
let entry = state.skipped_keys.entry(sender.to_owned()).or_default();
while state.receiving_chain_message_number < until {
let rck = Self::decode_hex_32(
state
.receiving_chain_key
.as_deref()
.ok_or(Nip104Error::SessionNotReady)?,
)?;
let outs = Self::kdf(&rck, &[1_u8], 2);
state.receiving_chain_key = Some(outs[0].to_hex());
entry
.message_keys
.insert(state.receiving_chain_message_number, outs[1].to_hex());
state.receiving_chain_message_number += 1;
}
Self::prune_skipped(&mut entry.message_keys);
Ok(())
}
fn try_skipped_message_keys(
state: &mut SessionState,
header: &Header,
ciphertext: &str,
sender: &str,
) -> Result<Option<Vec<u8>>> {
let Some(entry) = state.skipped_keys.get_mut(sender) else {
return Ok(None);
};
let Some(mk_hex) = entry.message_keys.remove(&header.number) else {
return Ok(None);
};
let message_key = Self::decode_hex_32(&mk_hex)?;
let pt = Self::decrypt_with_message_key(&message_key, ciphertext)?;
if entry.message_keys.is_empty() {
state.skipped_keys.remove(sender);
}
Ok(Some(pt))
}
fn decrypt_header(
state: &SessionState,
encrypted_header: &str,
sender: &str,
) -> Result<(Header, HeaderTarget)> {
if let Some(current) = &state.our_current_nostr_key
&& let Ok(h) =
Self::try_decrypt_header(¤t.secret_bytes()?, sender, encrypted_header)
{
return Ok((h, HeaderTarget::Current));
}
if let Ok(h) = Self::try_decrypt_header(
&state.our_next_nostr_key.secret_bytes()?,
sender,
encrypted_header,
) {
return Ok((h, HeaderTarget::Next));
}
if let Some(previous) = &state.our_previous_nostr_key
&& let Ok(h) =
Self::try_decrypt_header(&previous.secret_bytes()?, sender, encrypted_header)
{
return Ok((h, HeaderTarget::Previous));
}
Err(Nip104Error::InvalidHeader)
}
fn try_decrypt_header(
our_secret: &[u8; 32],
sender: &str,
encrypted_header: &str,
) -> Result<Header> {
let kp = Self::from_secret_bytes(our_secret)?;
let json = kp.nip_44_decrypt(encrypted_header, sender)?;
Ok(bourne::parse_str(&json)?)
}
fn prune_skipped(map: &mut BTreeMap<u32, String>) {
while map.len() > MAX_SKIP {
let Some(first) = map.keys().next().copied() else {
break;
};
map.remove(&first);
}
}
}
impl<K: NostrKeypair> Nip104Crypto for K {}
#[cfg(test)]
mod tests {
use super::*;
use nostro2_traits::NostrSigner as _;
type K = crate::tests::NipTester;
fn shared_secret() -> [u8; 32] {
[7_u8; 32]
}
#[test]
fn kdf_matches_reference_shape() {
let a = K::kdf(&[1_u8; 32], &[2_u8; 32], 2);
let b = K::kdf(&[1_u8; 32], &[2_u8; 32], 2);
assert_eq!(a, b);
assert_eq!(a.len(), 2);
assert_ne!(a[0], a[1]);
}
#[test]
fn header_json_is_camel_case_and_round_trips() {
let h = Header {
number: 3,
previous_chain_length: 2,
next_public_key: "ab".repeat(32),
};
let s = bourne::to_string(&h).unwrap();
assert!(s.contains("\"previousChainLength\":2"), "got {s}");
assert!(s.contains("\"nextPublicKey\":"), "got {s}");
assert!(!s.contains("previous_chain_length"), "got {s}");
let back: Header = bourne::parse_str(&s).unwrap();
assert_eq!(back, h);
}
#[test]
fn round_trip_single_message() {
let alice_secret = [1_u8; 32];
let bob_secret = [2_u8; 32];
let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
let alice = Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
let mut bob =
Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
let (alice_next, envelope) = alice.plan_send(b"hello bob").unwrap();
let _ = alice_next;
let (bob_next, plaintext) = bob.plan_receive(&envelope).unwrap();
bob.apply(bob_next);
assert_eq!(plaintext, b"hello bob");
}
#[test]
fn bidirectional_conversation_ratchets() {
let alice_secret = [1_u8; 32];
let bob_secret = [2_u8; 32];
let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
let mut alice =
Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
let mut bob =
Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
for msg in [b"a1".as_slice(), b"a2".as_slice()] {
let (an, env) = alice.plan_send(msg).unwrap();
alice.apply(an);
let (bn, pt) = bob.plan_receive(&env).unwrap();
bob.apply(bn);
assert_eq!(pt, msg);
}
for msg in [b"b1".as_slice(), b"b2".as_slice(), b"b3".as_slice()] {
let (bn, env) = bob.plan_send(msg).unwrap();
bob.apply(bn);
let (an, pt) = alice.plan_receive(&env).unwrap();
alice.apply(an);
assert_eq!(pt, msg);
}
let (an, env) = alice.plan_send(b"a3").unwrap();
alice.apply(an);
let (bn, pt) = bob.plan_receive(&env).unwrap();
bob.apply(bn);
assert_eq!(pt, b"a3");
}
#[test]
fn out_of_order_delivery_uses_skipped_keys() {
let alice_secret = [3_u8; 32];
let bob_secret = [4_u8; 32];
let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
let mut alice =
Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
let mut bob =
Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
let (a1, env1) = alice.plan_send(b"first").unwrap();
alice.apply(a1);
let (a2, env2) = alice.plan_send(b"second").unwrap();
alice.apply(a2);
let (a3, env3) = alice.plan_send(b"third").unwrap();
alice.apply(a3);
let (bn, pt1) = bob.plan_receive(&env1).unwrap();
bob.apply(bn);
assert_eq!(pt1, b"first");
let (bn, pt3) = bob.plan_receive(&env3).unwrap();
bob.apply(bn);
assert_eq!(pt3, b"third");
let (bn, pt2) = bob.plan_receive(&env2).unwrap();
bob.apply(bn);
assert_eq!(pt2, b"second");
}
#[test]
fn rust_reference_vector_msg1_decrypts() {
let vec_json = include_str!("../../test-vectors/nip104-rust-generated.json");
let field = |key: &str| -> String {
let needle = format!("\"{key}\":");
let start = vec_json.find(&needle).expect("key present") + needle.len();
let rest = &vec_json[start..];
let q1 = rest.find('"').unwrap() + 1;
let q2 = rest[q1..].find('"').unwrap();
rest[q1..q1 + q2].to_string()
};
let bob_sk = K::decode_hex_32(&field("bob_ephemeral_sk")).unwrap();
let alice_pk = K::decode_hex_32(&field("alice_ephemeral_pk")).unwrap();
let shared = K::decode_hex_32(&field("shared_secret")).unwrap();
let plaintext = field("plaintext");
let sender = field("pubkey");
let header = field("header");
let content = field("content");
let mut bob = Session::<K>::new_responder(&alice_pk, &bob_sk, &shared).unwrap();
let envelope = MessageEnvelope {
sender,
encrypted_header: header,
ciphertext: content,
};
let (next, payload) = bob
.plan_receive(&envelope)
.expect("reference msg1 must decrypt under native ratchet");
bob.apply(next);
let decoded = String::from_utf8(payload).expect("payload is UTF-8 JSON");
assert!(
decoded.contains(&plaintext),
"decrypted rumor {decoded:?} must contain plaintext {plaintext:?}"
);
}
#[test]
fn rust_reference_msg1_event_decrypts_via_codec() {
let vec_json = include_str!("../../test-vectors/nip104-rust-generated.json");
let field = |key: &str| -> String {
let needle = format!("\"{key}\":");
let start = vec_json.find(&needle).expect("key present") + needle.len();
let rest = &vec_json[start..];
let q1 = rest.find('"').unwrap() + 1;
let q2 = rest[q1..].find('"').unwrap();
rest[q1..q1 + q2].to_string()
};
let bob_sk = K::decode_hex_32(&field("bob_ephemeral_sk")).unwrap();
let alice_pk = K::decode_hex_32(&field("alice_ephemeral_pk")).unwrap();
let shared = K::decode_hex_32(&field("shared_secret")).unwrap();
let plaintext = field("plaintext");
let ev_start = vec_json.find("\"msg1_event\":").unwrap() + "\"msg1_event\":".len();
let obj_start = vec_json[ev_start..].find('{').unwrap() + ev_start;
let obj_end = vec_json[obj_start..].find('}').unwrap() + obj_start + 1;
let event: nostro2::NostrNote = vec_json[obj_start..obj_end].parse().unwrap();
let mut bob = Session::<K>::new_responder(&alice_pk, &bob_sk, &shared).unwrap();
let (next, payload) = bob
.plan_receive_event(&event)
.expect("reference event must decrypt via the native codec");
bob.apply(next);
let decoded = String::from_utf8(payload).unwrap();
assert!(decoded.contains(&plaintext), "got {decoded:?}");
}
#[test]
fn message_event_codec_round_trips() {
use nostro2::NostrEvent as _;
let alice_secret = [1_u8; 32];
let bob_secret = [2_u8; 32];
let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
let mut alice =
Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
let mut bob =
Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
let (anext, event) = alice
.plan_send_event(b"over the wire", 1_700_000_000)
.unwrap();
alice.apply(anext);
assert_eq!(event.kind, MESSAGE_EVENT_KIND);
assert!(event.verify(), "event must be self-consistently signed");
assert_eq!(event.created_at, 1_700_000_000);
assert_eq!(event.tags.find_tags_ref(HEADER_TAG).len(), 1);
assert_eq!(event.pubkey, alice_pub.to_hex());
let (bnext, pt) = bob.plan_receive_event(&event).unwrap();
bob.apply(bnext);
assert_eq!(pt, b"over the wire");
}
#[test]
fn message_event_rejects_tampering() {
let alice_secret = [1_u8; 32];
let bob_secret = [2_u8; 32];
let alice_pub = K::from_secret_bytes(&alice_secret).unwrap().pubkey_bytes();
let bob_pub = K::from_secret_bytes(&bob_secret).unwrap().pubkey_bytes();
let alice = Session::<K>::new_initiator(&bob_pub, &alice_secret, &shared_secret()).unwrap();
let bob = Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
let (_anext, mut event) = alice.plan_send_event(b"tamper me", 1_700_000_000).unwrap();
event.content.push('A');
assert!(matches!(
bob.plan_receive_event(&event),
Err(Nip104Error::InvalidHeader)
));
}
#[test]
fn unknown_sender_rejected() {
let bob_secret = [2_u8; 32];
let alice_pub = K::from_secret_bytes(&[1_u8; 32]).unwrap().pubkey_bytes();
let bob = Session::<K>::new_responder(&alice_pub, &bob_secret, &shared_secret()).unwrap();
let envelope = MessageEnvelope {
sender: "cd".repeat(32),
encrypted_header: "x".into(),
ciphertext: "x".into(),
};
assert!(matches!(
bob.plan_receive(&envelope),
Err(Nip104Error::UnexpectedSender)
));
}
#[test]
fn error_display_covers_all_variants() {
use std::error::Error as _;
let cases: Vec<Nip104Error> = vec![
Nip104Error::CannotSendYet,
Nip104Error::SessionNotReady,
Nip104Error::UnexpectedSender,
Nip104Error::TooManySkippedMessages,
Nip104Error::InvalidHeader,
Nip104Error::InvalidInvite("bad layer".into()),
Nip104Error::UnknownPeer("npub1xyz".into()),
Nip104Error::Signer(SignerError::InvalidSignature),
Nip104Error::Nip44(crate::Nip44Error::MacMismatch),
Nip104Error::Json("unexpected token".into()),
Nip104Error::Base64(general_purpose::STANDARD.decode("!!!").unwrap_err()),
];
for err in &cases {
assert!(!format!("{err}").is_empty(), "Display empty for {err:?}");
let _ = err.source();
}
}
}