Skip to main content

nostro2_nips/
nip_44.rs

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    /// Computes the shared secret used for encryption and decryption.
44    ///
45    /// # Errors
46    /// Returns `Nip44Error::SharedSecretError` if the ECDH computation fails.
47    fn shared_secret(&self, peer_pubkey: &str) -> Result<zeroize::Zeroizing<[u8; 32]>, Nip44Error>;
48
49    /// Encrypts a NIP-44 encrypted message.
50    ///
51    /// Will modify the note's content in place.
52    ///
53    /// # Errors
54    ///
55    /// - `SharedSecretError`: if shared secret derivation fails.
56    /// - `HkdfError`: if key derivation via HKDF fails.
57    /// - `Base64DecodingError`: if input is not valid base64.
58    /// - `InvalidLength`: if input does not include all required components.
59    /// - `DecryptionError`: if decryption fails or the decrypted length prefix is invalid.
60    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(&note.content, peer_pubkey)?.to_string();
66        Ok(())
67    }
68    /// Decrypts a NIP-44 encrypted message.
69    ///
70    /// Will return the decrypted content as a `Cow` to avoid unnecessary allocations.
71    ///
72    /// # Errors
73    ///
74    /// - `SharedSecretError`: if shared secret derivation fails.
75    /// - `HkdfError`: if key derivation via HKDF fails.
76    /// - `Base64DecodingError`: if input is not valid base64.
77    /// - `InvalidLength`: if input does not include all required components.
78    /// - `DecryptionError`: if decryption fails or the decrypted length prefix is invalid.
79    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(&note.content, peer_pubkey)
85    }
86
87    /// Encrypts the given plaintext using the NIP-44 protocol.
88    ///
89    /// # Errors
90    /// - `SharedSecretError`: if shared secret derivation fails.
91    /// - `HkdfError`: if key derivation via HKDF fails.
92    /// - `EncryptionError`: if the plaintext length is invalid or encryption fails.
93    /// - `HmacError`: if MAC calculation fails.
94    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    /// Decrypts a NIP-44 encrypted message.
123    ///
124    /// # Errors
125    /// - `SharedSecretError`: if shared secret derivation fails.
126    /// - `HkdfError`: if key derivation via HKDF fails.
127    /// - `Base64DecodingError`: if input is not valid base64.
128    /// - `InvalidLength`: if input does not include all required components.
129    /// - `DecryptionError`: if decryption fails or the decrypted length prefix is invalid.
130    /// - `Utf8Error`: if decrypted content is not valid UTF-8.
131    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        // Zeroize sensitive data after use
145        decoded.zeroize();
146
147        Ok(std::str::from_utf8(decrypted)?.to_string().into())
148    }
149    /// Encrypts bytes with the given key and nonce using `ChaCha20`.
150    ///
151    /// # Errors
152    /// - `SliceError`: if key or nonce length is invalid.
153    /// - `EncryptionError`: if input padding fails.
154    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    /// Decrypts a ChaCha20-encrypted message and removes NIP-44 padding.
167    ///
168    /// # Errors
169    /// - `SliceError`: if key or nonce is invalid length.
170    /// - `DecryptionError`: if decrypted data is too short or length prefix is invalid.
171    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        // Zeroize key, nonce, and buffer after use
201        key.zeroize();
202        nonce.zeroize();
203
204        Ok(&buffer[2..2 + len])
205    }
206
207    /// Derives a conversation key using HKDF.
208    ///
209    /// # Errors
210    /// - `HkdfError`: if HKDF expansion fails.
211    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    /// Extracts nonce and ciphertext from the decoded payload.
224    ///
225    /// # Errors
226    /// - `InvalidLength`: if the input is too short to contain required components.
227    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    /// Calculates the HMAC-SHA256 MAC for the given data and key.
237    ///
238    /// # Errors
239    /// - `HmacError`: if the MAC construction fails.
240    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    /// Adds a length prefix and pads plaintext to a power-of-two size.
249    ///
250    /// # Errors
251    /// - `EncryptionError`: if the plaintext is empty or too long.
252    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        // zero pad the rest
268        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        // Use the NipTester from lib.rs which uses k256
304        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}