Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/store/
password_store.rs

1//! Password-encrypted store wrapper for transparent encryption of any Store type.
2//!
3//! This module provides [`PasswordStore<S>`], a generic decorator that wraps any
4//! [`Store`] type `S` with AES-256-GCM encryption using Argon2id-derived keys.
5//! The wrapped store's type and configuration are stored encrypted in the `_index`
6//! subtree.
7//!
8//! # Encryption Architecture
9//!
10//! Encryption is transparent to the wrapped store. Data flows as:
11//!
12//! ```text
13//! Write: WrappedStore.put() → JSON → encrypt() → base64 → stored in entry
14//! Read:  entry data → base64 decode → decrypt() → JSON → WrappedStore CRDT merge
15//! ```
16//!
17//! The underlying CRDT (e.g., Doc) handles merging of decrypted data from multiple
18//! entry tips. `PasswordStore<S>` delegates `Store::Data` to `S::Data` — encryption
19//! is a transport-level concern, invisible at the type level.
20//!
21//! # Relay Node Support
22//!
23//! Relay nodes without the decryption key can store and forward encrypted entries.
24//! It is unnecessary to decrypt the data before forwarding it to other nodes.
25
26use std::marker::PhantomData;
27use std::sync::{Arc, Mutex};
28
29use aes_gcm::{
30    Aes256Gcm, KeyInit, Nonce,
31    aead::{Aead, AeadCore, OsRng},
32};
33use argon2::{Argon2, Params, password_hash::SaltString};
34use async_trait::async_trait;
35use serde::{Deserialize, Serialize};
36use zeroize::{Zeroize, ZeroizeOnDrop};
37
38use base64ct::{Base64, Encoding};
39
40use crate::{
41    Result, Transaction,
42    crdt::{CRDTError, Doc, doc::Value},
43    store::{Registered, Store, StoreError},
44    transaction::Encryptor,
45};
46
47/// Encrypted data fragment containing ciphertext and nonce.
48///
49/// Used for storing encrypted metadata (e.g., the wrapped store's configuration
50/// in [`PasswordStoreConfig::wrapped_config`]). This is a storage container
51/// with no CRDT semantics.
52#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
53pub struct EncryptedFragment {
54    /// AES-256-GCM encrypted ciphertext.
55    pub ciphertext: Vec<u8>,
56    /// 12-byte nonce for AES-GCM (must be unique per encryption).
57    pub nonce: Vec<u8>,
58}
59
60/// AES-256-GCM nonce size (96 bits / 12 bytes).
61const AES_GCM_NONCE_SIZE: usize = 12;
62
63/// Default Argon2 memory cost in KiB (19 MiB)
64pub const DEFAULT_ARGON2_M_COST: u32 = 19 * 1024;
65/// Default Argon2 time cost (iterations)
66pub const DEFAULT_ARGON2_T_COST: u32 = 2;
67/// Default Argon2 parallelism
68pub const DEFAULT_ARGON2_P_COST: u32 = 1;
69
70/// Encryption metadata stored in _index config (plaintext)
71#[derive(Serialize, Deserialize, Clone, Debug)]
72pub struct EncryptionInfo {
73    /// Encryption algorithm (always "aes-256-gcm" for v1)
74    pub algorithm: String,
75    /// Key derivation function (always "argon2id" for v1)
76    pub kdf: String,
77    /// Base64-encoded salt for Argon2 (16 bytes)
78    pub salt: String,
79    /// Version for future compatibility
80    pub version: String,
81    /// Argon2 memory cost in KiB (defaults to 19 MiB if not specified)
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub argon2_m_cost: Option<u32>,
84    /// Argon2 time cost / iterations (defaults to 2 if not specified)
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    pub argon2_t_cost: Option<u32>,
87    /// Argon2 parallelism (defaults to 1 if not specified)
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub argon2_p_cost: Option<u32>,
90}
91
92/// Configuration stored in _index for PasswordStore.
93#[derive(Serialize, Deserialize, Clone, Debug)]
94pub struct PasswordStoreConfig {
95    /// Encryption parameters (stored in plaintext in _index).
96    pub encryption: EncryptionInfo,
97    /// Encrypted wrapped store metadata.
98    /// Contains the wrapped store's configuration, e.g: {"type": "docstore:v0", "config": "{}"}
99    pub wrapped_config: EncryptedFragment,
100}
101
102impl From<EncryptedFragment> for Doc {
103    fn from(frag: EncryptedFragment) -> Doc {
104        let mut doc = Doc::new();
105        doc.set("ciphertext", Base64::encode_string(&frag.ciphertext));
106        doc.set("nonce", Base64::encode_string(&frag.nonce));
107        doc
108    }
109}
110
111impl TryFrom<&Doc> for EncryptedFragment {
112    type Error = crate::Error;
113
114    fn try_from(doc: &Doc) -> crate::Result<Self> {
115        let bytes = |key: &str| -> crate::Result<Vec<u8>> {
116            let b64 = doc
117                .get_as::<&str>(key)
118                .ok_or_else(|| CRDTError::ElementNotFound {
119                    key: key.to_string(),
120                })?;
121            Base64::decode_vec(b64).map_err(|_| {
122                CRDTError::DeserializationFailed {
123                    reason: format!("{key}: invalid base64"),
124                }
125                .into()
126            })
127        };
128
129        Ok(EncryptedFragment {
130            ciphertext: bytes("ciphertext")?,
131            nonce: bytes("nonce")?,
132        })
133    }
134}
135
136impl From<EncryptionInfo> for Doc {
137    fn from(info: EncryptionInfo) -> Doc {
138        // Functionally this is an atomic Doc, however we can save  small amount of space
139        // by relying on this always being a sub-Doc of the PasswordStoreConfig type.
140        let mut doc = Doc::new();
141        doc.set("algorithm", info.algorithm);
142        doc.set("kdf", info.kdf);
143        doc.set("salt", info.salt);
144        doc.set("version", info.version);
145        if let Some(m) = info.argon2_m_cost {
146            doc.set("argon2_m_cost", m);
147        }
148        if let Some(t) = info.argon2_t_cost {
149            doc.set("argon2_t_cost", t);
150        }
151        if let Some(p) = info.argon2_p_cost {
152            doc.set("argon2_p_cost", p);
153        }
154        doc
155    }
156}
157
158impl TryFrom<&Doc> for EncryptionInfo {
159    type Error = crate::Error;
160
161    fn try_from(doc: &Doc) -> crate::Result<Self> {
162        let text = |key: &str| -> crate::Result<String> {
163            doc.get_as::<&str>(key).map(String::from).ok_or_else(|| {
164                CRDTError::ElementNotFound {
165                    key: key.to_string(),
166                }
167                .into()
168            })
169        };
170
171        let cost = |key: &str| -> crate::Result<Option<u32>> {
172            doc.get_as::<i64>(key)
173                .map(|v| {
174                    u32::try_from(v).map_err(|_| {
175                        crate::Error::from(CRDTError::DeserializationFailed {
176                            reason: format!("{key}: value {v} out of u32 range"),
177                        })
178                    })
179                })
180                .transpose()
181        };
182
183        Ok(EncryptionInfo {
184            algorithm: text("algorithm")?,
185            kdf: text("kdf")?,
186            salt: text("salt")?,
187            version: text("version")?,
188            argon2_m_cost: cost("argon2_m_cost")?,
189            argon2_t_cost: cost("argon2_t_cost")?,
190            argon2_p_cost: cost("argon2_p_cost")?,
191        })
192    }
193}
194
195impl From<PasswordStoreConfig> for Doc {
196    fn from(config: PasswordStoreConfig) -> Doc {
197        // The config always needs to be written atomically to avoid problems with partial updates.
198        let mut doc = Doc::atomic();
199        doc.set("encryption", Value::Doc(config.encryption.into()));
200        doc.set("wrapped_config", Value::Doc(config.wrapped_config.into()));
201        doc
202    }
203}
204
205impl TryFrom<&Doc> for PasswordStoreConfig {
206    type Error = crate::Error;
207
208    fn try_from(doc: &Doc) -> crate::Result<Self> {
209        let sub = |key: &str| -> crate::Result<&Doc> {
210            match doc.get(key) {
211                Some(Value::Doc(d)) => Ok(d),
212                _ => Err(CRDTError::ElementNotFound {
213                    key: key.to_string(),
214                }
215                .into()),
216            }
217        };
218
219        Ok(PasswordStoreConfig {
220            encryption: sub("encryption")?.try_into()?,
221            wrapped_config: sub("wrapped_config")?.try_into()?,
222        })
223    }
224}
225
226impl TryFrom<Doc> for PasswordStoreConfig {
227    type Error = crate::Error;
228
229    fn try_from(doc: Doc) -> crate::Result<Self> {
230        Self::try_from(&doc)
231    }
232}
233
234/// Wrapped store metadata (stored encrypted in config)
235#[derive(Serialize, Deserialize, Clone, Debug)]
236struct WrappedStoreInfo {
237    #[serde(rename = "type")]
238    type_id: String,
239    config: Doc,
240}
241
242/// Internal state of a PasswordStore
243#[derive(Debug, Clone, PartialEq, Eq)]
244enum PasswordStoreState {
245    /// Just created via get_store(), no encryption configured yet
246    Uninitialized,
247    /// Has encryption config, but not yet decrypted for this session
248    Locked,
249    /// Decrypted, ready to use the wrapped store
250    Unlocked,
251}
252
253/// Securely stored password with automatic zeroization
254#[derive(Clone, Zeroize, ZeroizeOnDrop)]
255struct Password {
256    salt: String,
257    password: String,
258    /// Argon2 memory cost in KiB
259    argon2_m_cost: u32,
260    /// Argon2 time cost
261    argon2_t_cost: u32,
262    /// Argon2 parallelism
263    argon2_p_cost: u32,
264}
265
266/// Wrapper for derived key with automatic zeroization
267#[derive(ZeroizeOnDrop)]
268struct DerivedKey {
269    key: Option<Vec<u8>>,
270}
271
272impl DerivedKey {
273    fn new() -> Self {
274        Self { key: None }
275    }
276
277    fn set(&mut self, key: Vec<u8>) {
278        self.key = Some(key);
279    }
280
281    fn get(&self) -> Option<&Vec<u8>> {
282        self.key.as_ref()
283    }
284}
285
286/// Password-based encryptor implementing the Encryptor trait
287///
288/// Provides AES-256-GCM encryption with Argon2id key derivation.
289/// Caches the derived key to avoid expensive re-derivation on every operation.
290struct PasswordEncryptor {
291    password: Password,
292    subtree_name: String,
293    /// Cached derived key (zeroized on drop, thread-safe)
294    derived_key: Arc<Mutex<DerivedKey>>,
295}
296
297impl PasswordEncryptor {
298    /// Create a new PasswordEncryptor
299    fn new(password: Password, subtree_name: String) -> Self {
300        Self {
301            password,
302            subtree_name,
303            derived_key: Arc::new(Mutex::new(DerivedKey::new())),
304        }
305    }
306
307    /// Execute a function with access to the encryption key (with caching)
308    ///
309    /// Provides a reference to the key without cloning it. This avoids
310    /// leaving unzeroized copies of the key in memory. The lock is held
311    /// during key derivation to prevent concurrent derivation races.
312    fn with_key<F, R>(&self, f: F) -> Result<R>
313    where
314        F: FnOnce(&[u8]) -> Result<R>,
315    {
316        let mut guard = self.derived_key.lock().unwrap();
317
318        // Check if key is already cached
319        if let Some(key) = guard.get() {
320            return f(key);
321        }
322
323        // Derive the key (expensive Argon2 operation, but only done once)
324        let mut key = vec![0u8; 32];
325        let salt = SaltString::from_b64(&self.password.salt).map_err(|e| {
326            StoreError::ImplementationError {
327                store: self.subtree_name.clone(),
328                reason: format!("Invalid salt: {e}"),
329            }
330        })?;
331
332        // Build Argon2 with configured parameters
333        let params = Params::new(
334            self.password.argon2_m_cost,
335            self.password.argon2_t_cost,
336            self.password.argon2_p_cost,
337            Some(32), // output length
338        )
339        .map_err(|e| StoreError::ImplementationError {
340            store: self.subtree_name.clone(),
341            reason: format!("Invalid Argon2 parameters: {e}"),
342        })?;
343
344        let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
345
346        argon2
347            .hash_password_into(
348                self.password.password.as_bytes(),
349                salt.as_str().as_bytes(),
350                &mut key,
351            )
352            .map_err(|e| StoreError::ImplementationError {
353                store: self.subtree_name.clone(),
354                reason: format!("Key derivation failed: {e}"),
355            })?;
356
357        // Cache the key for future use
358        guard.set(key);
359
360        // Execute function with the derived key (guard still held)
361        f(guard.get().unwrap())
362    }
363}
364
365impl Encryptor for PasswordEncryptor {
366    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
367        // Wire format: nonce (12 bytes) || ciphertext
368        if ciphertext.len() < AES_GCM_NONCE_SIZE {
369            return Err(StoreError::DeserializationFailed {
370                store: self.subtree_name.clone(),
371                reason: format!(
372                    "Ciphertext too short: expected at least {} bytes, got {}",
373                    AES_GCM_NONCE_SIZE,
374                    ciphertext.len()
375                ),
376            }
377            .into());
378        }
379
380        let (nonce_bytes, encrypted_data) = ciphertext.split_at(AES_GCM_NONCE_SIZE);
381
382        // Use the encryption key without cloning
383        self.with_key(|encryption_key| {
384            // Create cipher
385            let cipher = Aes256Gcm::new_from_slice(encryption_key).map_err(|e| {
386                StoreError::ImplementationError {
387                    store: self.subtree_name.clone(),
388                    reason: format!("Failed to create cipher: {e}"),
389                }
390            })?;
391
392            // Decrypt
393            let nonce = Nonce::from_slice(nonce_bytes);
394            cipher.decrypt(nonce, encrypted_data).map_err(|_| {
395                StoreError::ImplementationError {
396                    store: self.subtree_name.clone(),
397                    reason: "Decryption failed".to_string(),
398                }
399                .into()
400            })
401        })
402    }
403
404    fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
405        // Use the encryption key without cloning
406        self.with_key(|encryption_key| {
407            // Create cipher
408            let cipher = Aes256Gcm::new_from_slice(encryption_key).map_err(|e| {
409                StoreError::ImplementationError {
410                    store: self.subtree_name.clone(),
411                    reason: format!("Failed to create cipher: {e}"),
412                }
413            })?;
414
415            // Generate random nonce
416            let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
417
418            // Encrypt
419            let ciphertext =
420                cipher
421                    .encrypt(&nonce, plaintext)
422                    .map_err(|e| StoreError::ImplementationError {
423                        store: self.subtree_name.clone(),
424                        reason: format!("Encryption failed: {e}"),
425                    })?;
426
427            // Wire format: nonce (12 bytes) || ciphertext
428            let mut result = nonce.to_vec();
429            result.extend(ciphertext);
430            Ok(result)
431        })
432    }
433}
434
435/// Password-encrypted store wrapper.
436///
437/// Wraps any [`Store`] type with transparent AES-256-GCM encryption using
438/// password-derived keys (Argon2id). The type parameter `S` specifies the
439/// wrapped store type, and `PasswordStore<S>` delegates `Store::Data` to
440/// `S::Data` — encryption is a transport-level concern invisible at the type level.
441///
442/// # Type Parameter
443///
444/// * `S` - The wrapped store type (e.g., `DocStore`, `Table<T>`)
445///
446/// # State Machine
447///
448/// PasswordStore has three states (derived from internal fields):
449///
450/// 1. **Uninitialized** - Created via `get_store()`, no encryption configured
451/// 2. **Locked** - Has encryption config, not yet decrypted
452/// 3. **Unlocked** - Decrypted and ready to use
453///
454/// State transitions:
455/// - `get_store()` → Uninitialized (new) or Locked (existing)
456/// - `initialize()` → Unlocked (from Uninitialized only)
457/// - `open()` → Unlocked (from Locked only)
458///
459/// # Security
460///
461/// - **Encryption**: AES-256-GCM authenticated encryption
462/// - **Key Derivation**: Argon2id memory-hard password hashing
463/// - **Nonces**: Unique random nonce per encryption operation
464/// - **Zeroization**: Passwords cleared from memory on drop
465///
466/// # Limitations
467///
468/// - **Password Loss**: Losing the password means permanent data loss
469/// - **Performance**: Encryption/decryption overhead on every operation
470///
471/// # Examples
472///
473/// Creating a new encrypted store:
474///
475/// ```rust,no_run
476/// # use eidetica::{Instance, backend::database::InMemory, crdt::Doc, Database};
477/// # use eidetica::store::{PasswordStore, DocStore};
478/// # use eidetica::auth::generate_keypair;
479/// # async fn example() -> eidetica::Result<()> {
480/// # let backend = InMemory::new();
481/// # let instance = Instance::open(Box::new(backend)).await?;
482/// # let (private_key, _) = generate_keypair();
483/// # let db = Database::create(&instance, private_key, Doc::new()).await?;
484/// let tx = db.new_transaction().await?;
485/// let mut encrypted = tx.get_store::<PasswordStore<DocStore>>("secrets").await?;
486/// encrypted.initialize("my_password", Doc::new()).await?;
487///
488/// let docstore = encrypted.inner().await?;
489/// docstore.set("key", "secret value").await?;
490/// tx.commit().await?;
491/// # Ok(())
492/// # }
493/// ```
494///
495/// Opening an existing encrypted store:
496///
497/// ```rust,no_run
498/// # use eidetica::{Instance, backend::database::InMemory, crdt::Doc, Database};
499/// # use eidetica::store::{PasswordStore, DocStore};
500/// # use eidetica::auth::generate_keypair;
501/// # async fn example() -> eidetica::Result<()> {
502/// # let backend = InMemory::new();
503/// # let instance = Instance::open(Box::new(backend)).await?;
504/// # let (private_key, _) = generate_keypair();
505/// # let db = Database::create(&instance, private_key, Doc::new()).await?;
506/// let tx = db.new_transaction().await?;
507/// let mut store = tx.get_store::<PasswordStore<DocStore>>("secrets").await?;
508/// store.open("my_password")?;
509///
510/// let docstore = store.inner().await?;
511/// let value = docstore.get("key").await?;
512/// # Ok(())
513/// # }
514/// ```
515pub struct PasswordStore<S: Store> {
516    /// Subtree name
517    name: String,
518    /// Transaction reference
519    transaction: Transaction,
520    /// Encryption configuration (None if uninitialized)
521    config: Option<PasswordStoreConfig>,
522    /// Cached password (zeroized on drop)
523    cached_password: Option<Password>,
524    /// Decrypted wrapped store info (only available after open())
525    wrapped_info: Option<WrappedStoreInfo>,
526    /// Phantom type for the wrapped store
527    _phantom: PhantomData<S>,
528}
529
530impl<S: Store> PasswordStore<S> {
531    /// Derive the current state from internal fields
532    fn state(&self) -> PasswordStoreState {
533        match (&self.config, &self.cached_password) {
534            (None, _) => PasswordStoreState::Uninitialized,
535            (Some(_), None) => PasswordStoreState::Locked,
536            (Some(_), Some(_)) => PasswordStoreState::Unlocked,
537        }
538    }
539}
540
541impl<S: Store> Registered for PasswordStore<S> {
542    fn type_id() -> &'static str {
543        // Explicitly use v0 to indicate instability
544        "encrypted:password:v0"
545    }
546}
547
548#[async_trait]
549impl<S: Store> Store for PasswordStore<S> {
550    type Data = S::Data;
551
552    async fn new(txn: &Transaction, subtree_name: String) -> Result<Self> {
553        // Try to load config from _index to determine state
554        let index_store = txn.get_index().await?;
555        let info = index_store.get_entry(&subtree_name).await?;
556
557        // Type validation
558        if !Self::supports_type_id(&info.type_id) {
559            return Err(StoreError::TypeMismatch {
560                store: subtree_name,
561                expected: Self::type_id().to_string(),
562                actual: info.type_id,
563            }
564            .into());
565        }
566
567        // Determine state based on config content
568        // Empty Doc means uninitialized, non-empty means locked
569        if info.config.is_empty() {
570            Ok(Self {
571                name: subtree_name,
572                transaction: txn.clone(),
573                config: None,
574                cached_password: None,
575                wrapped_info: None,
576                _phantom: PhantomData,
577            })
578        } else {
579            // Parse the config from the Doc
580            let config: PasswordStoreConfig =
581                info.config.try_into().map_err(|e: crate::Error| {
582                    StoreError::DeserializationFailed {
583                        store: subtree_name.clone(),
584                        reason: format!("Failed to parse PasswordStoreConfig: {e}"),
585                    }
586                })?;
587
588            // Validate encryption parameters
589            if config.encryption.algorithm != "aes-256-gcm" {
590                return Err(StoreError::InvalidConfiguration {
591                    store: subtree_name,
592                    reason: format!(
593                        "Unsupported encryption algorithm: {}",
594                        config.encryption.algorithm
595                    ),
596                }
597                .into());
598            }
599
600            if config.encryption.kdf != "argon2id" {
601                return Err(StoreError::InvalidConfiguration {
602                    store: subtree_name,
603                    reason: format!("Unsupported KDF: {}", config.encryption.kdf),
604                }
605                .into());
606            }
607
608            Ok(Self {
609                name: subtree_name,
610                transaction: txn.clone(),
611                config: Some(config),
612                cached_password: None,
613                wrapped_info: None,
614                _phantom: PhantomData,
615            })
616        }
617    }
618
619    async fn init(txn: &Transaction, subtree_name: String) -> Result<Self> {
620        // Register in _index with empty config (marks as uninitialized)
621        let index_store = txn.get_index().await?;
622        index_store
623            .set_entry(&subtree_name, Self::type_id(), Self::default_config())
624            .await?;
625
626        Ok(Self {
627            name: subtree_name,
628            transaction: txn.clone(),
629            config: None,
630            cached_password: None,
631            wrapped_info: None,
632            _phantom: PhantomData,
633        })
634    }
635
636    fn name(&self) -> &str {
637        &self.name
638    }
639
640    fn transaction(&self) -> &Transaction {
641        &self.transaction
642    }
643}
644
645impl<S: Store> PasswordStore<S> {
646    /// Initialize encryption on an uninitialized store
647    ///
648    /// This configures encryption for a PasswordStore that was obtained via
649    /// `get_store()`. The wrapped store's type (derived from `S`) and config
650    /// are encrypted and stored in the PasswordStore's configuration in `_index`.
651    ///
652    /// After calling this method, the store transitions to the Unlocked state
653    /// and is ready to use.
654    ///
655    /// # Arguments
656    /// * `password` - Password for encryption (will be zeroized after use)
657    /// * `wrapped_config` - Configuration for wrapped store
658    ///
659    /// # Returns
660    /// Ok(()) on success, the store is now unlocked
661    ///
662    /// # Errors
663    /// - Returns error if store is not in Uninitialized state
664    /// - Returns error if encryption fails
665    ///
666    /// # Examples
667    ///
668    /// ```rust,no_run
669    /// # use eidetica::{Instance, backend::database::InMemory, crdt::Doc, Database};
670    /// # use eidetica::store::{PasswordStore, DocStore};
671    /// # use eidetica::auth::generate_keypair;
672    /// # async fn example() -> eidetica::Result<()> {
673    /// # let backend = InMemory::new();
674    /// # let instance = Instance::open(Box::new(backend)).await?;
675    /// # let (private_key, _) = generate_keypair();
676    /// # let db = Database::create(&instance, private_key, Doc::new()).await?;
677    /// let tx = db.new_transaction().await?;
678    /// let mut encrypted = tx.get_store::<PasswordStore<DocStore>>("secrets").await?;
679    /// encrypted.initialize("my_password", Doc::new()).await?;
680    ///
681    /// let docstore = encrypted.inner().await?;
682    /// docstore.set("key", "secret value").await?;
683    /// tx.commit().await?;
684    /// # Ok(())
685    /// # }
686    /// ```
687    pub async fn initialize(
688        &mut self,
689        password: impl Into<String>,
690        wrapped_config: Doc,
691    ) -> Result<()> {
692        // Check state is Uninitialized
693        if self.state() != PasswordStoreState::Uninitialized {
694            return Err(StoreError::InvalidOperation {
695                store: self.name.clone(),
696                operation: "initialize".to_string(),
697                reason: "Store is already initialized - use open() instead".to_string(),
698            }
699            .into());
700        }
701
702        let password = password.into();
703        let wrapped_type_id = S::type_id().to_string();
704
705        // Use default Argon2 parameters
706        let argon2_m_cost = DEFAULT_ARGON2_M_COST;
707        let argon2_t_cost = DEFAULT_ARGON2_T_COST;
708        let argon2_p_cost = DEFAULT_ARGON2_P_COST;
709
710        // Generate encryption parameters
711        let salt = SaltString::generate(&mut OsRng);
712        let salt_str = salt.as_str().to_string();
713
714        // Build Argon2 with configured parameters
715        let params =
716            Params::new(argon2_m_cost, argon2_t_cost, argon2_p_cost, Some(32)).map_err(|e| {
717                StoreError::ImplementationError {
718                    store: self.name.clone(),
719                    reason: format!("Invalid Argon2 parameters: {e}"),
720                }
721            })?;
722        let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
723
724        // Derive encryption key from password
725        let mut encryption_key = vec![0u8; 32];
726        argon2
727            .hash_password_into(
728                password.as_bytes(),
729                salt.as_str().as_bytes(),
730                &mut encryption_key,
731            )
732            .map_err(|e| StoreError::ImplementationError {
733                store: self.name.clone(),
734                reason: format!("Failed to derive encryption key: {e}"),
735            })?;
736
737        // Create cipher
738        let cipher = Aes256Gcm::new_from_slice(&encryption_key).map_err(|e| {
739            StoreError::ImplementationError {
740                store: self.name.clone(),
741                reason: format!("Failed to create cipher: {e}"),
742            }
743        })?;
744
745        // Encrypt wrapped store metadata
746        let wrapped_info = WrappedStoreInfo {
747            type_id: wrapped_type_id,
748            config: wrapped_config,
749        };
750        let wrapped_json = serde_json::to_string(&wrapped_info)?;
751        let config_nonce = Aes256Gcm::generate_nonce(&mut OsRng);
752        let wrapped_config_ciphertext = cipher
753            .encrypt(&config_nonce, wrapped_json.as_bytes())
754            .map_err(|e| StoreError::ImplementationError {
755                store: self.name.clone(),
756                reason: format!("Failed to encrypt wrapped config: {e}"),
757            })?;
758
759        // Zeroize the encryption key
760        encryption_key.zeroize();
761
762        // Create configuration
763        let config = PasswordStoreConfig {
764            encryption: EncryptionInfo {
765                algorithm: "aes-256-gcm".to_string(),
766                kdf: "argon2id".to_string(),
767                salt: salt_str.clone(),
768                version: "v0".to_string(),
769                argon2_m_cost: Some(argon2_m_cost),
770                argon2_t_cost: Some(argon2_t_cost),
771                argon2_p_cost: Some(argon2_p_cost),
772            },
773            wrapped_config: EncryptedFragment {
774                ciphertext: wrapped_config_ciphertext,
775                nonce: config_nonce.to_vec(),
776            },
777        };
778
779        // Update _index with the encryption config
780        self.set_config(config.clone().into()).await?;
781
782        // Cache password and create encryptor
783        let password_cache = Password {
784            salt: salt_str,
785            password,
786            argon2_m_cost,
787            argon2_t_cost,
788            argon2_p_cost,
789        };
790
791        // Register encryptor with transaction (store is now unlocked)
792        let encryptor = Box::new(PasswordEncryptor::new(
793            password_cache.clone(),
794            self.name.clone(),
795        ));
796        self.transaction.register_encryptor(&self.name, encryptor)?;
797
798        // Update internal state
799        self.config = Some(config);
800        self.cached_password = Some(password_cache);
801        self.wrapped_info = Some(wrapped_info);
802
803        Ok(())
804    }
805
806    /// Open (unlock) the encrypted store with a password
807    ///
808    /// This decrypts the wrapped store configuration and caches the password
809    /// for subsequent encrypt/decrypt operations.
810    ///
811    /// # Arguments
812    /// * `password` - Password to decrypt the store
813    ///
814    /// # Returns
815    /// Ok(()) if password is correct, Err otherwise
816    ///
817    /// # Errors
818    /// - Returns error if store is Uninitialized (use `initialize()` first)
819    /// - Returns error if store is already Unlocked
820    /// - Returns error if password is incorrect
821    ///
822    /// # Security
823    /// The password is cached in memory (with zeroization on drop) for
824    /// convenience.
825    pub fn open(&mut self, password: impl Into<String>) -> Result<()> {
826        // Check state
827        match self.state() {
828            PasswordStoreState::Uninitialized => {
829                return Err(StoreError::InvalidOperation {
830                    store: self.name.clone(),
831                    operation: "open".to_string(),
832                    reason: "Store is not initialized - call initialize() first".to_string(),
833                }
834                .into());
835            }
836            PasswordStoreState::Unlocked => {
837                return Err(StoreError::InvalidOperation {
838                    store: self.name.clone(),
839                    operation: "open".to_string(),
840                    reason: "Store is already open".to_string(),
841                }
842                .into());
843            }
844            PasswordStoreState::Locked => {}
845        }
846
847        let config = self.config.as_ref().expect("Locked state requires config");
848        let password = password.into();
849
850        // Get Argon2 parameters from config (with defaults)
851        let argon2_m_cost = config
852            .encryption
853            .argon2_m_cost
854            .unwrap_or(DEFAULT_ARGON2_M_COST);
855        let argon2_t_cost = config
856            .encryption
857            .argon2_t_cost
858            .unwrap_or(DEFAULT_ARGON2_T_COST);
859        let argon2_p_cost = config
860            .encryption
861            .argon2_p_cost
862            .unwrap_or(DEFAULT_ARGON2_P_COST);
863
864        // Derive encryption key
865        let mut encryption_key = vec![0u8; 32];
866        let salt = SaltString::from_b64(&config.encryption.salt).map_err(|e| {
867            StoreError::ImplementationError {
868                store: self.name.clone(),
869                reason: format!("Invalid salt in config: {e}"),
870            }
871        })?;
872
873        // Build Argon2 with configured parameters
874        let params =
875            Params::new(argon2_m_cost, argon2_t_cost, argon2_p_cost, Some(32)).map_err(|e| {
876                StoreError::ImplementationError {
877                    store: self.name.clone(),
878                    reason: format!("Invalid Argon2 parameters: {e}"),
879                }
880            })?;
881        let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
882
883        argon2
884            .hash_password_into(
885                password.as_bytes(),
886                salt.as_str().as_bytes(),
887                &mut encryption_key,
888            )
889            .map_err(|e| StoreError::ImplementationError {
890                store: self.name.clone(),
891                reason: format!("Failed to derive encryption key: {e}"),
892            })?;
893
894        // Decrypt wrapped config
895        let cipher = Aes256Gcm::new_from_slice(&encryption_key).map_err(|e| {
896            StoreError::ImplementationError {
897                store: self.name.clone(),
898                reason: format!("Failed to create cipher: {e}"),
899            }
900        })?;
901
902        // Validate nonce length (must be 12 bytes for AES-GCM)
903        if config.wrapped_config.nonce.len() != 12 {
904            return Err(StoreError::InvalidConfiguration {
905                store: self.name.clone(),
906                reason: format!(
907                    "Invalid nonce length: expected 12 bytes, got {}",
908                    config.wrapped_config.nonce.len()
909                ),
910            }
911            .into());
912        }
913        let config_nonce = Nonce::from_slice(&config.wrapped_config.nonce);
914
915        let decrypted_config = cipher
916            .decrypt(config_nonce, config.wrapped_config.ciphertext.as_slice())
917            .map_err(|_| StoreError::ImplementationError {
918                store: self.name.clone(),
919                reason: "Failed to decrypt wrapped config - incorrect password?".to_string(),
920            })?;
921
922        // Zeroize encryption key
923        encryption_key.zeroize();
924
925        // Parse wrapped store info
926        let wrapped_info: WrappedStoreInfo =
927            serde_json::from_slice(&decrypted_config).map_err(|e| {
928                StoreError::DeserializationFailed {
929                    store: self.name.clone(),
930                    reason: format!("Failed to parse wrapped store info: {e}"),
931                }
932            })?;
933
934        // Verify the stored wrapped type matches the static type parameter S
935        if !S::supports_type_id(&wrapped_info.type_id) {
936            return Err(StoreError::TypeMismatch {
937                store: self.name.clone(),
938                expected: S::type_id().to_string(),
939                actual: wrapped_info.type_id,
940            }
941            .into());
942        }
943
944        // Cache password and wrapped info (state is derived from these fields)
945        let password_cache = Password {
946            salt: config.encryption.salt.clone(),
947            password,
948            argon2_m_cost,
949            argon2_t_cost,
950            argon2_p_cost,
951        };
952        self.cached_password = Some(password_cache.clone());
953        self.wrapped_info = Some(wrapped_info);
954
955        // Register encryptor with the transaction for transparent encryption
956        let encryptor = Box::new(PasswordEncryptor::new(password_cache, self.name.clone()));
957        self.transaction.register_encryptor(&self.name, encryptor)?;
958
959        Ok(())
960    }
961
962    /// Check if the store is currently unlocked (password cached)
963    pub fn is_open(&self) -> bool {
964        self.state() == PasswordStoreState::Unlocked
965    }
966
967    /// Check if the store is initialized (has encryption configuration)
968    pub fn is_initialized(&self) -> bool {
969        self.state() != PasswordStoreState::Uninitialized
970    }
971
972    /// Get the wrapped store, providing transparent encryption.
973    ///
974    /// Returns the inner `S` store instance that transparently encrypts data
975    /// on write and decrypts on read. The wrapped store is unaware of
976    /// encryption — all crypto operations are handled by an encryptor
977    /// registered with the transaction during `open()` or `initialize()`.
978    ///
979    /// # Errors
980    /// - Returns error if store is not opened (call `open()` first)
981    ///
982    /// # Examples
983    ///
984    /// ```rust,no_run
985    /// # use eidetica::{Instance, backend::database::InMemory, crdt::Doc, Database};
986    /// # use eidetica::store::{PasswordStore, DocStore};
987    /// # use eidetica::auth::generate_keypair;
988    /// # async fn example() -> eidetica::Result<()> {
989    /// # let backend = InMemory::new();
990    /// # let instance = Instance::open(Box::new(backend)).await?;
991    /// # let (private_key, _) = generate_keypair();
992    /// # let db = Database::create(&instance, private_key, Doc::new()).await?;
993    /// # let tx = db.new_transaction().await?;
994    /// # let mut encrypted = tx.get_store::<PasswordStore<DocStore>>("test").await?;
995    /// # encrypted.initialize("pass", Doc::new()).await?;
996    /// # tx.commit().await?;
997    /// # let tx2 = db.new_transaction().await?;
998    /// let mut encrypted = tx2.get_store::<PasswordStore<DocStore>>("test").await?;
999    /// encrypted.open("pass")?;
1000    ///
1001    /// let docstore = encrypted.inner().await?;
1002    /// docstore.set("key", "value").await?; // Automatically encrypted
1003    /// # Ok(())
1004    /// # }
1005    /// ```
1006    pub async fn inner(&self) -> Result<S> {
1007        if !self.is_open() {
1008            return Err(StoreError::InvalidOperation {
1009                store: self.name.clone(),
1010                operation: "inner".to_string(),
1011                reason: "Store not opened - call open() first".to_string(),
1012            }
1013            .into());
1014        }
1015
1016        // Create the wrapped store. The transaction has an encryptor registered,
1017        // so it transparently decrypts on read and encrypts on commit.
1018        // We call S::new() directly, bypassing Transaction::get_store() type
1019        // checking, since type consistency was already verified in open().
1020        S::new(&self.transaction, self.name.clone()).await
1021    }
1022}