1use aes::Aes256;
39use aes::cipher::block_padding::Pkcs7;
40use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit};
41use base64::Engine;
42use base64::engine::general_purpose::STANDARD as BASE64;
43use secp256k1::{Parity, ecdh};
44use thiserror::Error;
45use zeroize::Zeroize;
46
47use crate::key::{PublicKey, SecretKey};
48use crate::util::rng::{self, RngError};
49
50const KEY_BYTES: usize = 32;
52const IV_BYTES: usize = 16;
54const SEPARATOR: &str = "?iv=";
56
57type Aes256CbcEnc = cbc::Encryptor<Aes256>;
58type Aes256CbcDec = cbc::Decryptor<Aes256>;
59
60#[derive(Debug, Error)]
62#[non_exhaustive]
63pub enum Nip04Error {
64 #[error("payload is missing the `?iv=` separator")]
66 MissingIvSeparator,
67 #[error("base64 decode failed: {0}")]
69 Base64(#[from] base64::DecodeError),
70 #[error("IV must be {expected} bytes, got {actual}")]
72 InvalidIvLength {
73 expected: usize,
75 actual: usize,
77 },
78 #[error("AES-256-CBC unpadding failed (wrong key or tampered payload)")]
83 Unpad,
84 #[error("decrypted plaintext is not valid UTF-8")]
86 InvalidUtf8,
87 #[error(transparent)]
89 Rng(#[from] RngError),
90}
91
92fn shared_secret_x(secret: &SecretKey, peer: &PublicKey) -> [u8; KEY_BYTES] {
99 let normalized = secp256k1::PublicKey::from_x_only_public_key(*peer.as_inner(), Parity::Even);
100 let ssp = ecdh::shared_secret_point(&normalized, secret.as_inner());
101 let mut x = [0_u8; KEY_BYTES];
102 x.copy_from_slice(&ssp[..KEY_BYTES]);
103 x
104}
105
106pub fn encrypt(
116 secret: &SecretKey,
117 peer: &PublicKey,
118 plaintext: &str,
119) -> Result<String, Nip04Error> {
120 let mut key = shared_secret_x(secret, peer);
121 let iv = rng::random_bytes::<IV_BYTES>()?;
122
123 let ciphertext = Aes256CbcEnc::new((&key).into(), (&iv).into())
124 .encrypt_padded_vec::<Pkcs7>(plaintext.as_bytes());
125
126 key.zeroize();
129
130 Ok(format!(
131 "{}{SEPARATOR}{}",
132 BASE64.encode(&ciphertext),
133 BASE64.encode(iv),
134 ))
135}
136
137pub fn decrypt(secret: &SecretKey, peer: &PublicKey, payload: &str) -> Result<String, Nip04Error> {
151 let (ciphertext_b64, iv_b64) = payload
152 .split_once(SEPARATOR)
153 .ok_or(Nip04Error::MissingIvSeparator)?;
154
155 let mut ciphertext = BASE64.decode(ciphertext_b64)?;
156 let iv_bytes = BASE64.decode(iv_b64)?;
157 let iv: [u8; IV_BYTES] =
158 iv_bytes
159 .as_slice()
160 .try_into()
161 .map_err(|_| Nip04Error::InvalidIvLength {
162 expected: IV_BYTES,
163 actual: iv_bytes.len(),
164 })?;
165
166 let mut key = shared_secret_x(secret, peer);
167 let plaintext = Aes256CbcDec::new((&key).into(), (&iv).into())
168 .decrypt_padded::<Pkcs7>(&mut ciphertext)
169 .map_err(|_| Nip04Error::Unpad)?;
170
171 let result = std::str::from_utf8(plaintext)
172 .map_err(|_| Nip04Error::InvalidUtf8)?
173 .to_owned();
174
175 key.zeroize();
176 Ok(result)
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::key::Keys;
183
184 fn keys_alice() -> Keys {
185 Keys::parse("000000000000000000000000000000000000000000000000000000000000a1ce").unwrap()
186 }
187
188 fn keys_bob() -> Keys {
189 Keys::parse("00000000000000000000000000000000000000000000000000000000000000b0").unwrap()
190 }
191
192 #[test]
193 fn round_trip_short_message() {
194 let alice = keys_alice();
195 let bob = keys_bob();
196 let ciphertext = encrypt(alice.secret_key(), bob.public_key(), "hello").unwrap();
197 let recovered = decrypt(bob.secret_key(), alice.public_key(), &ciphertext).unwrap();
198 assert_eq!(recovered, "hello");
199 }
200
201 #[test]
202 fn round_trip_unicode_payload() {
203 let alice = keys_alice();
206 let bob = keys_bob();
207 let msg = "你好,nostr 🦀";
208 let ciphertext = encrypt(alice.secret_key(), bob.public_key(), msg).unwrap();
209 let recovered = decrypt(bob.secret_key(), alice.public_key(), &ciphertext).unwrap();
210 assert_eq!(recovered, msg);
211 }
212
213 #[test]
214 fn fresh_iv_per_call_yields_distinct_ciphertexts() {
215 let alice = keys_alice();
218 let bob = keys_bob();
219 let a = encrypt(alice.secret_key(), bob.public_key(), "same").unwrap();
220 let b = encrypt(alice.secret_key(), bob.public_key(), "same").unwrap();
221 assert_ne!(a, b, "two encryptions of the same plaintext must differ");
222 }
223
224 #[test]
225 fn empty_plaintext_round_trip() {
226 let alice = keys_alice();
229 let bob = keys_bob();
230 let ciphertext = encrypt(alice.secret_key(), bob.public_key(), "").unwrap();
231 let recovered = decrypt(bob.secret_key(), alice.public_key(), &ciphertext).unwrap();
232 assert_eq!(recovered, "");
233 }
234
235 #[test]
236 fn missing_separator_is_rejected() {
237 let alice = keys_alice();
238 let bob = keys_bob();
239 let err = decrypt(alice.secret_key(), bob.public_key(), "no-separator-here").unwrap_err();
240 assert!(matches!(err, Nip04Error::MissingIvSeparator));
241 }
242
243 #[test]
244 fn malformed_base64_is_rejected() {
245 let alice = keys_alice();
246 let bob = keys_bob();
247 let err = decrypt(
248 alice.secret_key(),
249 bob.public_key(),
250 "!!not-base64!!?iv=!!neither!!",
251 )
252 .unwrap_err();
253 assert!(matches!(err, Nip04Error::Base64(_)));
254 }
255
256 #[test]
257 fn wrong_iv_length_is_rejected() {
258 let alice = keys_alice();
259 let bob = keys_bob();
260 let payload = format!(
262 "{}{SEPARATOR}{}",
263 BASE64.encode([0_u8; 32]),
264 BASE64.encode([0_u8; 8]),
265 );
266 let err = decrypt(alice.secret_key(), bob.public_key(), &payload).unwrap_err();
267 assert!(matches!(
268 err,
269 Nip04Error::InvalidIvLength {
270 expected: 16,
271 actual: 8,
272 }
273 ));
274 }
275
276 #[test]
277 fn wrong_peer_yields_unpad_error() {
278 let alice = keys_alice();
282 let bob = keys_bob();
283 let mallory =
284 Keys::parse("00000000000000000000000000000000000000000000000000000000000ca800")
285 .unwrap();
286
287 let ciphertext = encrypt(alice.secret_key(), bob.public_key(), "for bob").unwrap();
288 let err = decrypt(mallory.secret_key(), alice.public_key(), &ciphertext).unwrap_err();
289 assert!(matches!(err, Nip04Error::Unpad | Nip04Error::InvalidUtf8));
293 }
294}