use serde_json::{Value, json};
use crate::nip44::{self, Nip44Error};
use crate::nostr_event::{NostrEvent, NostrEventError, nostr_event_id, verify_transport};
use crate::nostr_key::{schnorr_sign_digest, xonly_from_secret};
pub const PAIR_REQUEST_KIND: u32 = 21050;
pub const PAIR_ACK_KIND: u32 = 21051;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PairKind {
Request,
Ack,
}
impl PairKind {
fn kind(self) -> u32 {
match self {
PairKind::Request => PAIR_REQUEST_KIND,
PairKind::Ack => PAIR_ACK_KIND,
}
}
fn tag(self) -> &'static str {
match self {
PairKind::Request => "pair_req",
PairKind::Ack => "pair_ack",
}
}
fn from_kind(kind: u32) -> Option<PairKind> {
match kind {
PAIR_REQUEST_KIND => Some(PairKind::Request),
PAIR_ACK_KIND => Some(PairKind::Ack),
_ => None,
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum NipW1Error {
NotPairing,
Transport(NostrEventError),
Key,
Decrypt(Nip44Error),
BadPayload,
}
impl std::fmt::Display for NipW1Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NipW1Error::NotPairing => write!(f, "event kind is not a wire pairing kind"),
NipW1Error::Transport(e) => write!(f, "pairing event transport: {e}"),
NipW1Error::Key => write!(f, "malformed secp256k1 key"),
NipW1Error::Decrypt(e) => write!(f, "pairing payload decrypt: {e}"),
NipW1Error::BadPayload => write!(f, "malformed pairing payload"),
}
}
}
pub fn build_pair_event(
pair: PairKind,
my_secp_sk: &[u8; 32],
peer_xonly: &[u8; 32],
my_card: &Value,
created_at: i64,
) -> Result<NostrEvent, NipW1Error> {
let payload = json!({ "t": pair.tag(), "card": my_card }).to_string();
let ck = nip44::conversation_key(my_secp_sk, peer_xonly).map_err(|_| NipW1Error::Key)?;
let content = nip44::encrypt(&ck, &payload).map_err(NipW1Error::Decrypt)?;
let my_xonly = xonly_from_secret(my_secp_sk).map_err(|_| NipW1Error::Key)?;
let pubkey_hex = hex::encode(my_xonly);
let tags = vec![vec!["p".to_string(), hex::encode(peer_xonly)]];
let id = nostr_event_id(&pubkey_hex, created_at, pair.kind(), &tags, &content);
let sig = schnorr_sign_digest(my_secp_sk, &id).map_err(|_| NipW1Error::Key)?;
Ok(NostrEvent {
id: hex::encode(id),
pubkey: pubkey_hex,
created_at,
kind: pair.kind(),
tags,
content,
sig: hex::encode(sig),
})
}
pub fn open_pair_event(
ev: &NostrEvent,
my_secp_sk: &[u8; 32],
) -> Result<(PairKind, Value), NipW1Error> {
let pair = PairKind::from_kind(ev.kind).ok_or(NipW1Error::NotPairing)?;
let sender_xonly = verify_transport(ev).map_err(NipW1Error::Transport)?;
let ck = nip44::conversation_key(my_secp_sk, &sender_xonly).map_err(|_| NipW1Error::Key)?;
let plaintext = nip44::decrypt(&ck, &ev.content).map_err(NipW1Error::Decrypt)?;
let v: Value = serde_json::from_str(&plaintext).map_err(|_| NipW1Error::BadPayload)?;
if v.get("t").and_then(Value::as_str) != Some(pair.tag()) {
return Err(NipW1Error::BadPayload);
}
let card = v.get("card").cloned().ok_or(NipW1Error::BadPayload)?;
Ok((pair, card))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent_card::{build_agent_card, sign_agent_card};
use crate::nostr_key::generate_transport_key;
use crate::signing::generate_keypair;
fn a_signed_card(handle: &str) -> Value {
let (sk, pk) = generate_keypair();
sign_agent_card(&build_agent_card(handle, &pk, None, None, None), &sk)
}
#[test]
fn request_roundtrips_a_to_b() {
let (sk_a, _xa) = generate_transport_key();
let (sk_b, xb) = generate_transport_key();
let card_a = a_signed_card("slate-lotus");
let ev = build_pair_event(PairKind::Request, &sk_a, &xb, &card_a, 1_700_000_000).unwrap();
assert_eq!(ev.kind, PAIR_REQUEST_KIND);
assert_eq!(ev.tags[0], vec!["p".to_string(), hex::encode(xb)]);
assert!(!ev.content.contains("slate-lotus"));
let (pair, card) = open_pair_event(&ev, &sk_b).unwrap();
assert_eq!(pair, PairKind::Request);
assert_eq!(card, card_a);
}
#[test]
fn ack_roundtrips_and_carries_kind() {
let (sk_a, xa) = generate_transport_key();
let (sk_b, _xb) = generate_transport_key();
let card_b = a_signed_card("raven-kettle");
let ev = build_pair_event(PairKind::Ack, &sk_b, &xa, &card_b, 1_700_000_001).unwrap();
assert_eq!(ev.kind, PAIR_ACK_KIND);
let (pair, card) = open_pair_event(&ev, &sk_a).unwrap();
assert_eq!(pair, PairKind::Ack);
assert_eq!(card, card_b);
}
#[test]
fn a_third_party_cannot_decrypt() {
let (sk_a, _xa) = generate_transport_key();
let (_sk_b, xb) = generate_transport_key();
let (sk_c, _xc) = generate_transport_key(); let ev = build_pair_event(
PairKind::Request,
&sk_a,
&xb,
&a_signed_card("x"),
1_700_000_000,
)
.unwrap();
assert!(matches!(
open_pair_event(&ev, &sk_c),
Err(NipW1Error::Decrypt(_))
));
}
#[test]
fn tampered_content_fails_transport() {
let (sk_a, _xa) = generate_transport_key();
let (sk_b, xb) = generate_transport_key();
let mut ev = build_pair_event(
PairKind::Request,
&sk_a,
&xb,
&a_signed_card("x"),
1_700_000_000,
)
.unwrap();
ev.content.push('A'); assert!(matches!(
open_pair_event(&ev, &sk_b),
Err(NipW1Error::Transport(_))
));
}
#[test]
fn non_pairing_kind_rejected() {
let (sk_a, _xa) = generate_transport_key();
let (sk_b, xb) = generate_transport_key();
let mut ev = build_pair_event(
PairKind::Request,
&sk_a,
&xb,
&a_signed_card("x"),
1_700_000_000,
)
.unwrap();
ev.kind = 1;
ev.id = hex::encode(nostr_event_id(
&ev.pubkey,
ev.created_at,
ev.kind,
&ev.tags,
&ev.content,
));
let sig = schnorr_sign_digest(&sk_a, &hex32(&ev.id)).unwrap();
ev.sig = hex::encode(sig);
assert_eq!(open_pair_event(&ev, &sk_b), Err(NipW1Error::NotPairing));
}
fn hex32(s: &str) -> [u8; 32] {
hex::decode(s).unwrap().as_slice().try_into().unwrap()
}
}