1use base64::engine::{general_purpose, Engine as _};
2use chacha20::cipher::{KeyIvInit, StreamCipher};
3use hmac::Mac;
4use rand_core::RngCore;
5use zeroize::Zeroize;
6
7#[derive(Debug, thiserror::Error)]
8pub enum Nip44Error {
9 #[error("Shared secret error")]
10 SharedSecretError,
11 #[error("Hex decoding error {0}")]
12 FromHexError(#[from] hex::FromHexError),
13 #[error("Nostr note error {0}")]
14 NostrNoteError(#[from] nostro2::errors::NostrErrors),
15 #[error("Invalid input length")]
16 InvalidLength,
17 #[error("Base64 decoding error {0}")]
18 Base64DecodingError(#[from] base64::DecodeError),
19 #[error("UTF-8 conversion error {0}")]
20 FromUtf8Error(#[from] std::str::Utf8Error),
21 #[error("HKDF key derivation failed")]
22 HkdfError,
23 #[error("HMAC failure")]
24 HmacError,
25 #[error("ChaCha20 slice error")]
26 SliceError(#[from] chacha20::cipher::InvalidLength),
27 #[error("Invalid length prefix")]
28 InvalidPrefixLen,
29 #[error("Decryption error {0}")]
30 FromArrayError(#[from] std::array::TryFromSliceError),
31 #[error("Buffer too small")]
32 BufferTooSmall,
33 #[error("Encryption error {0}")]
34 FromIntError(#[from] std::num::TryFromIntError),
35}
36
37pub struct MacComponents<'a> {
38 nonce: zeroize::Zeroizing<[u8; 12]>,
39 ciphertext: &'a [u8],
40}
41
42pub trait Nip44 {
43 fn shared_secret(&self, peer_pubkey: &str) -> Result<zeroize::Zeroizing<[u8; 32]>, Nip44Error>;
48
49 fn nip44_encrypt_note<'a>(
61 &self,
62 note: &'a mut nostro2::NostrNote,
63 peer_pubkey: &'a str,
64 ) -> Result<(), Nip44Error> {
65 note.content = self.nip_44_encrypt(¬e.content, peer_pubkey)?.to_string();
66 Ok(())
67 }
68 fn nip44_decrypt_note<'a>(
80 &self,
81 note: &'a nostro2::NostrNote,
82 peer_pubkey: &'a str,
83 ) -> Result<std::borrow::Cow<'a, str>, Nip44Error> {
84 self.nip_44_decrypt(¬e.content, peer_pubkey)
85 }
86
87 fn nip_44_encrypt<'a>(
95 &self,
96 plaintext: &'a str,
97 peer_pubkey: &'a str,
98 ) -> Result<std::borrow::Cow<'a, str>, Nip44Error> {
99 let mut buffer =
100 zeroize::Zeroizing::new(vec![
101 0_u8;
102 (plaintext.len() + 2).next_power_of_two().max(32)
103 ]);
104 let shared_secret = self.shared_secret(peer_pubkey)?;
105 let mut conversation_key = Self::derive_conversation_key(shared_secret, b"nip44-v2")?;
106 let mut nonce = Self::generate_nonce();
107
108 let ciphertext = Self::encrypt(
109 plaintext.as_bytes(),
110 conversation_key.as_slice(),
111 nonce.as_slice(),
112 buffer.as_mut_slice(),
113 )?;
114
115 let mac = Self::calculate_mac(ciphertext, conversation_key.as_slice())?;
116 let encoded = Self::base64_encode_params(b"1", nonce.as_slice(), ciphertext, &mac);
117 conversation_key.zeroize();
118 nonce.zeroize();
119 Ok(encoded.into())
120 }
121
122 fn nip_44_decrypt<'a>(
132 &self,
133 ciphertext: &'a str,
134 peer_pubkey: &'a str,
135 ) -> Result<std::borrow::Cow<'a, str>, Nip44Error> {
136 let mut buffer = zeroize::Zeroizing::new(vec![0_u8; ciphertext.len()]);
137 let shared_secret = self.shared_secret(peer_pubkey)?;
138 let conversation_key = Self::derive_conversation_key(shared_secret, b"nip44-v2")?;
139 let mut decoded = zeroize::Zeroizing::new(general_purpose::STANDARD.decode(ciphertext)?);
140 let MacComponents { nonce, ciphertext } = Self::extract_components(&decoded)?;
141
142 let decrypted = Self::decrypt(ciphertext, conversation_key, nonce, buffer.as_mut_slice())?;
143
144 decoded.zeroize();
146
147 Ok(std::str::from_utf8(decrypted)?.to_string().into())
148 }
149 fn encrypt<'a>(
155 content: &[u8],
156 key: &[u8],
157 nonce: &[u8],
158 buffer: &'a mut [u8],
159 ) -> Result<&'a [u8], Nip44Error> {
160 let padded = Self::pad_string(content, buffer)?;
161 let mut cipher = chacha20::ChaCha20::new_from_slices(key, nonce)?;
162 cipher.apply_keystream(padded);
163 Ok(&padded[..])
164 }
165
166 fn decrypt<'a>(
172 ciphertext: &[u8],
173 mut key: zeroize::Zeroizing<[u8; 32]>,
174 mut nonce: zeroize::Zeroizing<[u8; 12]>,
175 buffer: &'a mut [u8],
176 ) -> Result<&'a [u8], Nip44Error> {
177 if key.len() != 32 || nonce.len() != 12 {
178 return Err(Nip44Error::InvalidLength);
179 }
180
181 if buffer.len() < ciphertext.len() {
182 return Err(Nip44Error::InvalidLength);
183 }
184
185 buffer[..ciphertext.len()].copy_from_slice(ciphertext);
186
187 let mut cipher = chacha20::ChaCha20::new_from_slices(key.as_slice(), nonce.as_slice())?;
188 cipher.apply_keystream(&mut buffer[..ciphertext.len()]);
189
190 if ciphertext.len() < 2 {
191 return Err(Nip44Error::InvalidLength);
192 }
193
194 let len = u16::from_be_bytes([buffer[0], buffer[1]]) as usize;
195
196 if len > ciphertext.len() - 2 {
197 return Err(Nip44Error::InvalidPrefixLen);
198 }
199
200 key.zeroize();
202 nonce.zeroize();
203
204 Ok(&buffer[2..2 + len])
205 }
206
207 fn derive_conversation_key(
212 mut shared_secret: zeroize::Zeroizing<[u8; 32]>,
213 salt: &[u8],
214 ) -> Result<zeroize::Zeroizing<[u8; 32]>, Nip44Error> {
215 let hkdf = hkdf::Hkdf::<sha2::Sha256>::new(Some(salt), shared_secret.as_slice());
216 shared_secret.zeroize();
217 let mut okm = [0_u8; 32];
218 hkdf.expand(&[], &mut okm)
219 .map_err(|_| Nip44Error::HkdfError)?;
220 Ok(okm.into())
221 }
222
223 fn extract_components(decoded: &[u8]) -> Result<MacComponents<'_>, Nip44Error> {
228 if decoded.len() < 1 + 12 + 32 {
229 return Err(Nip44Error::InvalidLength);
230 }
231 Ok(MacComponents {
232 nonce: zeroize::Zeroizing::new(decoded[1..13].try_into()?),
233 ciphertext: &decoded[13..decoded.len() - 32],
234 })
235 }
236 fn calculate_mac(data: &[u8], key: &[u8]) -> Result<[u8; 32], Nip44Error> {
241 let mut mac =
242 hmac::Hmac::<sha2::Sha256>::new_from_slice(key).map_err(|_| Nip44Error::HmacError)?;
243 mac.update(data);
244 let result = mac.finalize().into_bytes();
245 Ok(result.into())
246 }
247
248 fn pad_string<'a>(plaintext: &[u8], buffer: &'a mut [u8]) -> Result<&'a mut [u8], Nip44Error> {
253 if plaintext.is_empty() || plaintext.len() > 65535 {
254 return Err(Nip44Error::InvalidLength);
255 }
256
257 let total_len = (plaintext.len() + 2).next_power_of_two().max(32);
258
259 if buffer.len() < total_len {
260 return Err(Nip44Error::BufferTooSmall);
261 }
262
263 let len_bytes = u16::try_from(plaintext.len())?.to_be_bytes();
264 buffer[..2].copy_from_slice(&len_bytes);
265 buffer[2..2 + plaintext.len()].copy_from_slice(plaintext);
266
267 for b in &mut buffer[2 + plaintext.len()..total_len] {
269 *b = 0;
270 }
271
272 Ok(&mut buffer[..total_len])
273 }
274
275 #[must_use]
276 fn generate_nonce() -> zeroize::Zeroizing<[u8; 12]> {
277 let mut nonce = [0_u8; 12];
278 rand_core::OsRng.fill_bytes(&mut nonce);
279 nonce.into()
280 }
281 #[must_use]
282 fn base64_encode_params(version: &[u8], nonce: &[u8], ciphertext: &[u8], mac: &[u8]) -> String {
283 let mut buf =
284 Vec::with_capacity(version.len() + nonce.len() + ciphertext.len() + mac.len());
285 buf.extend_from_slice(version);
286 buf.extend_from_slice(nonce);
287 buf.extend_from_slice(ciphertext);
288 buf.extend_from_slice(mac);
289
290 let mut out = String::with_capacity((buf.len() * 4).div_ceil(3));
291 general_purpose::STANDARD.encode_string(&buf, &mut out);
292 out
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use nostro2::NostrSigner;
300
301 #[test]
302 fn test_encrypt_decrypt_success() {
303 let sender = crate::tests::NipTester::generate(false);
305 let receiver = crate::tests::NipTester::generate(false);
306
307 let plaintext = "Hello NIP-44 encryption!";
308 let receiver_pk = receiver.public_key();
309 let sender_pk = sender.public_key();
310 let ciphertext = sender.nip_44_encrypt(plaintext, &receiver_pk).unwrap();
311 let decrypted = receiver.nip_44_decrypt(&ciphertext, &sender_pk).unwrap();
312
313 assert_eq!(decrypted, plaintext);
314 }
315
316 #[test]
317 fn test_invalid_decryption_key() {
318 let sender = crate::tests::NipTester::generate(false);
319 let receiver = crate::tests::NipTester::generate(false);
320 let wrong_receiver = crate::tests::NipTester::generate(false);
321
322 let plaintext = "Hello NIP-44 encryption!";
323 let receiver_pk = receiver.public_key();
324 let sender_pk = sender.public_key();
325 let ciphertext = sender.nip_44_encrypt(plaintext, &receiver_pk).unwrap();
326 let result = wrong_receiver.nip_44_decrypt(&ciphertext, &sender_pk);
327
328 assert!(result.is_err());
329 }
330
331 use std::fmt::Write as _;
332 #[test]
333 fn encrypt_very_large_note() {
334 let sender = crate::tests::NipTester::generate(false);
335 let receiver = crate::tests::NipTester::generate(false);
336
337 let mut plaintext = String::new();
338 for i in 0..15329 {
339 let _ = write!(plaintext, "{i}");
340 }
341 let receiver_pk = receiver.public_key();
342 let sender_pk = sender.public_key();
343 let ciphertext = sender.nip_44_encrypt(&plaintext, &receiver_pk).unwrap();
344 let decrypted = receiver.nip_44_decrypt(&ciphertext, &sender_pk).unwrap();
345
346 assert_eq!(decrypted, plaintext);
347 }
348}