Skip to main content

neco_vault/
lib.rs

1//! Memory-only signing vault built on `neco-secp`.
2
3use 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, &params, &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], &params, &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        // vault と core 経路はどちらも BIP-340 推奨のランダム aux_rand で
1059        // 署名するため、sig のバイト列は呼び出しごとに異なる。よって等価性は
1060        // 「id/pubkey/created_at/kind/tags/content が一致し、両方とも verify OK」
1061        // という意味不変量で検証する。
1062        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}