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

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