1use base64::engine::{Engine as _, general_purpose};
2use chacha20::cipher::{KeyIvInit, StreamCipher};
3use hmac::{KeyInit, Mac};
4use zeroize::Zeroize;
5
6#[derive(Debug)]
7pub enum Nip44Error {
8 SharedSecretError,
9 FromHexError(nostro2_traits::hex::HexError),
10 NostrNoteError(nostro2::errors::NostrErrors),
11 InvalidLength,
12 Base64DecodingError(base64::DecodeError),
13 FromUtf8Error(std::str::Utf8Error),
14 HkdfError,
15 HmacError,
16 SliceError(chacha20::cipher::InvalidLength),
17 InvalidPrefixLen,
18 FromArrayError(std::array::TryFromSliceError),
19 BufferTooSmall,
20 FromIntError(std::num::TryFromIntError),
21 UnknownVersion(u8),
23 MacMismatch,
25}
26
27impl std::fmt::Display for Nip44Error {
28 #[allow(unknown_lints, crappy)]
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 Self::SharedSecretError => f.write_str("shared secret error"),
32 Self::FromHexError(e) => write!(f, "hex decoding error: {e}"),
33 Self::NostrNoteError(e) => write!(f, "{e}"),
34 Self::InvalidLength => f.write_str("invalid input length"),
35 Self::Base64DecodingError(e) => write!(f, "base64 decoding error: {e}"),
36 Self::FromUtf8Error(e) => write!(f, "UTF-8 conversion error: {e}"),
37 Self::HkdfError => f.write_str("HKDF key derivation failed"),
38 Self::HmacError => f.write_str("HMAC failure"),
39 Self::SliceError(e) => write!(f, "ChaCha20 slice error: {e}"),
40 Self::InvalidPrefixLen => f.write_str("invalid length prefix"),
41 Self::FromArrayError(e) => write!(f, "decryption error: {e}"),
42 Self::BufferTooSmall => f.write_str("buffer too small"),
43 Self::FromIntError(e) => write!(f, "encryption error: {e}"),
44 Self::UnknownVersion(v) => write!(f, "unsupported NIP-44 version byte: {v:#04x}"),
45 Self::MacMismatch => f.write_str("MAC mismatch: payload not authentic"),
46 }
47 }
48}
49
50impl std::error::Error for Nip44Error {
51 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
52 match self {
53 Self::FromHexError(e) => Some(e),
54 Self::NostrNoteError(e) => Some(e),
55 Self::Base64DecodingError(e) => Some(e),
56 Self::FromUtf8Error(e) => Some(e),
57 Self::FromArrayError(e) => Some(e),
58 Self::FromIntError(e) => Some(e),
59 _ => None,
60 }
61 }
62}
63
64impl From<nostro2_traits::hex::HexError> for Nip44Error {
65 fn from(e: nostro2_traits::hex::HexError) -> Self {
66 Self::FromHexError(e)
67 }
68}
69impl From<nostro2::errors::NostrErrors> for Nip44Error {
70 fn from(e: nostro2::errors::NostrErrors) -> Self {
71 Self::NostrNoteError(e)
72 }
73}
74impl From<base64::DecodeError> for Nip44Error {
75 fn from(e: base64::DecodeError) -> Self {
76 Self::Base64DecodingError(e)
77 }
78}
79impl From<std::str::Utf8Error> for Nip44Error {
80 fn from(e: std::str::Utf8Error) -> Self {
81 Self::FromUtf8Error(e)
82 }
83}
84impl From<chacha20::cipher::InvalidLength> for Nip44Error {
85 fn from(e: chacha20::cipher::InvalidLength) -> Self {
86 Self::SliceError(e)
87 }
88}
89impl From<std::array::TryFromSliceError> for Nip44Error {
90 fn from(e: std::array::TryFromSliceError) -> Self {
91 Self::FromArrayError(e)
92 }
93}
94impl From<std::num::TryFromIntError> for Nip44Error {
95 fn from(e: std::num::TryFromIntError) -> Self {
96 Self::FromIntError(e)
97 }
98}
99
100pub const VERSION_V2: u8 = 0x02;
104
105pub const VERSION_LEGACY: u8 = 0x31;
111
112pub struct MessageKeys {
115 chacha_key: zeroize::Zeroizing<[u8; 32]>,
116 chacha_nonce: zeroize::Zeroizing<[u8; 12]>,
117 hmac_key: zeroize::Zeroizing<[u8; 32]>,
118}
119
120pub trait Nip44: nostro2::NostrKeypair {
121 fn shared_secret(&self, peer_pubkey: &str) -> Result<zeroize::Zeroizing<[u8; 32]>, Nip44Error> {
126 Ok(nostro2::NostrKeypair::shared_point(self, peer_pubkey)
127 .map_err(|_| Nip44Error::SharedSecretError)?
128 .into())
129 }
130
131 fn nip44_encrypt_note<'a>(
136 &self,
137 note: &'a mut nostro2::NostrNote,
138 peer_pubkey: &'a str,
139 ) -> Result<(), Nip44Error> {
140 note.content = self.nip_44_encrypt(¬e.content, peer_pubkey)?.to_string();
141 Ok(())
142 }
143
144 fn nip44_decrypt_note<'a>(
149 &self,
150 note: &'a nostro2::NostrNote,
151 peer_pubkey: &'a str,
152 ) -> Result<std::borrow::Cow<'a, str>, Nip44Error> {
153 self.nip_44_decrypt(¬e.content, peer_pubkey)
154 }
155
156 fn nip_44_encrypt<'a>(
166 &self,
167 plaintext: &'a str,
168 peer_pubkey: &'a str,
169 ) -> Result<std::borrow::Cow<'a, str>, Nip44Error> {
170 let shared = self.shared_secret(peer_pubkey)?;
171 let conversation_key = Self::conversation_key_v2(shared)?;
172 let nonce = Self::generate_nonce_32();
173 let payload = Self::encrypt_v2(&conversation_key, &nonce, plaintext.as_bytes())?;
174 Ok(payload.into())
175 }
176
177 fn nip_44_decrypt<'a>(
186 &self,
187 payload: &'a str,
188 peer_pubkey: &'a str,
189 ) -> Result<std::borrow::Cow<'a, str>, Nip44Error> {
190 if payload.as_bytes().first() == Some(&b'#') {
193 return Err(Nip44Error::UnknownVersion(b'#'));
194 }
195 let mut decoded = zeroize::Zeroizing::new(general_purpose::STANDARD.decode(payload)?);
196 let version = *decoded.first().ok_or(Nip44Error::InvalidLength)?;
197 let shared = self.shared_secret(peer_pubkey)?;
198
199 let plaintext = match version {
200 VERSION_V2 => {
201 let conversation_key = Self::conversation_key_v2(shared)?;
202 Self::decrypt_v2(&conversation_key, &decoded)?
203 }
204 VERSION_LEGACY => {
205 let conversation_key = Self::derive_conversation_key_legacy(shared, b"nip44-v2")?;
206 Self::decrypt_legacy(&conversation_key, &decoded)?
207 }
208 other => {
209 decoded.zeroize();
210 return Err(Nip44Error::UnknownVersion(other));
211 }
212 };
213 decoded.zeroize();
214 Ok(plaintext.into())
215 }
216
217 fn conversation_key_v2(
226 mut shared_x: zeroize::Zeroizing<[u8; 32]>,
227 ) -> Result<zeroize::Zeroizing<[u8; 32]>, Nip44Error> {
228 let (prk, _hk) =
229 hkdf::Hkdf::<sha2::Sha256>::extract(Some(b"nip44-v2"), shared_x.as_slice());
230 shared_x.zeroize();
231 let mut key = zeroize::Zeroizing::new([0_u8; 32]);
232 key.copy_from_slice(&prk);
233 Ok(key)
234 }
235
236 fn get_message_keys(
242 conversation_key: &[u8; 32],
243 nonce: &[u8; 32],
244 ) -> Result<MessageKeys, Nip44Error> {
245 let hkdf = hkdf::Hkdf::<sha2::Sha256>::from_prk(conversation_key)
246 .map_err(|_| Nip44Error::HkdfError)?;
247 let mut okm = zeroize::Zeroizing::new([0_u8; 76]);
248 hkdf.expand(nonce, okm.as_mut_slice())
249 .map_err(|_| Nip44Error::HkdfError)?;
250 let mut chacha_key = zeroize::Zeroizing::new([0_u8; 32]);
251 chacha_key.copy_from_slice(&okm[0..32]);
252 let mut chacha_nonce = zeroize::Zeroizing::new([0_u8; 12]);
253 chacha_nonce.copy_from_slice(&okm[32..44]);
254 let mut hmac_key = zeroize::Zeroizing::new([0_u8; 32]);
255 hmac_key.copy_from_slice(&okm[44..76]);
256 Ok(MessageKeys {
257 chacha_key,
258 chacha_nonce,
259 hmac_key,
260 })
261 }
262
263 fn encrypt_v2(
271 conversation_key: &[u8; 32],
272 nonce: &[u8; 32],
273 plaintext: &[u8],
274 ) -> Result<String, Nip44Error> {
275 let keys = Self::get_message_keys(conversation_key, nonce)?;
276 let mut padded = Self::pad_v2(plaintext)?;
277
278 let mut cipher = chacha20::ChaCha20::new_from_slices(
279 keys.chacha_key.as_slice(),
280 keys.chacha_nonce.as_slice(),
281 )?;
282 cipher.apply_keystream(padded.as_mut_slice());
283 let mac = Self::hmac_with_aad(keys.hmac_key.as_slice(), padded.as_slice(), nonce)?;
286 let payload = Self::base64_encode_params(&[VERSION_V2], nonce, padded.as_slice(), &mac);
287 padded.zeroize();
288 Ok(payload)
289 }
290
291 fn decrypt_v2(conversation_key: &[u8; 32], decoded: &[u8]) -> Result<String, Nip44Error> {
300 let dlen = decoded.len();
301 if !(99..=65603).contains(&dlen) {
303 return Err(Nip44Error::InvalidLength);
304 }
305 let nonce: [u8; 32] = decoded[1..33].try_into()?;
306 let ciphertext = &decoded[33..dlen - 32];
307 let mac = &decoded[dlen - 32..];
308
309 let keys = Self::get_message_keys(conversation_key, &nonce)?;
310 Self::verify_hmac_with_aad(keys.hmac_key.as_slice(), ciphertext, &nonce, mac)?;
311
312 let mut buffer = zeroize::Zeroizing::new(ciphertext.to_vec());
313 let mut cipher = chacha20::ChaCha20::new_from_slices(
314 keys.chacha_key.as_slice(),
315 keys.chacha_nonce.as_slice(),
316 )?;
317 cipher.apply_keystream(buffer.as_mut_slice());
318
319 let plaintext = Self::unpad_v2(&buffer)?.to_string();
320 buffer.zeroize();
321 Ok(plaintext)
322 }
323
324 fn decrypt_v2_bytes(
333 conversation_key: &[u8; 32],
334 decoded: &[u8],
335 ) -> Result<Vec<u8>, Nip44Error> {
336 let dlen = decoded.len();
337 if !(99..=65603).contains(&dlen) {
338 return Err(Nip44Error::InvalidLength);
339 }
340 let nonce: [u8; 32] = decoded[1..33].try_into()?;
341 let ciphertext = &decoded[33..dlen - 32];
342 let mac = &decoded[dlen - 32..];
343
344 let keys = Self::get_message_keys(conversation_key, &nonce)?;
345 Self::verify_hmac_with_aad(keys.hmac_key.as_slice(), ciphertext, &nonce, mac)?;
346
347 let mut buffer = zeroize::Zeroizing::new(ciphertext.to_vec());
348 let mut cipher = chacha20::ChaCha20::new_from_slices(
349 keys.chacha_key.as_slice(),
350 keys.chacha_nonce.as_slice(),
351 )?;
352 cipher.apply_keystream(buffer.as_mut_slice());
353
354 if buffer.len() < 2 {
357 return Err(Nip44Error::InvalidLength);
358 }
359 let unpadded_len = u16::from_be_bytes([buffer[0], buffer[1]]) as usize;
360 if unpadded_len == 0
361 || 2 + unpadded_len > buffer.len()
362 || buffer.len() != 2 + Self::calc_padded_len(unpadded_len)
363 {
364 return Err(Nip44Error::InvalidPrefixLen);
365 }
366 let out = buffer[2..2 + unpadded_len].to_vec();
367 buffer.zeroize();
368 Ok(out)
369 }
370
371 #[must_use]
374 fn calc_padded_len(unpadded_len: usize) -> usize {
375 if unpadded_len <= 32 {
376 return 32;
377 }
378 let next_power = 1_usize << ((unpadded_len - 1).ilog2() + 1);
379 let chunk = if next_power <= 256 {
380 32
381 } else {
382 next_power / 8
383 };
384 chunk * ((unpadded_len - 1) / chunk + 1)
385 }
386
387 fn pad_v2(plaintext: &[u8]) -> Result<zeroize::Zeroizing<Vec<u8>>, Nip44Error> {
392 let len = plaintext.len();
393 if !(1..=65535).contains(&len) {
394 return Err(Nip44Error::InvalidLength);
395 }
396 let total = 2 + Self::calc_padded_len(len);
397 let mut buf = zeroize::Zeroizing::new(vec![0_u8; total]);
398 buf[..2].copy_from_slice(&u16::try_from(len)?.to_be_bytes());
399 buf[2..2 + len].copy_from_slice(plaintext);
400 Ok(buf)
401 }
402
403 fn unpad_v2(padded: &[u8]) -> Result<&str, Nip44Error> {
411 if padded.len() < 2 {
412 return Err(Nip44Error::InvalidLength);
413 }
414 let unpadded_len = u16::from_be_bytes([padded[0], padded[1]]) as usize;
415 if unpadded_len == 0
416 || 2 + unpadded_len > padded.len()
417 || padded.len() != 2 + Self::calc_padded_len(unpadded_len)
418 {
419 return Err(Nip44Error::InvalidPrefixLen);
420 }
421 Ok(std::str::from_utf8(&padded[2..2 + unpadded_len])?)
422 }
423
424 fn hmac_with_aad(key: &[u8], message: &[u8], aad: &[u8; 32]) -> Result<[u8; 32], Nip44Error> {
430 let mut mac =
431 hmac::Hmac::<sha2::Sha256>::new_from_slice(key).map_err(|_| Nip44Error::HmacError)?;
432 mac.update(aad);
433 mac.update(message);
434 Ok(mac.finalize().into_bytes().into())
435 }
436
437 fn verify_hmac_with_aad(
442 key: &[u8],
443 message: &[u8],
444 aad: &[u8; 32],
445 expected: &[u8],
446 ) -> Result<(), Nip44Error> {
447 let mut mac =
448 hmac::Hmac::<sha2::Sha256>::new_from_slice(key).map_err(|_| Nip44Error::HmacError)?;
449 mac.update(aad);
450 mac.update(message);
451 mac.verify_slice(expected)
452 .map_err(|_| Nip44Error::MacMismatch)
453 }
454
455 #[must_use]
456 fn generate_nonce_32() -> zeroize::Zeroizing<[u8; 32]> {
457 let mut nonce = [0_u8; 32];
458 getrandom::fill(&mut nonce).expect("getrandom failed");
459 nonce.into()
460 }
461
462 fn derive_conversation_key_legacy(
471 mut shared_secret: zeroize::Zeroizing<[u8; 32]>,
472 salt: &[u8],
473 ) -> Result<zeroize::Zeroizing<[u8; 32]>, Nip44Error> {
474 let hkdf = hkdf::Hkdf::<sha2::Sha256>::new(Some(salt), shared_secret.as_slice());
475 shared_secret.zeroize();
476 let mut okm = [0_u8; 32];
477 hkdf.expand(&[], &mut okm)
478 .map_err(|_| Nip44Error::HkdfError)?;
479 Ok(okm.into())
480 }
481
482 fn decrypt_legacy(conversation_key: &[u8; 32], decoded: &[u8]) -> Result<String, Nip44Error> {
493 if decoded.len() < 1 + 12 + 32 {
494 return Err(Nip44Error::InvalidLength);
495 }
496 let nonce: [u8; 12] = decoded[1..13].try_into()?;
497 let ciphertext = &decoded[13..decoded.len() - 32];
498 let tag = &decoded[decoded.len() - 32..];
499
500 let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(conversation_key)
501 .map_err(|_| Nip44Error::HmacError)?;
502 mac.update(ciphertext);
503 mac.verify_slice(tag).map_err(|_| Nip44Error::MacMismatch)?;
504
505 let mut buffer = zeroize::Zeroizing::new(ciphertext.to_vec());
506 let mut cipher = chacha20::ChaCha20::new_from_slices(conversation_key, &nonce)?;
507 cipher.apply_keystream(buffer.as_mut_slice());
508
509 if buffer.len() < 2 {
510 return Err(Nip44Error::InvalidLength);
511 }
512 let len = u16::from_be_bytes([buffer[0], buffer[1]]) as usize;
513 if len > buffer.len() - 2 {
514 return Err(Nip44Error::InvalidPrefixLen);
515 }
516 Ok(std::str::from_utf8(&buffer[2..2 + len])?.to_string())
517 }
518
519 #[must_use]
520 fn base64_encode_params(version: &[u8], nonce: &[u8], ciphertext: &[u8], mac: &[u8]) -> String {
521 let mut buf =
522 Vec::with_capacity(version.len() + nonce.len() + ciphertext.len() + mac.len());
523 buf.extend_from_slice(version);
524 buf.extend_from_slice(nonce);
525 buf.extend_from_slice(ciphertext);
526 buf.extend_from_slice(mac);
527
528 let mut out = String::with_capacity((buf.len() * 4).div_ceil(3));
529 general_purpose::STANDARD.encode_string(&buf, &mut out);
530 out
531 }
532}
533
534impl<T: nostro2::NostrKeypair + ?Sized> Nip44 for T {}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539 use nostro2::{NostrKeypair, NostrSigner};
540
541 type Tester = crate::tests::NipTester;
542
543 #[test]
544 fn test_encrypt_decrypt_success() {
545 let sender = Tester::generate();
546 let receiver = Tester::generate();
547
548 let plaintext = "Hello NIP-44 encryption!";
549 let receiver_pk = receiver.public_key();
550 let sender_pk = sender.public_key();
551 let ciphertext = sender.nip_44_encrypt(plaintext, &receiver_pk).unwrap();
552 let raw = general_purpose::STANDARD
554 .decode(ciphertext.as_ref())
555 .unwrap();
556 assert_eq!(raw[0], VERSION_V2, "expected v2 (0x02) payload");
557 let decrypted = receiver.nip_44_decrypt(&ciphertext, &sender_pk).unwrap();
558 assert_eq!(decrypted, plaintext);
559 }
560
561 #[test]
562 fn test_invalid_decryption_key() {
563 let sender = Tester::generate();
564 let receiver = Tester::generate();
565 let wrong_receiver = Tester::generate();
566
567 let plaintext = "Hello NIP-44 encryption!";
568 let receiver_pk = receiver.public_key();
569 let sender_pk = sender.public_key();
570 let ciphertext = sender.nip_44_encrypt(plaintext, &receiver_pk).unwrap();
571 let result = wrong_receiver.nip_44_decrypt(&ciphertext, &sender_pk);
573 assert!(result.is_err());
574 }
575
576 #[test]
580 fn vector_conversation_key() {
581 let sec1 = "0000000000000000000000000000000000000000000000000000000000000001";
582 let sec2 = "0000000000000000000000000000000000000000000000000000000000000002";
583 let a = Tester::from_hex(sec1).unwrap();
584 let b = Tester::from_hex(sec2).unwrap();
585
586 let shared = a.shared_secret(&b.public_key()).unwrap();
587 let conv = Tester::conversation_key_v2(shared).unwrap();
588 let hex = nostro2_traits::hex::Hexable::to_hex(conv.as_slice());
589 assert_eq!(
590 hex,
591 "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d"
592 );
593 }
594
595 #[test]
597 fn vector_known_answer_payload() {
598 use nostro2_traits::hex::FromHex as _;
599 let conv: [u8; 32] = "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d"
600 .decode_hex()
601 .unwrap()
602 .try_into()
603 .unwrap();
604 let nonce: [u8; 32] = "0000000000000000000000000000000000000000000000000000000000000001"
605 .decode_hex()
606 .unwrap()
607 .try_into()
608 .unwrap();
609 let payload = Tester::encrypt_v2(&conv, &nonce, b"a").unwrap();
610 assert_eq!(
611 payload,
612 "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"
613 );
614 }
615
616 #[test]
618 fn vector_calc_padded_len() {
619 let cases = [
620 (1_usize, 32_usize),
621 (32, 32),
622 (33, 64),
623 (37, 64),
624 (45, 64),
625 (49, 64),
626 (64, 64),
627 (65, 96),
628 (100, 128),
629 (111, 128),
630 (200, 224),
631 (250, 256),
632 (320, 320),
633 (361, 384),
634 (512, 512),
635 (1000, 1024),
636 (1024, 1024),
637 (1025, 1280),
638 (65535, 65536),
639 ];
640 for (unpadded, expected) in cases {
641 assert_eq!(
642 Tester::calc_padded_len(unpadded),
643 expected,
644 "calc_padded_len({unpadded})"
645 );
646 }
647 }
648
649 #[test]
651 fn v2_mac_rejects_tampering() {
652 let sender = Tester::generate();
653 let receiver = Tester::generate();
654 let receiver_pk = receiver.public_key();
655 let sender_pk = sender.public_key();
656 let ct = sender.nip_44_encrypt("authentic", &receiver_pk).unwrap();
657 let mut bytes = general_purpose::STANDARD.decode(ct.as_ref()).unwrap();
659 let mid = bytes.len() / 2;
660 bytes[mid] ^= 0x01;
661 let tampered = general_purpose::STANDARD.encode(&bytes);
662 let result = receiver.nip_44_decrypt(&tampered, &sender_pk);
663 assert!(matches!(result, Err(Nip44Error::MacMismatch)));
664 }
665
666 #[test]
668 fn legacy_payload_still_decrypts() {
669 let sender = Tester::generate();
672 let receiver = Tester::generate();
673 let receiver_pk = receiver.public_key();
674 let sender_pk = sender.public_key();
675 let shared = sender.shared_secret(&receiver_pk).unwrap();
676 let conv = Tester::derive_conversation_key_legacy(shared, b"nip44-v2").unwrap();
677
678 let plaintext = b"legacy data at rest";
679 let total = (plaintext.len() + 2).next_power_of_two().max(32);
681 let mut padded = vec![0_u8; total];
682 padded[..2].copy_from_slice(&u16::try_from(plaintext.len()).unwrap().to_be_bytes());
683 padded[2..2 + plaintext.len()].copy_from_slice(plaintext);
684
685 let mut nonce = [0_u8; 12];
686 getrandom::fill(&mut nonce).unwrap();
687 let mut cipher = chacha20::ChaCha20::new_from_slices(conv.as_slice(), &nonce).unwrap();
688 cipher.apply_keystream(&mut padded);
689 let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(conv.as_slice()).unwrap();
690 mac.update(&padded);
691 let tag: [u8; 32] = mac.finalize().into_bytes().into();
692 let legacy_payload = Tester::base64_encode_params(b"1", &nonce, &padded, &tag);
693
694 let out = receiver
696 .nip_44_decrypt(&legacy_payload, &sender_pk)
697 .unwrap();
698 assert_eq!(out, "legacy data at rest");
699 }
700
701 #[test]
705 fn legacy_payload_tampering_is_rejected() {
706 let sender = Tester::generate();
707 let receiver = Tester::generate();
708 let receiver_pk = receiver.public_key();
709 let sender_pk = sender.public_key();
710 let shared = sender.shared_secret(&receiver_pk).unwrap();
711 let conv = Tester::derive_conversation_key_legacy(shared, b"nip44-v2").unwrap();
712
713 let plaintext = b"legacy data at rest";
714 let total = (plaintext.len() + 2).next_power_of_two().max(32);
715 let mut padded = vec![0_u8; total];
716 padded[..2].copy_from_slice(&u16::try_from(plaintext.len()).unwrap().to_be_bytes());
717 padded[2..2 + plaintext.len()].copy_from_slice(plaintext);
718
719 let mut nonce = [0_u8; 12];
720 getrandom::fill(&mut nonce).unwrap();
721 let mut cipher = chacha20::ChaCha20::new_from_slices(conv.as_slice(), &nonce).unwrap();
722 cipher.apply_keystream(&mut padded);
723 let mut mac = hmac::Hmac::<sha2::Sha256>::new_from_slice(conv.as_slice()).unwrap();
724 mac.update(&padded);
725 let tag: [u8; 32] = mac.finalize().into_bytes().into();
726 let legacy_payload = Tester::base64_encode_params(b"1", &nonce, &padded, &tag);
727
728 let mut bytes = general_purpose::STANDARD.decode(&legacy_payload).unwrap();
729 let ct_start = 1 + 12;
730 bytes[ct_start] ^= 0x01;
731 let tampered = general_purpose::STANDARD.encode(&bytes);
732
733 let result = receiver.nip_44_decrypt(&tampered, &sender_pk);
734 assert!(matches!(result, Err(Nip44Error::MacMismatch)));
735 }
736
737 #[test]
738 fn unknown_version_is_reported() {
739 let sender = Tester::generate();
740 let receiver = Tester::generate();
741 let mut raw = vec![0x09_u8];
743 raw.extend_from_slice(&[0_u8; 80]);
744 let payload = general_purpose::STANDARD.encode(&raw);
745 let sender_pk = sender.public_key();
746 let result = receiver.nip_44_decrypt(&payload, &sender_pk);
747 assert!(matches!(result, Err(Nip44Error::UnknownVersion(0x09))));
748 }
749
750 #[test]
751 fn hash_flag_is_unsupported() {
752 let sender = Tester::generate();
753 let receiver = Tester::generate();
754 let sender_pk = sender.public_key();
755 let result = receiver.nip_44_decrypt("#unsupported", &sender_pk);
756 assert!(matches!(result, Err(Nip44Error::UnknownVersion(b'#'))));
757 }
758
759 use std::fmt::Write as _;
760 #[test]
761 fn encrypt_very_large_note() {
762 let sender = Tester::generate();
763 let receiver = Tester::generate();
764
765 let mut plaintext = String::new();
766 for i in 0..15329 {
767 let _ = write!(plaintext, "{i}");
768 }
769 let receiver_pk = receiver.public_key();
770 let sender_pk = sender.public_key();
771 let ciphertext = sender.nip_44_encrypt(&plaintext, &receiver_pk).unwrap();
772 let decrypted = receiver.nip_44_decrypt(&ciphertext, &sender_pk).unwrap();
773 assert_eq!(decrypted, plaintext);
774 }
775
776 fn utf8_err() -> std::str::Utf8Error {
777 let bad = [0xff_u8];
778 std::str::from_utf8(bad.as_slice()).unwrap_err()
779 }
780
781 fn slice_err() -> std::array::TryFromSliceError {
782 <[u8; 4]>::try_from([0_u8; 3].as_slice()).unwrap_err()
783 }
784
785 fn int_err() -> std::num::TryFromIntError {
786 u8::try_from(256_u16).unwrap_err()
787 }
788
789 #[test]
790 fn error_display_covers_all_variants() {
791 let cases: Vec<Nip44Error> = vec![
792 Nip44Error::SharedSecretError,
793 Nip44Error::FromHexError(nostro2_traits::hex::HexError::OddLength),
794 Nip44Error::NostrNoteError(nostro2::errors::NostrErrors::MissingId),
795 Nip44Error::InvalidLength,
796 Nip44Error::Base64DecodingError(
797 base64::engine::general_purpose::STANDARD
798 .decode("!!!")
799 .unwrap_err(),
800 ),
801 Nip44Error::FromUtf8Error(utf8_err()),
802 Nip44Error::HkdfError,
803 Nip44Error::HmacError,
804 Nip44Error::InvalidPrefixLen,
805 Nip44Error::FromArrayError(slice_err()),
806 Nip44Error::BufferTooSmall,
807 Nip44Error::FromIntError(int_err()),
808 Nip44Error::UnknownVersion(0x09),
809 Nip44Error::MacMismatch,
810 ];
811 for err in &cases {
812 let msg = format!("{err}");
813 assert!(!msg.is_empty(), "Display empty for {err:?}");
814 }
815 }
816
817 #[test]
818 fn error_source_delegates_correctly() {
819 use std::error::Error;
820
821 assert!(Nip44Error::SharedSecretError.source().is_none());
822 assert!(Nip44Error::InvalidLength.source().is_none());
823 assert!(Nip44Error::HkdfError.source().is_none());
824 assert!(Nip44Error::HmacError.source().is_none());
825 assert!(Nip44Error::InvalidPrefixLen.source().is_none());
826 assert!(Nip44Error::BufferTooSmall.source().is_none());
827 assert!(Nip44Error::UnknownVersion(0x09).source().is_none());
828 assert!(Nip44Error::MacMismatch.source().is_none());
829
830 assert!(
831 Nip44Error::FromHexError(nostro2_traits::hex::HexError::OddLength)
832 .source()
833 .is_some()
834 );
835 assert!(
836 Nip44Error::NostrNoteError(nostro2::errors::NostrErrors::MissingId)
837 .source()
838 .is_some()
839 );
840 assert!(
841 Nip44Error::Base64DecodingError(
842 base64::engine::general_purpose::STANDARD
843 .decode("!!!")
844 .unwrap_err()
845 )
846 .source()
847 .is_some()
848 );
849 assert!(Nip44Error::FromUtf8Error(utf8_err()).source().is_some());
850 assert!(Nip44Error::FromArrayError(slice_err()).source().is_some());
851 assert!(Nip44Error::FromIntError(int_err()).source().is_some());
852 }
853
854 mod proptests {
855 use super::*;
856 use proptest::prelude::*;
857
858 proptest! {
859 #[test]
860 fn encrypt_decrypt_round_trip(plaintext in ".{1,256}") {
861 let sender = Tester::generate();
862 let receiver = Tester::generate();
863 let receiver_pk = receiver.public_key();
864 let sender_pk = sender.public_key();
865
866 let ciphertext = sender.nip_44_encrypt(&plaintext, &receiver_pk).unwrap();
867 let decrypted = receiver.nip_44_decrypt(&ciphertext, &sender_pk).unwrap();
868 prop_assert_eq!(&plaintext, decrypted.as_ref());
869 }
870
871 #[test]
872 fn encrypt_is_non_deterministic(plaintext in ".{1,64}") {
873 let sender = Tester::generate();
874 let receiver = Tester::generate();
875 let receiver_pk = receiver.public_key();
876
877 let a = sender.nip_44_encrypt(&plaintext, &receiver_pk).unwrap();
878 let b = sender.nip_44_encrypt(&plaintext, &receiver_pk).unwrap();
879 prop_assert_ne!(a, b, "same plaintext must produce different ciphertexts");
880 }
881
882 #[test]
883 fn padded_len_is_valid(unpadded in 1_usize..65535) {
884 let padded = Tester::calc_padded_len(unpadded);
885 prop_assert!(padded >= 32);
886 prop_assert!(padded >= unpadded);
887 prop_assert_eq!(padded % 32, 0);
889 }
890 }
891 }
892}