1use base64::Engine as _;
32use base64::engine::general_purpose::STANDARD as B64;
33use chacha20::ChaCha20;
34use chacha20::cipher::{KeyIvInit, StreamCipher};
35use hkdf::Hkdf;
36use hmac::{Hmac, Mac};
37use secp256k1::{Parity, PublicKey, SecretKey, XOnlyPublicKey};
38use sha2::Sha256;
39
40type HmacSha256 = Hmac<Sha256>;
41
42const VERSION: u8 = 0x02;
43const SALT: &[u8] = b"nip44-v2";
44const MIN_PLAINTEXT: usize = 1;
45const MAX_PLAINTEXT: usize = 65535;
46
47#[derive(Debug, PartialEq, Eq)]
48pub enum Nip44Error {
49 Key,
51 PlaintextLen,
53 BadPayload,
55 Mac,
57 Padding,
59 Utf8,
61}
62
63impl std::fmt::Display for Nip44Error {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 let s = match self {
66 Nip44Error::Key => "invalid secp256k1 key / ECDH failure",
67 Nip44Error::PlaintextLen => "plaintext length out of range (1..=65535)",
68 Nip44Error::BadPayload => "malformed NIP-44 payload",
69 Nip44Error::Mac => "NIP-44 MAC verification failed",
70 Nip44Error::Padding => "malformed NIP-44 padding",
71 Nip44Error::Utf8 => "decrypted bytes are not valid UTF-8",
72 };
73 write!(f, "{s}")
74 }
75}
76
77pub fn conversation_key(
80 my_secp_sk: &[u8; 32],
81 their_xonly: &[u8; 32],
82) -> Result<[u8; 32], Nip44Error> {
83 let sk = SecretKey::from_byte_array(*my_secp_sk).map_err(|_| Nip44Error::Key)?;
84 let xonly = XOnlyPublicKey::from_byte_array(*their_xonly).map_err(|_| Nip44Error::Key)?;
85 let pk = PublicKey::from_x_only_public_key(xonly, Parity::Even);
87 let point = secp256k1::ecdh::shared_secret_point(&pk, &sk);
89 let (prk, _) = Hkdf::<Sha256>::extract(Some(SALT), &point[..32]);
90 let mut ck = [0u8; 32];
91 ck.copy_from_slice(&prk);
92 Ok(ck)
93}
94
95pub fn calc_padded_len(unpadded: usize) -> usize {
99 if unpadded <= 32 {
100 return 32;
101 }
102 let next_power = 1usize << ((unpadded - 1).ilog2() + 1);
104 let chunk = if next_power <= 256 {
105 32
106 } else {
107 next_power / 8
108 };
109 chunk * ((unpadded - 1) / chunk + 1)
110}
111
112fn pad(plaintext: &[u8]) -> Result<Vec<u8>, Nip44Error> {
114 let n = plaintext.len();
115 if !(MIN_PLAINTEXT..=MAX_PLAINTEXT).contains(&n) {
116 return Err(Nip44Error::PlaintextLen);
117 }
118 let total = 2 + calc_padded_len(n);
119 let mut buf = vec![0u8; total];
120 buf[0..2].copy_from_slice(&(n as u16).to_be_bytes());
121 buf[2..2 + n].copy_from_slice(plaintext);
122 Ok(buf)
123}
124
125fn unpad(buf: &[u8]) -> Result<Vec<u8>, Nip44Error> {
127 if buf.len() < 2 {
128 return Err(Nip44Error::Padding);
129 }
130 let n = u16::from_be_bytes([buf[0], buf[1]]) as usize;
131 if !(MIN_PLAINTEXT..=MAX_PLAINTEXT).contains(&n) {
132 return Err(Nip44Error::Padding);
133 }
134 if buf.len() != 2 + calc_padded_len(n) {
135 return Err(Nip44Error::Padding);
136 }
137 Ok(buf[2..2 + n].to_vec())
138}
139
140fn message_keys(conversation_key: &[u8; 32], nonce: &[u8; 32]) -> ([u8; 32], [u8; 12], [u8; 32]) {
143 let hk = Hkdf::<Sha256>::from_prk(conversation_key).expect("32-byte PRK is valid");
144 let mut okm = [0u8; 76];
145 hk.expand(nonce, &mut okm).expect("76 < 255*32");
146 let mut ck = [0u8; 32];
147 let mut cn = [0u8; 12];
148 let mut hm = [0u8; 32];
149 ck.copy_from_slice(&okm[0..32]);
150 cn.copy_from_slice(&okm[32..44]);
151 hm.copy_from_slice(&okm[44..76]);
152 (ck, cn, hm)
153}
154
155fn hmac(hmac_key: &[u8; 32], nonce: &[u8; 32], ciphertext: &[u8]) -> [u8; 32] {
156 let mut mac = HmacSha256::new_from_slice(hmac_key).expect("hmac accepts any key length");
157 mac.update(nonce);
158 mac.update(ciphertext);
159 let out = mac.finalize().into_bytes();
160 let mut t = [0u8; 32];
161 t.copy_from_slice(&out);
162 t
163}
164
165pub fn encrypt_with_nonce(
169 conversation_key: &[u8; 32],
170 nonce: &[u8; 32],
171 plaintext: &str,
172) -> Result<String, Nip44Error> {
173 let (ck, cn, hm) = message_keys(conversation_key, nonce);
174 let mut buf = pad(plaintext.as_bytes())?;
175 ChaCha20::new(&ck.into(), &cn.into()).apply_keystream(&mut buf);
176 let mac = hmac(&hm, nonce, &buf);
177
178 let mut payload = Vec::with_capacity(1 + 32 + buf.len() + 32);
179 payload.push(VERSION);
180 payload.extend_from_slice(nonce);
181 payload.extend_from_slice(&buf);
182 payload.extend_from_slice(&mac);
183 Ok(B64.encode(&payload))
184}
185
186pub fn encrypt(conversation_key: &[u8; 32], plaintext: &str) -> Result<String, Nip44Error> {
188 use rand::RngCore;
189 let mut nonce = [0u8; 32];
190 rand::thread_rng().fill_bytes(&mut nonce);
191 encrypt_with_nonce(conversation_key, &nonce, plaintext)
192}
193
194pub fn decrypt(conversation_key: &[u8; 32], payload_b64: &str) -> Result<String, Nip44Error> {
197 let payload = B64
198 .decode(payload_b64.as_bytes())
199 .map_err(|_| Nip44Error::BadPayload)?;
200 if payload.len() < 1 + 32 + 34 + 32 || payload[0] != VERSION {
203 return Err(Nip44Error::BadPayload);
204 }
205 let nonce: [u8; 32] = payload[1..33].try_into().unwrap();
206 let mac_start = payload.len() - 32;
207 let ciphertext = &payload[33..mac_start];
208 let their_mac = &payload[mac_start..];
209
210 let (ck, cn, hm) = message_keys(conversation_key, &nonce);
211 let mut mac = HmacSha256::new_from_slice(&hm).expect("hmac accepts any key length");
213 mac.update(&nonce);
214 mac.update(ciphertext);
215 mac.verify_slice(their_mac).map_err(|_| Nip44Error::Mac)?;
216
217 let mut buf = ciphertext.to_vec();
218 ChaCha20::new(&ck.into(), &cn.into()).apply_keystream(&mut buf);
219 let plaintext = unpad(&buf)?;
220 String::from_utf8(plaintext).map_err(|_| Nip44Error::Utf8)
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::nostr_key::generate_transport_key;
227
228 #[test]
229 fn conversation_key_is_symmetric() {
230 let (sk_a, pub_a) = generate_transport_key();
232 let (sk_b, pub_b) = generate_transport_key();
233 let ck_ab = conversation_key(&sk_a, &pub_b).unwrap();
234 let ck_ba = conversation_key(&sk_b, &pub_a).unwrap();
235 assert_eq!(ck_ab, ck_ba, "ECDH conversation key must be symmetric");
236 }
237
238 #[test]
239 fn encrypt_decrypt_roundtrip() {
240 let (sk_a, _pa) = generate_transport_key();
241 let (_sb, pub_b) = generate_transport_key();
242 let ck = conversation_key(&sk_a, &pub_b).unwrap();
243 for msg in ["x", "hello over nostr", &"A".repeat(1000)] {
244 let ct = encrypt(&ck, msg).unwrap();
245 assert_eq!(decrypt(&ck, &ct).unwrap(), msg);
246 }
247 }
248
249 #[test]
250 fn the_other_party_decrypts() {
251 let (sk_a, pub_a) = generate_transport_key();
253 let (sk_b, pub_b) = generate_transport_key();
254 let ck_a = conversation_key(&sk_a, &pub_b).unwrap();
255 let ck_b = conversation_key(&sk_b, &pub_a).unwrap();
256 let ct = encrypt(&ck_a, "private to bob").unwrap();
257 assert_eq!(decrypt(&ck_b, &ct).unwrap(), "private to bob");
258 }
259
260 #[test]
261 fn deterministic_with_fixed_nonce() {
262 let (sk_a, _pa) = generate_transport_key();
263 let (_sb, pub_b) = generate_transport_key();
264 let ck = conversation_key(&sk_a, &pub_b).unwrap();
265 let nonce = [7u8; 32];
266 assert_eq!(
267 encrypt_with_nonce(&ck, &nonce, "same").unwrap(),
268 encrypt_with_nonce(&ck, &nonce, "same").unwrap()
269 );
270 }
271
272 #[test]
273 fn tampered_ciphertext_fails_mac() {
274 let (sk_a, _pa) = generate_transport_key();
275 let (_sb, pub_b) = generate_transport_key();
276 let ck = conversation_key(&sk_a, &pub_b).unwrap();
277 let ct = encrypt(&ck, "tamperme").unwrap();
278 let mut raw = B64.decode(&ct).unwrap();
279 let n = raw.len();
280 raw[n - 40] ^= 0xff; let bad = B64.encode(&raw);
282 assert_eq!(decrypt(&ck, &bad), Err(Nip44Error::Mac));
283 }
284
285 #[test]
286 fn wrong_key_fails_mac() {
287 let (sk_a, _pa) = generate_transport_key();
288 let (_sb, pub_b) = generate_transport_key();
289 let (sk_c, _pc) = generate_transport_key();
290 let (_sd, pub_d) = generate_transport_key();
291 let ck = conversation_key(&sk_a, &pub_b).unwrap();
292 let other = conversation_key(&sk_c, &pub_d).unwrap();
293 let ct = encrypt(&ck, "secret").unwrap();
294 assert_eq!(decrypt(&other, &ct), Err(Nip44Error::Mac));
295 }
296
297 #[test]
298 fn rejects_bad_version_and_short_payload() {
299 let ck = [9u8; 32];
300 assert_eq!(
301 decrypt(&ck, &B64.encode([0x01u8; 200])),
302 Err(Nip44Error::BadPayload)
303 );
304 assert_eq!(decrypt(&ck, "!!notbase64"), Err(Nip44Error::BadPayload));
305 assert_eq!(
306 decrypt(&ck, &B64.encode([0x02u8; 10])),
307 Err(Nip44Error::BadPayload)
308 );
309 }
310
311 #[test]
312 fn empty_and_oversize_plaintext_rejected() {
313 let ck = [3u8; 32];
314 assert_eq!(encrypt(&ck, ""), Err(Nip44Error::PlaintextLen));
315 let huge = "A".repeat(MAX_PLAINTEXT + 1);
316 assert_eq!(encrypt(&ck, &huge), Err(Nip44Error::PlaintextLen));
317 }
318
319 #[test]
320 fn padded_len_matches_spec_examples() {
321 for (unpadded, expected) in [
323 (1, 32),
324 (16, 32),
325 (32, 32),
326 (33, 64),
327 (37, 64),
328 (65, 96),
329 (100, 128),
330 ] {
331 assert_eq!(calc_padded_len(unpadded), expected, "len {unpadded}");
332 }
333 let mut prev = 0;
335 for n in 1..2000usize {
336 let p = calc_padded_len(n);
337 assert!(p >= n, "padded {p} < unpadded {n}");
338 assert_eq!(p % 32, 0, "padded {p} not a multiple of 32");
339 assert!(p >= prev, "padded len must be monotonic");
340 prev = p;
341 }
342 }
343
344 const OFFICIAL_VECTORS: &str = include_str!("testdata/nip44_official_vectors.json");
353
354 fn hex32(s: &str) -> [u8; 32] {
355 let v = hex::decode(s).expect("vector hex");
356 v.as_slice().try_into().expect("vector is 32 bytes")
357 }
358
359 #[test]
360 fn official_get_conversation_key_vectors() {
361 let v: serde_json::Value = serde_json::from_str(OFFICIAL_VECTORS).unwrap();
362 let cases = v["get_conversation_key"].as_array().unwrap();
363 assert!(cases.len() >= 30, "expected the full vector set");
364 for (i, c) in cases.iter().enumerate() {
365 let sec1 = hex32(c["sec1"].as_str().unwrap());
366 let pub2 = hex32(c["pub2"].as_str().unwrap());
367 let expected = hex32(c["conversation_key"].as_str().unwrap());
368 assert_eq!(
369 conversation_key(&sec1, &pub2).unwrap(),
370 expected,
371 "get_conversation_key vector #{i}"
372 );
373 }
374 }
375
376 #[test]
377 fn official_encrypt_decrypt_vectors() {
378 let v: serde_json::Value = serde_json::from_str(OFFICIAL_VECTORS).unwrap();
379 let cases = v["encrypt_decrypt"].as_array().unwrap();
380 assert!(!cases.is_empty());
381 for (i, c) in cases.iter().enumerate() {
382 let sec1 = hex32(c["sec1"].as_str().unwrap());
383 let sec2 = hex32(c["sec2"].as_str().unwrap());
384 let ck = hex32(c["conversation_key"].as_str().unwrap());
385 let nonce = hex32(c["nonce"].as_str().unwrap());
386 let plaintext = c["plaintext"].as_str().unwrap();
387 let payload = c["payload"].as_str().unwrap();
388
389 let pub2 = crate::nostr_key::xonly_from_secret(&sec2).unwrap();
391 assert_eq!(
392 conversation_key(&sec1, &pub2).unwrap(),
393 ck,
394 "ck derivation #{i}"
395 );
396 assert_eq!(
398 encrypt_with_nonce(&ck, &nonce, plaintext).unwrap(),
399 payload,
400 "encrypt vector #{i}"
401 );
402 assert_eq!(
404 decrypt(&ck, payload).unwrap(),
405 plaintext,
406 "decrypt vector #{i}"
407 );
408 }
409 }
410
411 #[test]
412 fn official_calc_padded_len_vectors() {
413 let v: serde_json::Value = serde_json::from_str(OFFICIAL_VECTORS).unwrap();
414 for pair in v["calc_padded_len"].as_array().unwrap() {
415 let unpadded = pair[0].as_u64().unwrap() as usize;
416 let expected = pair[1].as_u64().unwrap() as usize;
417 assert_eq!(
418 calc_padded_len(unpadded),
419 expected,
420 "calc_padded_len({unpadded})"
421 );
422 }
423 }
424}