1use std::collections::HashMap;
4#[cfg(feature = "security-hardening")]
5use std::hint::spin_loop;
6
7#[cfg(feature = "encrypted")]
8use aes::Aes256;
9#[cfg(feature = "encrypted")]
10use cbc::cipher::block_padding::Pkcs7;
11#[cfg(feature = "encrypted")]
12use cbc::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
13#[cfg(feature = "nostr")]
14use neco_secp::{nostr, SignedEvent, UnsignedEvent};
15use neco_secp::{SecpError, SecretKey, XOnlyPublicKey};
16#[cfg(feature = "encrypted")]
17use scrypt::Params as ScryptParams;
18#[cfg(feature = "encrypted-legacy-v1")]
19use sha2::{Digest, Sha256};
20
21#[cfg(feature = "encrypted")]
22type Aes256CbcEnc = cbc::Encryptor<Aes256>;
23#[cfg(feature = "encrypted")]
24type Aes256CbcDec = cbc::Decryptor<Aes256>;
25#[cfg(feature = "encrypted")]
26const ENCRYPTED_V2_VERSION: u8 = 0x02;
27#[cfg(feature = "encrypted")]
28const ENCRYPTED_V2_LOG_N: u8 = 15;
29#[cfg(feature = "encrypted")]
30const ENCRYPTED_V2_R: u8 = 8;
31#[cfg(feature = "encrypted")]
32const ENCRYPTED_V2_P: u8 = 1;
33#[cfg(feature = "encrypted-legacy-v1")]
34const ENCRYPTED_V1_LEN: usize = 64;
35#[cfg(feature = "encrypted")]
36const ENCRYPTED_V2_LEN: usize = 100;
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct SecurityConfig {
40 pub enable_constant_time: bool,
41 pub enable_random_delay: bool,
42 pub enable_dummy_operations: bool,
43}
44
45impl Default for SecurityConfig {
46 fn default() -> Self {
47 Self {
48 enable_constant_time: true,
49 enable_random_delay: false,
50 enable_dummy_operations: false,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy)]
56pub struct VaultConfig {
57 pub cache_timeout_seconds: u64,
58 pub security: SecurityConfig,
59}
60
61impl Default for VaultConfig {
62 fn default() -> Self {
63 Self {
64 cache_timeout_seconds: 300,
65 security: SecurityConfig::default(),
66 }
67 }
68}
69
70#[derive(Debug)]
71pub enum VaultError {
72 DuplicateLabel,
73 MissingLabel,
74 NoActiveAccount,
75 InvalidEncrypted(&'static str),
76 Crypto(SecpError),
77}
78
79impl core::fmt::Display for VaultError {
80 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
81 match self {
82 Self::DuplicateLabel => f.write_str("duplicate label"),
83 Self::MissingLabel => f.write_str("missing label"),
84 Self::NoActiveAccount => f.write_str("no active account"),
85 Self::InvalidEncrypted(message) => f.write_str(message),
86 Self::Crypto(error) => write!(f, "{error}"),
87 }
88 }
89}
90
91impl std::error::Error for VaultError {}
92
93impl From<SecpError> for VaultError {
94 fn from(value: SecpError) -> Self {
95 Self::Crypto(value)
96 }
97}
98
99#[derive(Debug)]
100struct Entry {
101 secret: SecretKey,
102 last_used_unix_seconds: u64,
103}
104
105#[cfg(feature = "security-hardening")]
106fn apply_random_delay() {
107 let mut byte = [0u8; 1];
108 if getrandom::getrandom(&mut byte).is_err() {
109 return;
110 }
111 let loops = 64 + usize::from(byte[0] & 0x3f);
112 for _ in 0..loops {
113 spin_loop();
114 }
115}
116
117#[cfg(feature = "security-hardening")]
118fn apply_dummy_sign(secret: &SecretKey) {
119 let _ = secret.sign_schnorr_prehash([0x5a; 32]);
120}
121
122#[cfg(all(feature = "security-hardening", feature = "nip04"))]
123fn apply_dummy_nip04(secret: &SecretKey) {
124 if let Ok(peer) = secret.xonly_public_key() {
125 let _ = neco_secp::nip04::encrypt(secret, &peer, "", Some([0u8; 16]));
126 }
127}
128
129#[cfg(all(feature = "security-hardening", feature = "nip44"))]
130fn apply_dummy_nip44(secret: &SecretKey) {
131 if let Ok(peer) = secret.xonly_public_key() {
132 if let Ok(conversation_key) = neco_secp::nip44::get_conversation_key(secret, &peer) {
133 let _ = neco_secp::nip44::encrypt("", &conversation_key, Some([0u8; 32]));
134 }
135 }
136}
137
138#[cfg(feature = "security-hardening")]
139fn apply_security_before(security: SecurityConfig, secret: &SecretKey) {
140 if security.enable_dummy_operations {
141 apply_dummy_sign(secret);
142 }
143 if security.enable_random_delay {
144 apply_random_delay();
145 }
146 if security.enable_constant_time {
147 std::hint::black_box(secret.to_bytes());
148 }
149}
150
151#[cfg(feature = "security-hardening")]
152fn apply_security_after(security: SecurityConfig, secret: &SecretKey) {
153 if security.enable_constant_time {
154 std::hint::black_box(secret.to_bytes());
155 }
156 if security.enable_dummy_operations {
157 apply_dummy_sign(secret);
158 }
159 if security.enable_random_delay {
160 apply_random_delay();
161 }
162}
163
164#[cfg(feature = "encrypted-legacy-v1")]
165fn sha256(input: &[u8]) -> [u8; 32] {
166 let mut out = [0u8; 32];
167 out.copy_from_slice(&Sha256::digest(input));
168 out
169}
170
171#[cfg(feature = "encrypted")]
172fn scrypt_derive(
173 passphrase: &[u8],
174 salt: &[u8; 32],
175 log_n: u8,
176 r: u8,
177 p: u8,
178) -> Result<[u8; 32], VaultError> {
179 let params = ScryptParams::new(log_n, r.into(), p.into(), 32)
180 .map_err(|_| VaultError::InvalidEncrypted("invalid scrypt params"))?;
181 let mut out = [0u8; 32];
182 scrypt::scrypt(passphrase, salt, ¶ms, &mut out)
183 .map_err(|_| VaultError::InvalidEncrypted("failed to derive key"))?;
184 Ok(out)
185}
186
187#[cfg(feature = "encrypted")]
188fn aes256_cbc_encrypt(
189 key: &[u8; 32],
190 iv: &[u8; 16],
191 plaintext: &[u8],
192) -> Result<Vec<u8>, VaultError> {
193 let mut buf = plaintext.to_vec();
194 let msg_len = buf.len();
195 buf.resize(msg_len + 16, 0);
196 let ciphertext = Aes256CbcEnc::new(key.into(), iv.into())
197 .encrypt_padded_mut::<Pkcs7>(&mut buf, msg_len)
198 .map_err(|_| VaultError::InvalidEncrypted("failed to encrypt"))?;
199 Ok(ciphertext.to_vec())
200}
201
202#[cfg(feature = "encrypted")]
203fn aes256_cbc_decrypt(
204 key: &[u8; 32],
205 iv: &[u8; 16],
206 ciphertext: &[u8],
207) -> Result<Vec<u8>, VaultError> {
208 let mut buf = ciphertext.to_vec();
209 let plaintext = Aes256CbcDec::new(key.into(), iv.into())
210 .decrypt_padded_mut::<Pkcs7>(&mut buf)
211 .map_err(|_| VaultError::InvalidEncrypted("failed to decrypt"))?;
212 Ok(plaintext.to_vec())
213}
214
215#[derive(Debug)]
216pub struct Vault {
217 config: VaultConfig,
218 entries: HashMap<String, Entry>,
219 active_label: Option<String>,
220}
221
222impl Vault {
223 pub fn new(config: VaultConfig) -> Result<Self, VaultError> {
224 Ok(Self {
225 config,
226 entries: HashMap::new(),
227 active_label: None,
228 })
229 }
230
231 pub fn import_plaintext(
232 &mut self,
233 label: &str,
234 secret: SecretKey,
235 now_unix_seconds: u64,
236 ) -> Result<(), VaultError> {
237 if self.entries.contains_key(label) {
238 return Err(VaultError::DuplicateLabel);
239 }
240 let set_active = self.entries.is_empty();
241 self.entries.insert(
242 label.to_string(),
243 Entry {
244 secret,
245 last_used_unix_seconds: now_unix_seconds,
246 },
247 );
248 if set_active {
249 self.active_label = Some(label.to_string());
250 }
251 Ok(())
252 }
253
254 pub fn contains(&self, label: &str) -> bool {
255 self.entries.contains_key(label)
256 }
257
258 pub fn set_active(&mut self, label: &str) -> Result<(), VaultError> {
259 if !self.entries.contains_key(label) {
260 return Err(VaultError::MissingLabel);
261 }
262 self.active_label = Some(label.to_string());
263 Ok(())
264 }
265
266 pub fn active_label(&self) -> Option<&str> {
267 self.active_label.as_deref()
268 }
269
270 pub fn set_security_config(&mut self, security: SecurityConfig) {
271 self.config.security = security;
272 }
273
274 pub fn security_config(&self) -> SecurityConfig {
275 self.config.security
276 }
277
278 pub fn remove(&mut self, label: &str) -> Result<(), VaultError> {
279 if self.entries.remove(label).is_none() {
280 return Err(VaultError::MissingLabel);
281 }
282 if self.active_label.as_deref() == Some(label) {
283 self.active_label = None;
284 }
285 Ok(())
286 }
287
288 pub fn labels(&self) -> Vec<&str> {
289 let mut labels: Vec<_> = self.entries.keys().map(String::as_str).collect();
290 labels.sort();
291 labels
292 }
293
294 pub fn public_key(&self, label: &str) -> Result<XOnlyPublicKey, VaultError> {
295 let entry = self.entries.get(label).ok_or(VaultError::MissingLabel)?;
296 entry.secret.xonly_public_key().map_err(VaultError::from)
297 }
298
299 pub fn public_key_active(&self) -> Result<XOnlyPublicKey, VaultError> {
300 let label = self
301 .active_label
302 .as_deref()
303 .ok_or(VaultError::NoActiveAccount)?;
304 self.public_key(label)
305 }
306
307 #[cfg(feature = "nostr")]
308 pub fn sign_event(
309 &mut self,
310 label: &str,
311 unsigned: UnsignedEvent,
312 now_unix_seconds: u64,
313 ) -> Result<SignedEvent, VaultError> {
314 #[cfg(feature = "security-hardening")]
315 let security = self.config.security;
316 let entry = self
317 .entries
318 .get_mut(label)
319 .ok_or(VaultError::MissingLabel)?;
320 entry.last_used_unix_seconds = now_unix_seconds;
321 #[cfg(feature = "security-hardening")]
322 apply_security_before(security, &entry.secret);
323 let signed = nostr::finalize_event(unsigned, &entry.secret).map_err(VaultError::from)?;
324 #[cfg(feature = "security-hardening")]
325 apply_security_after(security, &entry.secret);
326 Ok(signed)
327 }
328
329 #[cfg(feature = "nostr")]
330 pub fn sign_event_active(
331 &mut self,
332 unsigned: UnsignedEvent,
333 now_unix_seconds: u64,
334 ) -> Result<SignedEvent, VaultError> {
335 let label = self
336 .active_label
337 .clone()
338 .ok_or(VaultError::NoActiveAccount)?;
339 self.sign_event(&label, unsigned, now_unix_seconds)
340 }
341
342 #[cfg(feature = "nostr")]
343 pub fn create_auth_event(
344 &mut self,
345 label: &str,
346 challenge: &str,
347 relay_url: &str,
348 now_unix_seconds: u64,
349 ) -> Result<SignedEvent, VaultError> {
350 #[cfg(feature = "security-hardening")]
351 let security = self.config.security;
352 let entry = self
353 .entries
354 .get_mut(label)
355 .ok_or(VaultError::MissingLabel)?;
356 entry.last_used_unix_seconds = now_unix_seconds;
357 #[cfg(feature = "security-hardening")]
358 apply_security_before(security, &entry.secret);
359 let event = neco_secp::nip42::create_auth_event(
360 challenge,
361 relay_url,
362 &entry.secret,
363 now_unix_seconds,
364 )
365 .map_err(VaultError::from)?;
366 #[cfg(feature = "security-hardening")]
367 apply_security_after(security, &entry.secret);
368 Ok(event)
369 }
370
371 #[cfg(feature = "nostr")]
372 pub fn create_auth_event_active(
373 &mut self,
374 challenge: &str,
375 relay_url: &str,
376 now_unix_seconds: u64,
377 ) -> Result<SignedEvent, VaultError> {
378 let label = self
379 .active_label
380 .clone()
381 .ok_or(VaultError::NoActiveAccount)?;
382 self.create_auth_event(&label, challenge, relay_url, now_unix_seconds)
383 }
384
385 pub fn clear_cache(&mut self) {
386 self.entries.clear();
387 self.active_label = None;
388 }
389
390 pub fn clear_expired_cache(&mut self, now_unix_seconds: u64) {
391 let timeout = self.config.cache_timeout_seconds;
392 self.entries.retain(|_, entry| {
393 now_unix_seconds.saturating_sub(entry.last_used_unix_seconds) <= timeout
394 });
395 if self
396 .active_label
397 .as_deref()
398 .is_some_and(|label| !self.entries.contains_key(label))
399 {
400 self.active_label = None;
401 }
402 }
403}
404
405#[cfg(feature = "nip04")]
406impl Vault {
407 pub fn nip04_encrypt(
408 &mut self,
409 label: &str,
410 peer: &XOnlyPublicKey,
411 plaintext: &str,
412 now_unix_seconds: u64,
413 ) -> Result<String, VaultError> {
414 #[cfg(feature = "security-hardening")]
415 let security = self.config.security;
416 let entry = self
417 .entries
418 .get_mut(label)
419 .ok_or(VaultError::MissingLabel)?;
420 entry.last_used_unix_seconds = now_unix_seconds;
421 #[cfg(feature = "security-hardening")]
422 {
423 apply_security_before(security, &entry.secret);
424 if security.enable_dummy_operations {
425 apply_dummy_nip04(&entry.secret);
426 }
427 }
428 let payload = neco_secp::nip04::encrypt(&entry.secret, peer, plaintext, None)
429 .map_err(VaultError::from)?;
430 #[cfg(feature = "security-hardening")]
431 apply_security_after(security, &entry.secret);
432 Ok(payload)
433 }
434
435 pub fn nip04_decrypt(
436 &mut self,
437 label: &str,
438 peer: &XOnlyPublicKey,
439 payload: &str,
440 now_unix_seconds: u64,
441 ) -> Result<String, VaultError> {
442 #[cfg(feature = "security-hardening")]
443 let security = self.config.security;
444 let entry = self
445 .entries
446 .get_mut(label)
447 .ok_or(VaultError::MissingLabel)?;
448 entry.last_used_unix_seconds = now_unix_seconds;
449 #[cfg(feature = "security-hardening")]
450 {
451 apply_security_before(security, &entry.secret);
452 if security.enable_dummy_operations {
453 apply_dummy_nip04(&entry.secret);
454 }
455 }
456 let plaintext =
457 neco_secp::nip04::decrypt(&entry.secret, peer, payload).map_err(VaultError::from)?;
458 #[cfg(feature = "security-hardening")]
459 apply_security_after(security, &entry.secret);
460 Ok(plaintext)
461 }
462
463 pub fn nip04_encrypt_active(
464 &mut self,
465 peer: &XOnlyPublicKey,
466 plaintext: &str,
467 now_unix_seconds: u64,
468 ) -> Result<String, VaultError> {
469 let label = self
470 .active_label
471 .as_deref()
472 .ok_or(VaultError::NoActiveAccount)?
473 .to_string();
474 self.nip04_encrypt(&label, peer, plaintext, now_unix_seconds)
475 }
476
477 pub fn nip04_decrypt_active(
478 &mut self,
479 peer: &XOnlyPublicKey,
480 payload: &str,
481 now_unix_seconds: u64,
482 ) -> Result<String, VaultError> {
483 let label = self
484 .active_label
485 .as_deref()
486 .ok_or(VaultError::NoActiveAccount)?
487 .to_string();
488 self.nip04_decrypt(&label, peer, payload, now_unix_seconds)
489 }
490}
491
492#[cfg(feature = "nip44")]
493impl Vault {
494 pub fn nip44_encrypt(
495 &mut self,
496 label: &str,
497 peer: &XOnlyPublicKey,
498 plaintext: &str,
499 now_unix_seconds: u64,
500 ) -> Result<String, VaultError> {
501 #[cfg(feature = "security-hardening")]
502 let security = self.config.security;
503 let entry = self
504 .entries
505 .get_mut(label)
506 .ok_or(VaultError::MissingLabel)?;
507 entry.last_used_unix_seconds = now_unix_seconds;
508 #[cfg(feature = "security-hardening")]
509 {
510 apply_security_before(security, &entry.secret);
511 if security.enable_dummy_operations {
512 apply_dummy_nip44(&entry.secret);
513 }
514 }
515 let conversation_key = neco_secp::nip44::get_conversation_key(&entry.secret, peer)
516 .map_err(VaultError::from)?;
517 let payload = neco_secp::nip44::encrypt(plaintext, &conversation_key, None)
518 .map_err(VaultError::from)?;
519 #[cfg(feature = "security-hardening")]
520 apply_security_after(security, &entry.secret);
521 Ok(payload)
522 }
523
524 pub fn nip44_decrypt(
525 &mut self,
526 label: &str,
527 peer: &XOnlyPublicKey,
528 payload: &str,
529 now_unix_seconds: u64,
530 ) -> Result<String, VaultError> {
531 #[cfg(feature = "security-hardening")]
532 let security = self.config.security;
533 let entry = self
534 .entries
535 .get_mut(label)
536 .ok_or(VaultError::MissingLabel)?;
537 entry.last_used_unix_seconds = now_unix_seconds;
538 #[cfg(feature = "security-hardening")]
539 {
540 apply_security_before(security, &entry.secret);
541 if security.enable_dummy_operations {
542 apply_dummy_nip44(&entry.secret);
543 }
544 }
545 let conversation_key = neco_secp::nip44::get_conversation_key(&entry.secret, peer)
546 .map_err(VaultError::from)?;
547 let plaintext =
548 neco_secp::nip44::decrypt(payload, &conversation_key).map_err(VaultError::from)?;
549 #[cfg(feature = "security-hardening")]
550 apply_security_after(security, &entry.secret);
551 Ok(plaintext)
552 }
553
554 pub fn nip44_encrypt_active(
555 &mut self,
556 peer: &XOnlyPublicKey,
557 plaintext: &str,
558 now_unix_seconds: u64,
559 ) -> Result<String, VaultError> {
560 let label = self
561 .active_label
562 .as_deref()
563 .ok_or(VaultError::NoActiveAccount)?
564 .to_string();
565 self.nip44_encrypt(&label, peer, plaintext, now_unix_seconds)
566 }
567
568 pub fn nip44_decrypt_active(
569 &mut self,
570 peer: &XOnlyPublicKey,
571 payload: &str,
572 now_unix_seconds: u64,
573 ) -> Result<String, VaultError> {
574 let label = self
575 .active_label
576 .as_deref()
577 .ok_or(VaultError::NoActiveAccount)?
578 .to_string();
579 self.nip44_decrypt(&label, peer, payload, now_unix_seconds)
580 }
581}
582
583#[cfg(feature = "nip17")]
584impl Vault {
585 pub fn create_sealed_dm(
586 &mut self,
587 label: &str,
588 content: &str,
589 recipient: &XOnlyPublicKey,
590 now_unix_seconds: u64,
591 ) -> Result<SignedEvent, VaultError> {
592 #[cfg(feature = "security-hardening")]
593 let security = self.config.security;
594 let entry = self
595 .entries
596 .get_mut(label)
597 .ok_or(VaultError::MissingLabel)?;
598 entry.last_used_unix_seconds = now_unix_seconds;
599 #[cfg(feature = "security-hardening")]
600 {
601 apply_security_before(security, &entry.secret);
602 if security.enable_dummy_operations {
603 apply_dummy_nip44(&entry.secret);
604 }
605 }
606 let inner = UnsignedEvent {
607 created_at: now_unix_seconds,
608 kind: 14,
609 tags: vec![vec!["p".to_string(), recipient.to_hex()]],
610 content: content.to_string(),
611 };
612 let seal = neco_secp::nip17::create_seal(inner, &entry.secret, recipient)
613 .map_err(VaultError::from)?;
614 let gift_wrap =
615 neco_secp::nip17::create_gift_wrap(&seal, recipient).map_err(VaultError::from)?;
616 #[cfg(feature = "security-hardening")]
617 apply_security_after(security, &entry.secret);
618 Ok(gift_wrap)
619 }
620
621 pub fn open_gift_wrap_dm(
622 &mut self,
623 label: &str,
624 gift_wrap: &SignedEvent,
625 now_unix_seconds: u64,
626 ) -> Result<SignedEvent, VaultError> {
627 #[cfg(feature = "security-hardening")]
628 let security = self.config.security;
629 let entry = self
630 .entries
631 .get_mut(label)
632 .ok_or(VaultError::MissingLabel)?;
633 entry.last_used_unix_seconds = now_unix_seconds;
634 #[cfg(feature = "security-hardening")]
635 {
636 apply_security_before(security, &entry.secret);
637 if security.enable_dummy_operations {
638 apply_dummy_nip44(&entry.secret);
639 }
640 }
641 let inner =
642 neco_secp::nip17::open_gift_wrap(gift_wrap, &entry.secret).map_err(VaultError::from)?;
643 #[cfg(feature = "security-hardening")]
644 apply_security_after(security, &entry.secret);
645 Ok(inner)
646 }
647}
648
649#[cfg(feature = "encrypted")]
650impl Vault {
651 pub fn export_encrypted(&self, label: &str, passphrase: &[u8]) -> Result<Vec<u8>, VaultError> {
652 let entry = self.entries.get(label).ok_or(VaultError::MissingLabel)?;
653 let mut salt = [0u8; 32];
654 let mut iv = [0u8; 16];
655 getrandom::getrandom(&mut salt)
656 .map_err(|_| VaultError::InvalidEncrypted("failed to generate salt"))?;
657 getrandom::getrandom(&mut iv)
658 .map_err(|_| VaultError::InvalidEncrypted("failed to generate iv"))?;
659 let key = scrypt_derive(
660 passphrase,
661 &salt,
662 ENCRYPTED_V2_LOG_N,
663 ENCRYPTED_V2_R,
664 ENCRYPTED_V2_P,
665 )?;
666 let ciphertext = aes256_cbc_encrypt(&key, &iv, &entry.secret.to_bytes())?;
667 let mut out = Vec::with_capacity(ENCRYPTED_V2_LEN);
668 out.push(ENCRYPTED_V2_VERSION);
669 out.push(ENCRYPTED_V2_LOG_N);
670 out.push(ENCRYPTED_V2_R);
671 out.push(ENCRYPTED_V2_P);
672 out.extend_from_slice(&salt);
673 out.extend_from_slice(&iv);
674 out.extend_from_slice(&ciphertext);
675 Ok(out)
676 }
677
678 pub fn import_encrypted(
679 &mut self,
680 label: &str,
681 passphrase: &[u8],
682 data: &[u8],
683 now_unix_seconds: u64,
684 ) -> Result<(), VaultError> {
685 let (key, iv, ciphertext) =
686 if data.len() == ENCRYPTED_V2_LEN && data[0] == ENCRYPTED_V2_VERSION {
687 let log_n = data[1];
688 let r = data[2];
689 let p = data[3];
690 let salt: [u8; 32] = data[4..36]
691 .try_into()
692 .map_err(|_| VaultError::InvalidEncrypted("invalid salt"))?;
693 let iv: [u8; 16] = data[36..52]
694 .try_into()
695 .map_err(|_| VaultError::InvalidEncrypted("invalid iv"))?;
696 let key = scrypt_derive(passphrase, &salt, log_n, r, p)?;
697 (key, iv, &data[52..])
698 } else {
699 #[cfg(feature = "encrypted-legacy-v1")]
700 if data.len() == ENCRYPTED_V1_LEN {
701 let iv: [u8; 16] = data[..16]
702 .try_into()
703 .map_err(|_| VaultError::InvalidEncrypted("invalid iv"))?;
704 (sha256(passphrase), iv, &data[16..])
705 } else {
706 return Err(VaultError::InvalidEncrypted("invalid encrypted payload"));
707 }
708
709 #[cfg(not(feature = "encrypted-legacy-v1"))]
710 {
711 return Err(VaultError::InvalidEncrypted("invalid encrypted payload"));
712 }
713 };
714 let plaintext = aes256_cbc_decrypt(&key, &iv, ciphertext)?;
715 let secret_bytes: [u8; 32] = plaintext
716 .as_slice()
717 .try_into()
718 .map_err(|_| VaultError::InvalidEncrypted("invalid secret key"))?;
719 let secret = SecretKey::from_bytes(secret_bytes)
720 .map_err(|_| VaultError::InvalidEncrypted("invalid secret key"))?;
721 self.import_plaintext(label, secret, now_unix_seconds)
722 }
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728
729 #[cfg(feature = "encrypted-legacy-v1")]
730 fn legacy_v1_payload(secret: &SecretKey, passphrase: &[u8]) -> Vec<u8> {
731 let key = sha256(passphrase);
732 let iv = [0x55; 16];
733 let ciphertext = aes256_cbc_encrypt(&key, &iv, &secret.to_bytes()).expect("legacy encrypt");
734 let mut exported = Vec::with_capacity(ENCRYPTED_V1_LEN);
735 exported.extend_from_slice(&iv);
736 exported.extend_from_slice(&ciphertext);
737 exported
738 }
739
740 #[cfg(all(feature = "encrypted", not(feature = "encrypted-legacy-v1")))]
741 fn legacy_v1_payload(secret: &SecretKey, passphrase: &[u8]) -> Vec<u8> {
742 use scrypt::Params as ScryptParams;
743
744 let mut out = [0u8; 32];
745 let params = ScryptParams::new(15, 8, 1, 32).expect("scrypt params");
746 scrypt::scrypt(passphrase, &[0u8; 32], ¶ms, &mut out).expect("scrypt");
747 let iv = [0x55; 16];
748 let ciphertext = aes256_cbc_encrypt(&out, &iv, &secret.to_bytes()).expect("ciphertext");
749 let mut exported = Vec::with_capacity(64);
750 exported.extend_from_slice(&iv);
751 exported.extend_from_slice(&ciphertext);
752 exported
753 }
754
755 #[test]
756 fn first_import_sets_active() {
757 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
758 let secret = SecretKey::generate().expect("secret");
759 vault.import_plaintext("main", secret, 100).expect("import");
760 assert_eq!(vault.active_label(), Some("main"));
761 }
762
763 #[test]
764 fn second_import_does_not_change_active() {
765 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
766 vault
767 .import_plaintext("main", SecretKey::generate().expect("main secret"), 100)
768 .expect("first import");
769 vault
770 .import_plaintext("backup", SecretKey::generate().expect("backup secret"), 101)
771 .expect("second import");
772 assert_eq!(vault.active_label(), Some("main"));
773 }
774
775 #[test]
776 fn set_active_switches_label() {
777 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
778 vault
779 .import_plaintext("main", SecretKey::generate().expect("main secret"), 100)
780 .expect("first import");
781 vault
782 .import_plaintext("backup", SecretKey::generate().expect("backup secret"), 101)
783 .expect("second import");
784 vault.set_active("backup").expect("set active");
785 assert_eq!(vault.active_label(), Some("backup"));
786 }
787
788 #[test]
789 fn set_active_missing_label_fails() {
790 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
791 let error = vault
792 .set_active("missing")
793 .expect_err("missing label must fail");
794 assert!(matches!(error, VaultError::MissingLabel));
795 }
796
797 #[test]
798 fn remove_active_sets_none() {
799 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
800 vault
801 .import_plaintext("main", SecretKey::generate().expect("secret"), 100)
802 .expect("import");
803 vault.remove("main").expect("remove");
804 assert_eq!(vault.active_label(), None);
805 }
806
807 #[test]
808 fn remove_non_active_keeps_active() {
809 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
810 vault
811 .import_plaintext("main", SecretKey::generate().expect("main secret"), 100)
812 .expect("first import");
813 vault
814 .import_plaintext("backup", SecretKey::generate().expect("backup secret"), 101)
815 .expect("second import");
816 vault.remove("backup").expect("remove");
817 assert_eq!(vault.active_label(), Some("main"));
818 }
819
820 #[test]
821 fn labels_returns_all() {
822 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
823 vault
824 .import_plaintext("main", SecretKey::generate().expect("main secret"), 100)
825 .expect("first import");
826 vault
827 .import_plaintext("backup", SecretKey::generate().expect("backup secret"), 101)
828 .expect("second import");
829 assert_eq!(vault.labels(), vec!["backup", "main"]);
830 }
831
832 #[test]
833 fn public_key_returns_expected_xonly() {
834 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
835 let secret = SecretKey::generate().expect("secret");
836 let expected = secret.xonly_public_key().expect("public key");
837 vault.import_plaintext("main", secret, 100).expect("import");
838 assert_eq!(
839 vault.public_key("main").expect("vault public key"),
840 expected
841 );
842 }
843
844 #[test]
845 fn public_key_active_returns_expected_xonly() {
846 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
847 let secret = SecretKey::generate().expect("secret");
848 let expected = secret.xonly_public_key().expect("public key");
849 vault.import_plaintext("main", secret, 100).expect("import");
850 assert_eq!(
851 vault.public_key_active().expect("active public key"),
852 expected
853 );
854 }
855
856 #[test]
857 fn public_key_missing_label_fails() {
858 let vault = Vault::new(VaultConfig::default()).expect("vault");
859 let error = vault
860 .public_key("missing")
861 .expect_err("missing label must fail");
862 assert!(matches!(error, VaultError::MissingLabel));
863 }
864
865 #[test]
866 fn public_key_active_without_active_fails() {
867 let vault = Vault::new(VaultConfig::default()).expect("vault");
868 let error = vault
869 .public_key_active()
870 .expect_err("active label must exist");
871 assert!(matches!(error, VaultError::NoActiveAccount));
872 }
873
874 #[test]
875 fn default_security_config_matches_spec() {
876 let vault = Vault::new(VaultConfig::default()).expect("vault");
877 assert_eq!(
878 vault.security_config(),
879 SecurityConfig {
880 enable_constant_time: true,
881 enable_random_delay: false,
882 enable_dummy_operations: false,
883 }
884 );
885 }
886
887 #[test]
888 fn set_security_config_updates_vault_immediately() {
889 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
890 let security = SecurityConfig {
891 enable_constant_time: false,
892 enable_random_delay: true,
893 enable_dummy_operations: true,
894 };
895 vault.set_security_config(security);
896 assert_eq!(vault.security_config(), security);
897 }
898
899 #[test]
900 fn clear_cache_resets_active() {
901 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
902 vault
903 .import_plaintext("main", SecretKey::generate().expect("secret"), 100)
904 .expect("import");
905 vault.clear_cache();
906 assert_eq!(vault.active_label(), None);
907 }
908
909 #[test]
910 fn clear_expired_resets_active_when_expired() {
911 let mut vault = Vault::new(VaultConfig {
912 cache_timeout_seconds: 10,
913 security: SecurityConfig::default(),
914 })
915 .expect("vault");
916 vault
917 .import_plaintext("main", SecretKey::generate().expect("secret"), 100)
918 .expect("import");
919 vault.clear_expired_cache(111);
920 assert_eq!(vault.active_label(), None);
921 }
922
923 #[test]
924 fn clear_expired_keeps_active_when_fresh() {
925 let mut vault = Vault::new(VaultConfig {
926 cache_timeout_seconds: 10,
927 security: SecurityConfig::default(),
928 })
929 .expect("vault");
930 vault
931 .import_plaintext("main", SecretKey::generate().expect("secret"), 100)
932 .expect("import");
933 vault.clear_expired_cache(110);
934 assert_eq!(vault.active_label(), Some("main"));
935 }
936
937 #[test]
938 #[cfg(feature = "nostr")]
939 fn sign_event_active_works() {
940 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
941 let secret = SecretKey::generate().expect("secret");
942 vault.import_plaintext("main", secret, 100).expect("import");
943
944 let unsigned = UnsignedEvent {
945 created_at: 101,
946 kind: 1,
947 tags: Vec::new(),
948 content: "hello".to_string(),
949 };
950
951 let signed = vault.sign_event_active(unsigned, 102).expect("sign");
952 nostr::verify_event(&signed).expect("verify");
953 }
954
955 #[test]
956 fn duplicate_label_is_rejected() {
957 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
958 let secret = SecretKey::generate().expect("secret");
959 vault
960 .import_plaintext("main", secret, 100)
961 .expect("first import");
962 let error = vault
963 .import_plaintext("main", secret, 101)
964 .expect_err("must reject duplicate");
965 assert!(matches!(error, VaultError::DuplicateLabel));
966 }
967
968 #[test]
969 #[cfg(feature = "nostr")]
970 fn sign_event_active_no_active_fails() {
971 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
972 let unsigned = UnsignedEvent {
973 created_at: 101,
974 kind: 1,
975 tags: Vec::new(),
976 content: "hello".to_string(),
977 };
978 let error = vault
979 .sign_event_active(unsigned, 102)
980 .expect_err("active label must exist");
981 assert!(matches!(error, VaultError::NoActiveAccount));
982 }
983
984 #[test]
985 #[cfg(feature = "nostr")]
986 fn missing_label_is_rejected() {
987 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
988 let unsigned = UnsignedEvent {
989 created_at: 101,
990 kind: 1,
991 tags: Vec::new(),
992 content: "hello".to_string(),
993 };
994 let error = vault
995 .sign_event("missing", unsigned, 102)
996 .expect_err("missing label must fail");
997 assert!(matches!(error, VaultError::MissingLabel));
998 }
999
1000 #[test]
1001 fn clear_expired_cache_removes_old_entries() {
1002 let mut vault = Vault::new(VaultConfig {
1003 cache_timeout_seconds: 10,
1004 security: SecurityConfig::default(),
1005 })
1006 .expect("vault");
1007 let secret = SecretKey::generate().expect("secret");
1008 vault.import_plaintext("main", secret, 100).expect("import");
1009 vault.clear_expired_cache(111);
1010 assert!(!vault.contains("main"));
1011 }
1012
1013 #[test]
1014 fn clear_expired_cache_keeps_entry_at_timeout_boundary() {
1015 let mut vault = Vault::new(VaultConfig {
1016 cache_timeout_seconds: 10,
1017 security: SecurityConfig::default(),
1018 })
1019 .expect("vault");
1020 let secret = SecretKey::generate().expect("secret");
1021 vault.import_plaintext("main", secret, 100).expect("import");
1022 vault.clear_expired_cache(110);
1023 assert!(vault.contains("main"));
1024 }
1025
1026 #[test]
1027 fn clear_expired_cache_removes_only_expired_labels() {
1028 let mut vault = Vault::new(VaultConfig {
1029 cache_timeout_seconds: 10,
1030 security: SecurityConfig::default(),
1031 })
1032 .expect("vault");
1033 vault
1034 .import_plaintext("old", SecretKey::generate().expect("old secret"), 100)
1035 .expect("old import");
1036 vault
1037 .import_plaintext("fresh", SecretKey::generate().expect("fresh secret"), 105)
1038 .expect("fresh import");
1039 vault.clear_expired_cache(111);
1040 assert!(!vault.contains("old"));
1041 assert!(vault.contains("fresh"));
1042 }
1043
1044 #[test]
1045 #[cfg(feature = "nostr")]
1046 fn vault_sign_matches_core_finalize_event() {
1047 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
1048 let secret = SecretKey::generate().expect("secret");
1049 vault.import_plaintext("main", secret, 100).expect("import");
1050
1051 let unsigned = UnsignedEvent {
1052 created_at: 101,
1053 kind: 7,
1054 tags: vec![vec!["t".to_string(), "rust".to_string()]],
1055 content: "hello".to_string(),
1056 };
1057
1058 let from_vault = vault
1063 .sign_event("main", unsigned.clone(), 102)
1064 .expect("vault sign");
1065 let from_core = nostr::finalize_event(unsigned, &secret).expect("core sign");
1066 assert_eq!(from_vault.id, from_core.id);
1067 assert_eq!(from_vault.pubkey, from_core.pubkey);
1068 assert_eq!(from_vault.created_at, from_core.created_at);
1069 assert_eq!(from_vault.kind, from_core.kind);
1070 assert_eq!(from_vault.tags, from_core.tags);
1071 assert_eq!(from_vault.content, from_core.content);
1072 nostr::verify_event(&from_vault).expect("verify vault");
1073 nostr::verify_event(&from_core).expect("verify core");
1074 }
1075
1076 #[test]
1077 #[cfg(feature = "nostr")]
1078 fn vault_nip42_create_auth() {
1079 let mut vault = Vault::new(VaultConfig {
1080 cache_timeout_seconds: 10,
1081 security: SecurityConfig::default(),
1082 })
1083 .expect("vault");
1084 let secret = SecretKey::generate().expect("secret");
1085 let expected = secret.xonly_public_key().expect("pubkey");
1086 vault.import_plaintext("main", secret, 100).expect("import");
1087
1088 let event = vault
1089 .create_auth_event("main", "challenge-123", "wss://relay.example.com", 105)
1090 .expect("create auth event");
1091
1092 assert_eq!(
1093 neco_secp::nip42::validate_auth_event(
1094 &event,
1095 "challenge-123",
1096 "wss://relay.example.com"
1097 )
1098 .expect("validate auth event"),
1099 expected
1100 );
1101
1102 vault.clear_expired_cache(115);
1103 assert!(vault.contains("main"));
1104 }
1105
1106 #[test]
1107 #[cfg(feature = "nostr")]
1108 fn vault_nip42_active_auth() {
1109 let mut vault = Vault::new(VaultConfig {
1110 cache_timeout_seconds: 10,
1111 security: SecurityConfig::default(),
1112 })
1113 .expect("vault");
1114 let secret = SecretKey::generate().expect("secret");
1115 let expected = secret.xonly_public_key().expect("pubkey");
1116 vault.import_plaintext("main", secret, 100).expect("import");
1117
1118 let event = vault
1119 .create_auth_event_active("challenge-456", "wss://relay.example.com", 105)
1120 .expect("create auth event");
1121
1122 assert_eq!(
1123 neco_secp::nip42::validate_auth_event(
1124 &event,
1125 "challenge-456",
1126 "wss://relay.example.com"
1127 )
1128 .expect("validate auth event"),
1129 expected
1130 );
1131
1132 vault.clear_expired_cache(115);
1133 assert!(vault.contains("main"));
1134 }
1135
1136 #[test]
1137 #[cfg(feature = "encrypted")]
1138 fn encrypted_v2_roundtrip() {
1139 let mut source = Vault::new(VaultConfig::default()).expect("source vault");
1140 let mut dest = Vault::new(VaultConfig::default()).expect("dest vault");
1141 let secret = SecretKey::from_bytes([0x11; 32]).expect("secret");
1142 source
1143 .import_plaintext("main", secret, 100)
1144 .expect("source import");
1145
1146 let exported = source
1147 .export_encrypted("main", b"passphrase")
1148 .expect("export encrypted");
1149 assert_eq!(exported.len(), ENCRYPTED_V2_LEN);
1150 assert_eq!(exported[0], ENCRYPTED_V2_VERSION);
1151
1152 dest.import_encrypted("main", b"passphrase", &exported, 200)
1153 .expect("import encrypted");
1154 assert!(dest.contains("main"));
1155 }
1156
1157 #[test]
1158 #[cfg(all(feature = "encrypted", not(feature = "encrypted-legacy-v1")))]
1159 fn encrypted_v1_payload_rejected_without_legacy_feature() {
1160 let mut dest = Vault::new(VaultConfig::default()).expect("dest vault");
1161 let secret = SecretKey::from_bytes([0x44; 32]).expect("secret");
1162 let exported = legacy_v1_payload(&secret, b"passphrase");
1163
1164 let error = dest
1165 .import_encrypted("main", b"passphrase", &exported, 200)
1166 .expect_err("v1 payload must be rejected by default");
1167 assert!(matches!(
1168 error,
1169 VaultError::InvalidEncrypted("invalid encrypted payload")
1170 ));
1171 }
1172
1173 #[test]
1174 #[cfg(all(feature = "encrypted", feature = "encrypted-legacy-v1"))]
1175 fn encrypted_v1_backward_compat() {
1176 let mut dest = Vault::new(VaultConfig::default()).expect("dest vault");
1177 let secret = SecretKey::from_bytes([0x44; 32]).expect("secret");
1178 let exported = legacy_v1_payload(&secret, b"passphrase");
1179
1180 dest.import_encrypted("main", b"passphrase", &exported, 200)
1181 .expect("import encrypted");
1182 assert!(dest.contains("main"));
1183 assert_eq!(
1184 dest.public_key("main").expect("public key"),
1185 secret.xonly_public_key().expect("expected public key")
1186 );
1187 }
1188
1189 #[test]
1190 #[cfg(feature = "encrypted")]
1191 fn encrypted_wrong_passphrase_fails() {
1192 let mut source = Vault::new(VaultConfig::default()).expect("source vault");
1193 let mut dest = Vault::new(VaultConfig::default()).expect("dest vault");
1194 source
1195 .import_plaintext(
1196 "main",
1197 SecretKey::from_bytes([0x22; 32]).expect("secret"),
1198 100,
1199 )
1200 .expect("source import");
1201 let exported = source
1202 .export_encrypted("main", b"correct")
1203 .expect("export encrypted");
1204
1205 let error = dest
1206 .import_encrypted("main", b"wrong", &exported, 200)
1207 .expect_err("wrong passphrase must fail");
1208 assert!(matches!(
1209 error,
1210 VaultError::InvalidEncrypted("failed to decrypt")
1211 ));
1212 }
1213
1214 #[test]
1215 #[cfg(feature = "encrypted")]
1216 fn encrypted_invalid_data_fails() {
1217 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
1218 let error = vault
1219 .import_encrypted("main", b"passphrase", &[1, 2, 3], 100)
1220 .expect_err("invalid data must fail");
1221 assert!(matches!(
1222 error,
1223 VaultError::InvalidEncrypted("invalid encrypted payload")
1224 ));
1225 }
1226
1227 #[test]
1228 #[cfg(feature = "encrypted")]
1229 fn encrypted_import_sets_active_on_empty_vault() {
1230 let mut source = Vault::new(VaultConfig::default()).expect("source vault");
1231 let mut dest = Vault::new(VaultConfig::default()).expect("dest vault");
1232 source
1233 .import_plaintext(
1234 "main",
1235 SecretKey::from_bytes([0x33; 32]).expect("secret"),
1236 100,
1237 )
1238 .expect("source import");
1239 let exported = source
1240 .export_encrypted("main", b"passphrase")
1241 .expect("export encrypted");
1242
1243 dest.import_encrypted("main", b"passphrase", &exported, 200)
1244 .expect("import encrypted");
1245 assert_eq!(dest.active_label(), Some("main"));
1246 }
1247
1248 #[test]
1249 #[cfg(feature = "encrypted")]
1250 fn encrypted_export_missing_label_fails() {
1251 let vault = Vault::new(VaultConfig::default()).expect("vault");
1252 let error = vault
1253 .export_encrypted("missing", b"passphrase")
1254 .expect_err("missing label must fail");
1255 assert!(matches!(error, VaultError::MissingLabel));
1256 }
1257
1258 #[test]
1259 #[cfg(feature = "nip04")]
1260 fn nip04_roundtrip_between_vaults() {
1261 let mut alice = Vault::new(VaultConfig::default()).expect("alice vault");
1262 let mut bob = Vault::new(VaultConfig::default()).expect("bob vault");
1263 let alice_secret = SecretKey::generate().expect("alice secret");
1264 let bob_secret = SecretKey::generate().expect("bob secret");
1265 let alice_pubkey = alice_secret.xonly_public_key().expect("alice pubkey");
1266 let bob_pubkey = bob_secret.xonly_public_key().expect("bob pubkey");
1267 alice
1268 .import_plaintext("alice", alice_secret, 100)
1269 .expect("alice import");
1270 bob.import_plaintext("bob", bob_secret, 100)
1271 .expect("bob import");
1272
1273 let payload = alice
1274 .nip04_encrypt("alice", &bob_pubkey, "hello", 101)
1275 .expect("encrypt");
1276 let plaintext = bob
1277 .nip04_decrypt("bob", &alice_pubkey, &payload, 102)
1278 .expect("decrypt");
1279 assert_eq!(plaintext, "hello");
1280 }
1281
1282 #[test]
1283 #[cfg(feature = "nip04")]
1284 fn nip04_active_roundtrip_between_vaults() {
1285 let mut alice = Vault::new(VaultConfig::default()).expect("alice vault");
1286 let mut bob = Vault::new(VaultConfig::default()).expect("bob vault");
1287 let alice_secret = SecretKey::generate().expect("alice secret");
1288 let bob_secret = SecretKey::generate().expect("bob secret");
1289 let alice_pubkey = alice_secret.xonly_public_key().expect("alice pubkey");
1290 let bob_pubkey = bob_secret.xonly_public_key().expect("bob pubkey");
1291 alice
1292 .import_plaintext("alice", alice_secret, 100)
1293 .expect("alice import");
1294 bob.import_plaintext("bob", bob_secret, 100)
1295 .expect("bob import");
1296
1297 let payload = alice
1298 .nip04_encrypt_active(&bob_pubkey, "hello", 101)
1299 .expect("encrypt");
1300 let plaintext = bob
1301 .nip04_decrypt_active(&alice_pubkey, &payload, 102)
1302 .expect("decrypt");
1303 assert_eq!(plaintext, "hello");
1304 }
1305
1306 #[test]
1307 #[cfg(feature = "nip04")]
1308 fn nip04_missing_label_fails() {
1309 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
1310 let peer = SecretKey::generate()
1311 .expect("peer secret")
1312 .xonly_public_key()
1313 .expect("peer pubkey");
1314 let error = vault
1315 .nip04_encrypt("missing", &peer, "hello", 100)
1316 .expect_err("missing label must fail");
1317 assert!(matches!(error, VaultError::MissingLabel));
1318 }
1319
1320 #[test]
1321 #[cfg(feature = "nip04")]
1322 fn nip04_active_without_active_fails() {
1323 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
1324 let peer = SecretKey::generate()
1325 .expect("peer secret")
1326 .xonly_public_key()
1327 .expect("peer pubkey");
1328 let error = vault
1329 .nip04_encrypt_active(&peer, "hello", 100)
1330 .expect_err("active label must exist");
1331 assert!(matches!(error, VaultError::NoActiveAccount));
1332 }
1333
1334 #[test]
1335 #[cfg(feature = "nip44")]
1336 fn nip44_roundtrip_between_vaults() {
1337 let mut alice = Vault::new(VaultConfig::default()).expect("alice vault");
1338 let mut bob = Vault::new(VaultConfig::default()).expect("bob vault");
1339 let alice_secret = SecretKey::generate().expect("alice secret");
1340 let bob_secret = SecretKey::generate().expect("bob secret");
1341 let alice_pubkey = alice_secret.xonly_public_key().expect("alice pubkey");
1342 let bob_pubkey = bob_secret.xonly_public_key().expect("bob pubkey");
1343 alice
1344 .import_plaintext("alice", alice_secret, 100)
1345 .expect("alice import");
1346 bob.import_plaintext("bob", bob_secret, 100)
1347 .expect("bob import");
1348
1349 let payload = alice
1350 .nip44_encrypt("alice", &bob_pubkey, "hello", 101)
1351 .expect("encrypt");
1352 let plaintext = bob
1353 .nip44_decrypt("bob", &alice_pubkey, &payload, 102)
1354 .expect("decrypt");
1355 assert_eq!(plaintext, "hello");
1356 }
1357
1358 #[test]
1359 #[cfg(feature = "nip44")]
1360 fn nip44_active_roundtrip_between_vaults() {
1361 let mut alice = Vault::new(VaultConfig::default()).expect("alice vault");
1362 let mut bob = Vault::new(VaultConfig::default()).expect("bob vault");
1363 let alice_secret = SecretKey::generate().expect("alice secret");
1364 let bob_secret = SecretKey::generate().expect("bob secret");
1365 let alice_pubkey = alice_secret.xonly_public_key().expect("alice pubkey");
1366 let bob_pubkey = bob_secret.xonly_public_key().expect("bob pubkey");
1367 alice
1368 .import_plaintext("alice", alice_secret, 100)
1369 .expect("alice import");
1370 bob.import_plaintext("bob", bob_secret, 100)
1371 .expect("bob import");
1372
1373 let payload = alice
1374 .nip44_encrypt_active(&bob_pubkey, "hello", 101)
1375 .expect("encrypt");
1376 let plaintext = bob
1377 .nip44_decrypt_active(&alice_pubkey, &payload, 102)
1378 .expect("decrypt");
1379 assert_eq!(plaintext, "hello");
1380 }
1381
1382 #[test]
1383 #[cfg(feature = "nip44")]
1384 fn nip44_missing_label_fails() {
1385 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
1386 let peer = SecretKey::generate()
1387 .expect("peer secret")
1388 .xonly_public_key()
1389 .expect("peer pubkey");
1390 let error = vault
1391 .nip44_encrypt("missing", &peer, "hello", 100)
1392 .expect_err("missing label must fail");
1393 assert!(matches!(error, VaultError::MissingLabel));
1394 }
1395
1396 #[test]
1397 #[cfg(feature = "nip44")]
1398 fn nip44_active_without_active_fails() {
1399 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
1400 let peer = SecretKey::generate()
1401 .expect("peer secret")
1402 .xonly_public_key()
1403 .expect("peer pubkey");
1404 let error = vault
1405 .nip44_encrypt_active(&peer, "hello", 100)
1406 .expect_err("active label must exist");
1407 assert!(matches!(error, VaultError::NoActiveAccount));
1408 }
1409
1410 #[test]
1411 #[cfg(all(feature = "security-hardening", feature = "nip44"))]
1412 fn security_hardening_preserves_nip44_roundtrip() {
1413 let mut alice = Vault::new(VaultConfig::default()).expect("alice vault");
1414 let mut bob = Vault::new(VaultConfig::default()).expect("bob vault");
1415 let alice_secret = SecretKey::generate().expect("alice secret");
1416 let bob_secret = SecretKey::generate().expect("bob secret");
1417 let alice_pubkey = alice_secret.xonly_public_key().expect("alice pubkey");
1418 let bob_pubkey = bob_secret.xonly_public_key().expect("bob pubkey");
1419 let security = SecurityConfig {
1420 enable_constant_time: true,
1421 enable_random_delay: true,
1422 enable_dummy_operations: true,
1423 };
1424 alice.set_security_config(security);
1425 bob.set_security_config(security);
1426 alice
1427 .import_plaintext("alice", alice_secret, 100)
1428 .expect("alice import");
1429 bob.import_plaintext("bob", bob_secret, 100)
1430 .expect("bob import");
1431
1432 let payload = alice
1433 .nip44_encrypt("alice", &bob_pubkey, "hello", 101)
1434 .expect("encrypt");
1435 let plaintext = bob
1436 .nip44_decrypt("bob", &alice_pubkey, &payload, 102)
1437 .expect("decrypt");
1438 assert_eq!(plaintext, "hello");
1439 }
1440
1441 #[test]
1442 #[cfg(feature = "nip17")]
1443 fn vault_nip17_dm_roundtrip() {
1444 let mut vault_sender = Vault::new(VaultConfig::default()).expect("vault");
1445 let mut vault_recipient = Vault::new(VaultConfig::default()).expect("vault");
1446 let sender_secret = SecretKey::generate().expect("sender");
1447 let recipient_secret = SecretKey::generate().expect("recipient");
1448 vault_sender
1449 .import_plaintext("sender", sender_secret, 100)
1450 .expect("import");
1451 vault_recipient
1452 .import_plaintext("recipient", recipient_secret, 100)
1453 .expect("import");
1454 let recipient_pubkey = vault_recipient.public_key("recipient").expect("pubkey");
1455 let gift_wrap = vault_sender
1456 .create_sealed_dm("sender", "hello via vault", &recipient_pubkey, 101)
1457 .expect("create dm");
1458 assert_eq!(gift_wrap.kind, 1059);
1459 let inner = vault_recipient
1460 .open_gift_wrap_dm("recipient", &gift_wrap, 102)
1461 .expect("open dm");
1462 assert_eq!(inner.kind, 14);
1463 assert_eq!(inner.content, "hello via vault");
1464 }
1465
1466 #[test]
1467 #[cfg(feature = "nip17")]
1468 fn vault_nip17_missing_label_fails() {
1469 let mut vault = Vault::new(VaultConfig::default()).expect("vault");
1470 let peer = SecretKey::generate()
1471 .expect("s")
1472 .xonly_public_key()
1473 .expect("x");
1474 assert!(vault
1475 .create_sealed_dm("missing", "test", &peer, 100)
1476 .is_err());
1477 }
1478}