1use aes_gcm::{
18 aead::{Aead, KeyInit},
19 Aes256Gcm,
20};
21use argon2::Argon2;
22use rand::RngCore;
23use zeroize::Zeroize;
24
25use crate::kdf::{AeadNonce, NostrSecretKey, RecipientSecretKey, SigningSecretKey};
26
27const VERSION_V2: u8 = 2;
29
30const VERSION_V1: u8 = 1;
32
33const AAD: &[u8] = b"void:identity-keys:v1";
35
36const VERSION_LEN: usize = 1;
38const SALT_LEN: usize = 16;
39const TAG_LEN: usize = 16;
40
41const KEYS_LEN_V1: usize = 64; const KEYS_LEN_V2: usize = 96; const MIN_ENCRYPTED_LEN: usize =
47 VERSION_LEN + SALT_LEN + AeadNonce::SIZE + KEYS_LEN_V1 + TAG_LEN;
48
49#[derive(Debug, thiserror::Error)]
51pub enum PinError {
52 #[error("PIN must not be empty")]
53 EmptyPin,
54
55 #[error("key derivation failed: {0}")]
56 DerivationFailed(String),
57
58 #[error("encryption failed")]
59 EncryptionFailed,
60
61 #[error("decryption failed (wrong PIN or corrupted data)")]
62 DecryptionFailed,
63
64 #[error("invalid encrypted data: too short")]
65 DataTooShort,
66
67 #[error("unsupported format version: {0}")]
68 UnsupportedVersion(u8),
69}
70
71pub fn encrypt_identity_keys(
76 signing: &SigningSecretKey,
77 recipient: &RecipientSecretKey,
78 nostr: &NostrSecretKey,
79 pin: &str,
80) -> Result<Vec<u8>, PinError> {
81 if pin.is_empty() {
82 return Err(PinError::EmptyPin);
83 }
84
85 let mut salt = [0u8; SALT_LEN];
87 rand::thread_rng().fill_bytes(&mut salt);
88 let nonce = AeadNonce::generate();
89
90 let mut derived_key = derive_key_from_pin(pin, &salt)?;
92
93 let mut plaintext = [0u8; KEYS_LEN_V2];
95 plaintext[..32].copy_from_slice(signing.as_bytes());
96 plaintext[32..64].copy_from_slice(recipient.as_bytes());
97 plaintext[64..96].copy_from_slice(nostr.as_bytes());
98
99 let cipher =
101 Aes256Gcm::new_from_slice(&derived_key).map_err(|_| PinError::EncryptionFailed)?;
102 let gcm_nonce = aes_gcm::Nonce::from_slice(nonce.as_bytes());
103
104 let ciphertext = match cipher.encrypt(
105 gcm_nonce,
106 aes_gcm::aead::Payload {
107 msg: &plaintext,
108 aad: AAD,
109 },
110 ) {
111 Ok(ciphertext) => ciphertext,
112 Err(_) => {
113 derived_key.zeroize();
114 plaintext.zeroize();
115 return Err(PinError::EncryptionFailed);
116 }
117 };
118
119 derived_key.zeroize();
121 plaintext.zeroize();
122
123 let mut output =
125 Vec::with_capacity(VERSION_LEN + SALT_LEN + AeadNonce::SIZE + ciphertext.len());
126 output.push(VERSION_V2);
127 output.extend_from_slice(&salt);
128 output.extend_from_slice(nonce.as_bytes());
129 output.extend_from_slice(&ciphertext);
130
131 Ok(output)
132}
133
134pub fn decrypt_identity_keys(
139 encrypted: &[u8],
140 pin: &str,
141) -> Result<(SigningSecretKey, RecipientSecretKey, Option<NostrSecretKey>), PinError> {
142 if pin.is_empty() {
143 return Err(PinError::EmptyPin);
144 }
145
146 if encrypted.len() < MIN_ENCRYPTED_LEN {
147 return Err(PinError::DataTooShort);
148 }
149
150 let version = encrypted[0];
152 let expected_payload_len = match version {
153 VERSION_V1 => KEYS_LEN_V1,
154 VERSION_V2 => KEYS_LEN_V2,
155 _ => return Err(PinError::UnsupportedVersion(version)),
156 };
157
158 let salt = &encrypted[VERSION_LEN..VERSION_LEN + SALT_LEN];
159 let nonce_start = VERSION_LEN + SALT_LEN;
160 let nonce_end = nonce_start + AeadNonce::SIZE;
161 let nonce = AeadNonce::from_bytes(&encrypted[nonce_start..nonce_end])
162 .ok_or(PinError::DataTooShort)?;
163 let ciphertext = &encrypted[nonce_end..];
164
165 let mut derived_key = derive_key_from_pin(pin, salt)?;
167
168 let cipher =
170 Aes256Gcm::new_from_slice(&derived_key).map_err(|_| PinError::DecryptionFailed)?;
171 let gcm_nonce = aes_gcm::Nonce::from_slice(nonce.as_bytes());
172
173 let mut plaintext = match cipher.decrypt(
174 gcm_nonce,
175 aes_gcm::aead::Payload {
176 msg: ciphertext,
177 aad: AAD,
178 },
179 ) {
180 Ok(plaintext) => plaintext,
181 Err(_) => {
182 derived_key.zeroize();
183 return Err(PinError::DecryptionFailed);
184 }
185 };
186
187 derived_key.zeroize();
189
190 if plaintext.len() != expected_payload_len {
191 plaintext.zeroize();
192 return Err(PinError::DecryptionFailed);
193 }
194
195 let mut signing_bytes = [0u8; 32];
197 let mut recipient_bytes = [0u8; 32];
198 signing_bytes.copy_from_slice(&plaintext[..32]);
199 recipient_bytes.copy_from_slice(&plaintext[32..64]);
200
201 let nostr = if version == VERSION_V2 {
202 let mut nostr_bytes = [0u8; 32];
203 nostr_bytes.copy_from_slice(&plaintext[64..96]);
204 Some(NostrSecretKey::from_bytes(nostr_bytes))
205 } else {
206 None
207 };
208
209 plaintext.zeroize();
211
212 Ok((
213 SigningSecretKey::from_bytes(signing_bytes),
214 RecipientSecretKey::from_bytes(recipient_bytes),
215 nostr,
216 ))
217}
218
219fn derive_key_from_pin(pin: &str, salt: &[u8]) -> Result<[u8; 32], PinError> {
223 let params = argon2::Params::new(
224 64 * 1024, 3, 4, Some(32), )
229 .map_err(|e| PinError::DerivationFailed(e.to_string()))?;
230
231 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
232
233 let mut output = [0u8; 32];
234 argon2
235 .hash_password_into(pin.as_bytes(), salt, &mut output)
236 .map_err(|e| PinError::DerivationFailed(e.to_string()))?;
237
238 Ok(output)
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 fn test_keys() -> (SigningSecretKey, RecipientSecretKey, NostrSecretKey) {
246 (
247 SigningSecretKey::from_bytes([0x42u8; 32]),
248 RecipientSecretKey::from_bytes([0x99u8; 32]),
249 NostrSecretKey::from_bytes([0xaa; 32]),
250 )
251 }
252
253 #[test]
254 fn roundtrip_encrypt_decrypt_v2() {
255 let (signing, recipient, nostr) = test_keys();
256 let pin = "1234";
257
258 let encrypted = encrypt_identity_keys(&signing, &recipient, &nostr, pin).unwrap();
259 assert_eq!(encrypted[0], VERSION_V2);
260
261 let (dec_signing, dec_recipient, dec_nostr) =
262 decrypt_identity_keys(&encrypted, pin).unwrap();
263
264 assert_eq!(signing.as_bytes(), dec_signing.as_bytes());
265 assert_eq!(recipient.as_bytes(), dec_recipient.as_bytes());
266 assert!(dec_nostr.is_some());
267 assert_eq!(nostr.as_bytes(), dec_nostr.unwrap().as_bytes());
268 }
269
270 #[test]
271 fn backward_compat_v1_decrypt() {
272 let signing = SigningSecretKey::from_bytes([0x42u8; 32]);
273 let recipient = RecipientSecretKey::from_bytes([0x99u8; 32]);
274 let pin = "test-pin";
275
276 let mut salt = [0u8; SALT_LEN];
278 rand::thread_rng().fill_bytes(&mut salt);
279 let nonce = AeadNonce::generate();
280
281 let derived_key = derive_key_from_pin(pin, &salt).unwrap();
282 let cipher = Aes256Gcm::new_from_slice(&derived_key).unwrap();
283 let gcm_nonce = aes_gcm::Nonce::from_slice(nonce.as_bytes());
284
285 let mut plaintext = [0u8; KEYS_LEN_V1];
286 plaintext[..32].copy_from_slice(signing.as_bytes());
287 plaintext[32..].copy_from_slice(recipient.as_bytes());
288
289 let ciphertext = cipher
290 .encrypt(
291 gcm_nonce,
292 aes_gcm::aead::Payload {
293 msg: &plaintext,
294 aad: AAD,
295 },
296 )
297 .unwrap();
298
299 let mut v1_blob = Vec::new();
300 v1_blob.push(VERSION_V1);
301 v1_blob.extend_from_slice(&salt);
302 v1_blob.extend_from_slice(nonce.as_bytes());
303 v1_blob.extend_from_slice(&ciphertext);
304
305 let (dec_signing, dec_recipient, dec_nostr) =
306 decrypt_identity_keys(&v1_blob, pin).unwrap();
307
308 assert_eq!(signing.as_bytes(), dec_signing.as_bytes());
309 assert_eq!(recipient.as_bytes(), dec_recipient.as_bytes());
310 assert!(dec_nostr.is_none());
311 }
312
313 #[test]
314 fn wrong_pin_fails() {
315 let (signing, recipient, nostr) = test_keys();
316
317 let encrypted = encrypt_identity_keys(&signing, &recipient, &nostr, "correct").unwrap();
318 let result = decrypt_identity_keys(&encrypted, "wrong");
319
320 assert!(result.is_err());
321 assert!(matches!(result, Err(PinError::DecryptionFailed)));
322 }
323
324 #[test]
325 fn tampered_data_fails() {
326 let (signing, recipient, nostr) = test_keys();
327
328 let mut encrypted = encrypt_identity_keys(&signing, &recipient, &nostr, "pin").unwrap();
329 let last = encrypted.len() - 1;
330 encrypted[last] ^= 0xFF;
331
332 let result = decrypt_identity_keys(&encrypted, "pin");
333 assert!(result.is_err());
334 }
335
336 #[test]
337 fn empty_pin_rejected() {
338 let (signing, recipient, nostr) = test_keys();
339
340 let result = encrypt_identity_keys(&signing, &recipient, &nostr, "");
341 assert!(matches!(result, Err(PinError::EmptyPin)));
342
343 let result = decrypt_identity_keys(&[0u8; MIN_ENCRYPTED_LEN], "");
344 assert!(matches!(result, Err(PinError::EmptyPin)));
345 }
346
347 #[test]
348 fn too_short_data_rejected() {
349 let result = decrypt_identity_keys(&[1u8; 5], "pin");
350 assert!(matches!(result, Err(PinError::DataTooShort)));
351 }
352
353 #[test]
354 fn unsupported_version_rejected() {
355 let (signing, recipient, nostr) = test_keys();
356
357 let mut encrypted = encrypt_identity_keys(&signing, &recipient, &nostr, "pin").unwrap();
358 encrypted[0] = 99;
359
360 let result = decrypt_identity_keys(&encrypted, "pin");
361 assert!(matches!(result, Err(PinError::UnsupportedVersion(99))));
362 }
363
364 #[test]
365 fn different_encryptions_produce_different_output() {
366 let (signing, recipient, nostr) = test_keys();
367 let pin = "same-pin";
368
369 let enc1 = encrypt_identity_keys(&signing, &recipient, &nostr, pin).unwrap();
370 let enc2 = encrypt_identity_keys(&signing, &recipient, &nostr, pin).unwrap();
371
372 assert_ne!(enc1, enc2);
373
374 let (s1, r1, n1) = decrypt_identity_keys(&enc1, pin).unwrap();
375 let (s2, r2, n2) = decrypt_identity_keys(&enc2, pin).unwrap();
376 assert_eq!(s1.as_bytes(), s2.as_bytes());
377 assert_eq!(r1.as_bytes(), r2.as_bytes());
378 assert_eq!(n1.unwrap().as_bytes(), n2.unwrap().as_bytes());
379 }
380}