1use serde_json::{Value, json};
22
23use crate::nip44::{self, Nip44Error};
24use crate::nostr_event::{NostrEvent, NostrEventError, nostr_event_id, verify_transport};
25use crate::nostr_key::{schnorr_sign_digest, xonly_from_secret};
26
27pub const PAIR_REQUEST_KIND: u32 = 21050;
29pub const PAIR_ACK_KIND: u32 = 21051;
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum PairKind {
35 Request,
36 Ack,
37}
38
39impl PairKind {
40 fn kind(self) -> u32 {
41 match self {
42 PairKind::Request => PAIR_REQUEST_KIND,
43 PairKind::Ack => PAIR_ACK_KIND,
44 }
45 }
46 fn tag(self) -> &'static str {
49 match self {
50 PairKind::Request => "pair_req",
51 PairKind::Ack => "pair_ack",
52 }
53 }
54 fn from_kind(kind: u32) -> Option<PairKind> {
55 match kind {
56 PAIR_REQUEST_KIND => Some(PairKind::Request),
57 PAIR_ACK_KIND => Some(PairKind::Ack),
58 _ => None,
59 }
60 }
61}
62
63#[derive(Debug, PartialEq, Eq)]
64pub enum NipW1Error {
65 NotPairing,
67 Transport(NostrEventError),
69 Key,
71 Decrypt(Nip44Error),
73 BadPayload,
76}
77
78impl std::fmt::Display for NipW1Error {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 NipW1Error::NotPairing => write!(f, "event kind is not a wire pairing kind"),
82 NipW1Error::Transport(e) => write!(f, "pairing event transport: {e}"),
83 NipW1Error::Key => write!(f, "malformed secp256k1 key"),
84 NipW1Error::Decrypt(e) => write!(f, "pairing payload decrypt: {e}"),
85 NipW1Error::BadPayload => write!(f, "malformed pairing payload"),
86 }
87 }
88}
89
90pub fn build_pair_event(
94 pair: PairKind,
95 my_secp_sk: &[u8; 32],
96 peer_xonly: &[u8; 32],
97 my_card: &Value,
98 created_at: i64,
99) -> Result<NostrEvent, NipW1Error> {
100 let payload = json!({ "t": pair.tag(), "card": my_card }).to_string();
101 let ck = nip44::conversation_key(my_secp_sk, peer_xonly).map_err(|_| NipW1Error::Key)?;
102 let content = nip44::encrypt(&ck, &payload).map_err(NipW1Error::Decrypt)?;
103
104 let my_xonly = xonly_from_secret(my_secp_sk).map_err(|_| NipW1Error::Key)?;
105 let pubkey_hex = hex::encode(my_xonly);
106 let tags = vec![vec!["p".to_string(), hex::encode(peer_xonly)]];
107 let id = nostr_event_id(&pubkey_hex, created_at, pair.kind(), &tags, &content);
108 let sig = schnorr_sign_digest(my_secp_sk, &id).map_err(|_| NipW1Error::Key)?;
109
110 Ok(NostrEvent {
111 id: hex::encode(id),
112 pubkey: pubkey_hex,
113 created_at,
114 kind: pair.kind(),
115 tags,
116 content,
117 sig: hex::encode(sig),
118 })
119}
120
121pub fn open_pair_event(
128 ev: &NostrEvent,
129 my_secp_sk: &[u8; 32],
130) -> Result<(PairKind, Value), NipW1Error> {
131 let pair = PairKind::from_kind(ev.kind).ok_or(NipW1Error::NotPairing)?;
132 let sender_xonly = verify_transport(ev).map_err(NipW1Error::Transport)?;
134 let ck = nip44::conversation_key(my_secp_sk, &sender_xonly).map_err(|_| NipW1Error::Key)?;
136 let plaintext = nip44::decrypt(&ck, &ev.content).map_err(NipW1Error::Decrypt)?;
137
138 let v: Value = serde_json::from_str(&plaintext).map_err(|_| NipW1Error::BadPayload)?;
139 if v.get("t").and_then(Value::as_str) != Some(pair.tag()) {
141 return Err(NipW1Error::BadPayload);
142 }
143 let card = v.get("card").cloned().ok_or(NipW1Error::BadPayload)?;
144 Ok((pair, card))
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::agent_card::{build_agent_card, sign_agent_card};
151 use crate::nostr_key::generate_transport_key;
152 use crate::signing::generate_keypair;
153
154 fn a_signed_card(handle: &str) -> Value {
155 let (sk, pk) = generate_keypair();
156 sign_agent_card(&build_agent_card(handle, &pk, None, None, None), &sk)
157 }
158
159 #[test]
160 fn request_roundtrips_a_to_b() {
161 let (sk_a, _xa) = generate_transport_key();
162 let (sk_b, xb) = generate_transport_key();
163 let card_a = a_signed_card("slate-lotus");
164
165 let ev = build_pair_event(PairKind::Request, &sk_a, &xb, &card_a, 1_700_000_000).unwrap();
166 assert_eq!(ev.kind, PAIR_REQUEST_KIND);
167 assert_eq!(ev.tags[0], vec!["p".to_string(), hex::encode(xb)]);
169 assert!(!ev.content.contains("slate-lotus"));
171
172 let (pair, card) = open_pair_event(&ev, &sk_b).unwrap();
174 assert_eq!(pair, PairKind::Request);
175 assert_eq!(card, card_a);
176 }
177
178 #[test]
179 fn ack_roundtrips_and_carries_kind() {
180 let (sk_a, xa) = generate_transport_key();
181 let (sk_b, _xb) = generate_transport_key();
182 let card_b = a_signed_card("raven-kettle");
183 let ev = build_pair_event(PairKind::Ack, &sk_b, &xa, &card_b, 1_700_000_001).unwrap();
184 assert_eq!(ev.kind, PAIR_ACK_KIND);
185 let (pair, card) = open_pair_event(&ev, &sk_a).unwrap();
186 assert_eq!(pair, PairKind::Ack);
187 assert_eq!(card, card_b);
188 }
189
190 #[test]
191 fn a_third_party_cannot_decrypt() {
192 let (sk_a, _xa) = generate_transport_key();
193 let (_sk_b, xb) = generate_transport_key();
194 let (sk_c, _xc) = generate_transport_key(); let ev = build_pair_event(
196 PairKind::Request,
197 &sk_a,
198 &xb,
199 &a_signed_card("x"),
200 1_700_000_000,
201 )
202 .unwrap();
203 assert!(matches!(
205 open_pair_event(&ev, &sk_c),
206 Err(NipW1Error::Decrypt(_))
207 ));
208 }
209
210 #[test]
211 fn tampered_content_fails_transport() {
212 let (sk_a, _xa) = generate_transport_key();
213 let (sk_b, xb) = generate_transport_key();
214 let mut ev = build_pair_event(
215 PairKind::Request,
216 &sk_a,
217 &xb,
218 &a_signed_card("x"),
219 1_700_000_000,
220 )
221 .unwrap();
222 ev.content.push('A'); assert!(matches!(
224 open_pair_event(&ev, &sk_b),
225 Err(NipW1Error::Transport(_))
226 ));
227 }
228
229 #[test]
230 fn non_pairing_kind_rejected() {
231 let (sk_a, _xa) = generate_transport_key();
232 let (sk_b, xb) = generate_transport_key();
233 let mut ev = build_pair_event(
234 PairKind::Request,
235 &sk_a,
236 &xb,
237 &a_signed_card("x"),
238 1_700_000_000,
239 )
240 .unwrap();
241 ev.kind = 1;
244 ev.id = hex::encode(nostr_event_id(
245 &ev.pubkey,
246 ev.created_at,
247 ev.kind,
248 &ev.tags,
249 &ev.content,
250 ));
251 let sig = schnorr_sign_digest(&sk_a, &hex32(&ev.id)).unwrap();
252 ev.sig = hex::encode(sig);
253 assert_eq!(open_pair_event(&ev, &sk_b), Err(NipW1Error::NotPairing));
254 }
255
256 fn hex32(s: &str) -> [u8; 32] {
257 hex::decode(s).unwrap().as_slice().try_into().unwrap()
258 }
259}