1use std::marker::PhantomData;
30use std::sync::{Arc, Mutex};
31
32use aes_gcm::{
33 Aes256Gcm, KeyInit, Nonce,
34 aead::{Aead, AeadCore, OsRng, Payload},
35};
36use argon2::{Argon2, Params, password_hash::SaltString};
37use async_trait::async_trait;
38use serde::{Deserialize, Serialize};
39use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
40
41use base64ct::{Base64, Encoding};
42
43use crate::{
44 Result, Transaction,
45 crdt::{CRDTError, Doc, doc::Value},
46 store::{ProjectionDescriptor, Registered, Store, StoreError, StoreStateModel},
47 transaction::Encryptor,
48};
49
50#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
56pub struct EncryptedFragment {
57 pub ciphertext: Vec<u8>,
59 pub nonce: Vec<u8>,
61}
62
63const AES_GCM_NONCE_SIZE: usize = 12;
65
66pub const DEFAULT_ARGON2_M_COST: u32 = 19 * 1024;
68pub const DEFAULT_ARGON2_T_COST: u32 = 2;
70pub const DEFAULT_ARGON2_P_COST: u32 = 1;
72
73#[derive(Serialize, Deserialize, Clone, Debug)]
75pub struct EncryptionInfo {
76 pub algorithm: String,
78 pub kdf: String,
80 pub salt: String,
82 pub version: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub argon2_m_cost: Option<u32>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub argon2_t_cost: Option<u32>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub argon2_p_cost: Option<u32>,
93}
94
95#[derive(Serialize, Deserialize, Clone, Debug)]
97pub struct PasswordStoreConfig {
98 pub encryption: EncryptionInfo,
100 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 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 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#[derive(Serialize, Deserialize, Clone, Debug)]
239struct WrappedStoreInfo {
240 #[serde(rename = "type")]
241 type_id: String,
242 config: Doc,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
247enum PasswordStoreState {
248 Uninitialized,
250 Locked,
252 Unlocked,
254}
255
256#[derive(Clone, Zeroize, ZeroizeOnDrop)]
258struct Password {
259 salt: String,
260 password: String,
261 argon2_m_cost: u32,
263 argon2_t_cost: u32,
265 argon2_p_cost: u32,
267}
268
269#[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
289struct PasswordEncryptor {
294 password: Password,
295 subtree_name: String,
296 store_identity: Vec<u8>,
297 derived_key: Arc<Mutex<DerivedKey>>,
299}
300
301#[derive(Serialize, Deserialize)]
302struct EncryptedRecordEnvelope<'a> {
303 key: &'a [u8],
304 value: &'a [u8],
305}
306
307#[derive(Deserialize)]
308struct DecryptedRecordEnvelope {
309 key: Vec<u8>,
310 value: Vec<u8>,
311}
312
313impl PasswordEncryptor {
314 fn new(password: Password, subtree_name: String, database_id: &[u8]) -> Self {
316 let mut store_identity = database_id.to_vec();
317 store_identity.push(0);
318 store_identity.extend_from_slice(subtree_name.as_bytes());
319 Self {
320 password,
321 subtree_name,
322 store_identity,
323 derived_key: Arc::new(Mutex::new(DerivedKey::new())),
324 }
325 }
326
327 fn with_key<F, R>(&self, f: F) -> Result<R>
333 where
334 F: FnOnce(&[u8]) -> Result<R>,
335 {
336 let mut guard = self.derived_key.lock().unwrap();
337
338 if let Some(key) = guard.get() {
340 return f(key);
341 }
342
343 let mut key = vec![0u8; 32];
345 let salt = SaltString::from_b64(&self.password.salt).map_err(|e| {
346 StoreError::ImplementationError {
347 store: self.subtree_name.clone(),
348 reason: format!("Invalid salt: {e}"),
349 }
350 })?;
351
352 let params = Params::new(
354 self.password.argon2_m_cost,
355 self.password.argon2_t_cost,
356 self.password.argon2_p_cost,
357 Some(32), )
359 .map_err(|e| StoreError::ImplementationError {
360 store: self.subtree_name.clone(),
361 reason: format!("Invalid Argon2 parameters: {e}"),
362 })?;
363
364 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
365
366 argon2
367 .hash_password_into(
368 self.password.password.as_bytes(),
369 salt.as_str().as_bytes(),
370 &mut key,
371 )
372 .map_err(|e| StoreError::ImplementationError {
373 store: self.subtree_name.clone(),
374 reason: format!("Key derivation failed: {e}"),
375 })?;
376
377 guard.set(key);
379
380 f(guard.get().unwrap())
382 }
383
384 fn record_key_material(&self) -> Result<Zeroizing<[u8; 32]>> {
385 self.with_key(|master| {
386 Ok(Zeroizing::new(blake3::derive_key(
387 "eidetica/password-store/record-key/v1",
388 master,
389 )))
390 })
391 }
392
393 fn record_value_material(&self) -> Result<Zeroizing<[u8; 32]>> {
394 self.with_key(|master| {
395 Ok(Zeroizing::new(blake3::derive_key(
396 "eidetica/password-store/record-value/v1",
397 master,
398 )))
399 })
400 }
401}
402
403impl Encryptor for PasswordEncryptor {
404 fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
405 if ciphertext.len() < AES_GCM_NONCE_SIZE {
407 return Err(StoreError::DeserializationFailed {
408 store: self.subtree_name.clone(),
409 reason: format!(
410 "Ciphertext too short: expected at least {} bytes, got {}",
411 AES_GCM_NONCE_SIZE,
412 ciphertext.len()
413 ),
414 }
415 .into());
416 }
417
418 let (nonce_bytes, encrypted_data) = ciphertext.split_at(AES_GCM_NONCE_SIZE);
419
420 self.with_key(|encryption_key| {
422 let cipher = Aes256Gcm::new_from_slice(encryption_key).map_err(|e| {
424 StoreError::ImplementationError {
425 store: self.subtree_name.clone(),
426 reason: format!("Failed to create cipher: {e}"),
427 }
428 })?;
429
430 let nonce = Nonce::from_slice(nonce_bytes);
432 cipher.decrypt(nonce, encrypted_data).map_err(|_| {
433 StoreError::ImplementationError {
434 store: self.subtree_name.clone(),
435 reason: "Decryption failed".to_string(),
436 }
437 .into()
438 })
439 })
440 }
441
442 fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
443 self.with_key(|encryption_key| {
445 let cipher = Aes256Gcm::new_from_slice(encryption_key).map_err(|e| {
447 StoreError::ImplementationError {
448 store: self.subtree_name.clone(),
449 reason: format!("Failed to create cipher: {e}"),
450 }
451 })?;
452
453 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
455
456 let ciphertext =
458 cipher
459 .encrypt(&nonce, plaintext)
460 .map_err(|e| StoreError::ImplementationError {
461 store: self.subtree_name.clone(),
462 reason: format!("Encryption failed: {e}"),
463 })?;
464
465 let mut result = nonce.to_vec();
467 result.extend(ciphertext);
468 Ok(result)
469 })
470 }
471
472 fn physical_record_key(&self, logical_key: &[u8]) -> Result<Vec<u8>> {
473 let key = self.record_key_material()?;
474 Ok(blake3::keyed_hash(&key, logical_key).as_bytes().to_vec())
475 }
476
477 fn encrypt_record(&self, logical_key: &[u8], plaintext: &[u8]) -> Result<Vec<u8>> {
478 let key = self.record_value_material()?;
479 let cipher = Aes256Gcm::new_from_slice(key.as_ref()).map_err(|error| {
480 StoreError::ImplementationError {
481 store: self.subtree_name.clone(),
482 reason: format!("Failed to create record cipher: {error}"),
483 }
484 })?;
485 let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
486 let physical_key = self.physical_record_key(logical_key)?;
487 let aad = physical_record_aad(&self.store_identity, &physical_key);
488 let plaintext = serde_json::to_vec(&EncryptedRecordEnvelope {
489 key: logical_key,
490 value: plaintext,
491 })?;
492 let ciphertext = cipher
493 .encrypt(
494 &nonce,
495 Payload {
496 msg: &plaintext,
497 aad: &aad,
498 },
499 )
500 .map_err(|error| StoreError::ImplementationError {
501 store: self.subtree_name.clone(),
502 reason: format!("Record encryption failed: {error}"),
503 })?;
504 let mut result = nonce.to_vec();
505 result.extend(ciphertext);
506 Ok(result)
507 }
508
509 fn decrypt_record(&self, physical_key: &[u8], ciphertext: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
510 if ciphertext.len() < AES_GCM_NONCE_SIZE {
511 return Err(StoreError::DeserializationFailed {
512 store: self.subtree_name.clone(),
513 reason: "Encrypted record is shorter than its nonce".to_string(),
514 }
515 .into());
516 }
517 let (nonce, ciphertext) = ciphertext.split_at(AES_GCM_NONCE_SIZE);
518 let key = self.record_value_material()?;
519 let cipher = Aes256Gcm::new_from_slice(key.as_ref()).map_err(|error| {
520 StoreError::ImplementationError {
521 store: self.subtree_name.clone(),
522 reason: format!("Failed to create record cipher: {error}"),
523 }
524 })?;
525 let aad = physical_record_aad(&self.store_identity, physical_key);
526 let plaintext = cipher
527 .decrypt(
528 Nonce::from_slice(nonce),
529 Payload {
530 msg: ciphertext,
531 aad: &aad,
532 },
533 )
534 .map_err(|_| StoreError::DataCorruption {
535 store: self.subtree_name.clone(),
536 reason: "record authentication failed".to_string(),
537 })?;
538 let envelope: DecryptedRecordEnvelope = serde_json::from_slice(&plaintext)?;
539 if self.physical_record_key(&envelope.key)? != physical_key {
540 return Err(StoreError::DataCorruption {
541 store: self.subtree_name.clone(),
542 reason: "record envelope key does not match its physical key".to_string(),
543 }
544 .into());
545 }
546 Ok((envelope.key, envelope.value))
547 }
548
549 fn projection_descriptor(&self, descriptor: ProjectionDescriptor) -> ProjectionDescriptor {
550 ProjectionDescriptor {
551 name: format!("eidetica/password/{}", descriptor.name),
552 version: descriptor.version,
553 }
554 }
555}
556
557fn physical_record_aad(store_identity: &[u8], physical_key: &[u8]) -> Vec<u8> {
558 let mut aad = b"eidetica/password-store/record-envelope/v1\0".to_vec();
559 aad.extend_from_slice(store_identity);
560 aad.push(0);
561 aad.extend_from_slice(physical_key);
562 aad
563}
564
565pub struct PasswordStore<S: Store> {
647 name: String,
649 transaction: Transaction,
651 config: Option<PasswordStoreConfig>,
653 cached_password: Option<Password>,
655 wrapped_info: Option<WrappedStoreInfo>,
657 _phantom: PhantomData<S>,
659}
660
661impl<S: Store> PasswordStore<S> {
662 fn state(&self) -> PasswordStoreState {
664 match (&self.config, &self.cached_password) {
665 (None, _) => PasswordStoreState::Uninitialized,
666 (Some(_), None) => PasswordStoreState::Locked,
667 (Some(_), Some(_)) => PasswordStoreState::Unlocked,
668 }
669 }
670}
671
672impl<S: Store> Registered for PasswordStore<S> {
673 fn type_id() -> &'static str {
674 "encrypted:password:v0"
676 }
677}
678
679#[async_trait]
680impl<S: Store> Store for PasswordStore<S> {
681 type Data = S::Data;
682
683 fn state_model() -> StoreStateModel<Self::Data> {
684 let inner = S::state_model();
685 let descriptor = inner.descriptor();
686 inner.with_descriptor(ProjectionDescriptor {
687 name: format!("eidetica/password/{}", descriptor.name),
688 version: descriptor.version,
689 })
690 }
691
692 async fn load(txn: &Transaction, subtree_name: String) -> Result<Self> {
693 let index_store = txn.get_index().await?;
695 let info = index_store.get_entry(&subtree_name).await?;
696
697 if !Self::supports_type_id(&info.type_id) {
699 return Err(StoreError::TypeMismatch {
700 store: subtree_name,
701 expected: Self::type_id().to_string(),
702 actual: info.type_id,
703 }
704 .into());
705 }
706
707 if info.config.is_empty() {
710 Ok(Self {
711 name: subtree_name,
712 transaction: txn.clone(),
713 config: None,
714 cached_password: None,
715 wrapped_info: None,
716 _phantom: PhantomData,
717 })
718 } else {
719 let config: PasswordStoreConfig =
721 info.config.try_into().map_err(|e: crate::Error| {
722 StoreError::DeserializationFailed {
723 store: subtree_name.clone(),
724 reason: format!("Failed to parse PasswordStoreConfig: {e}"),
725 }
726 })?;
727
728 if config.encryption.algorithm != "aes-256-gcm" {
730 return Err(StoreError::InvalidConfiguration {
731 store: subtree_name,
732 reason: format!(
733 "Unsupported encryption algorithm: {}",
734 config.encryption.algorithm
735 ),
736 }
737 .into());
738 }
739
740 if config.encryption.kdf != "argon2id" {
741 return Err(StoreError::InvalidConfiguration {
742 store: subtree_name,
743 reason: format!("Unsupported KDF: {}", config.encryption.kdf),
744 }
745 .into());
746 }
747
748 Ok(Self {
749 name: subtree_name,
750 transaction: txn.clone(),
751 config: Some(config),
752 cached_password: None,
753 wrapped_info: None,
754 _phantom: PhantomData,
755 })
756 }
757 }
758
759 async fn register(txn: &Transaction, subtree_name: String) -> Result<Self> {
760 let index_store = txn.get_index().await?;
762 index_store
763 .set_entry(&subtree_name, Self::type_id(), Self::default_config())
764 .await?;
765
766 Ok(Self {
767 name: subtree_name,
768 transaction: txn.clone(),
769 config: None,
770 cached_password: None,
771 wrapped_info: None,
772 _phantom: PhantomData,
773 })
774 }
775
776 fn name(&self) -> &str {
777 &self.name
778 }
779
780 fn transaction(&self) -> &Transaction {
781 &self.transaction
782 }
783}
784
785impl<S: Store> PasswordStore<S> {
786 pub async fn initialize(
828 &mut self,
829 password: impl Into<String>,
830 wrapped_config: Doc,
831 ) -> Result<()> {
832 if self.state() != PasswordStoreState::Uninitialized {
834 return Err(StoreError::InvalidOperation {
835 store: self.name.clone(),
836 operation: "initialize".to_string(),
837 reason: "Store is already initialized - use open() instead".to_string(),
838 }
839 .into());
840 }
841
842 let password = password.into();
843 let wrapped_type_id = S::type_id().to_string();
844
845 let argon2_m_cost = DEFAULT_ARGON2_M_COST;
847 let argon2_t_cost = DEFAULT_ARGON2_T_COST;
848 let argon2_p_cost = DEFAULT_ARGON2_P_COST;
849
850 let salt = SaltString::generate(&mut OsRng);
852 let salt_str = salt.as_str().to_string();
853
854 let params =
856 Params::new(argon2_m_cost, argon2_t_cost, argon2_p_cost, Some(32)).map_err(|e| {
857 StoreError::ImplementationError {
858 store: self.name.clone(),
859 reason: format!("Invalid Argon2 parameters: {e}"),
860 }
861 })?;
862 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
863
864 let mut encryption_key = vec![0u8; 32];
866 argon2
867 .hash_password_into(
868 password.as_bytes(),
869 salt.as_str().as_bytes(),
870 &mut encryption_key,
871 )
872 .map_err(|e| StoreError::ImplementationError {
873 store: self.name.clone(),
874 reason: format!("Failed to derive encryption key: {e}"),
875 })?;
876
877 let cipher = Aes256Gcm::new_from_slice(&encryption_key).map_err(|e| {
879 StoreError::ImplementationError {
880 store: self.name.clone(),
881 reason: format!("Failed to create cipher: {e}"),
882 }
883 })?;
884
885 let wrapped_info = WrappedStoreInfo {
887 type_id: wrapped_type_id,
888 config: wrapped_config,
889 };
890 let wrapped_json = serde_json::to_string(&wrapped_info)?;
891 let config_nonce = Aes256Gcm::generate_nonce(&mut OsRng);
892 let wrapped_config_ciphertext = cipher
893 .encrypt(&config_nonce, wrapped_json.as_bytes())
894 .map_err(|e| StoreError::ImplementationError {
895 store: self.name.clone(),
896 reason: format!("Failed to encrypt wrapped config: {e}"),
897 })?;
898
899 encryption_key.zeroize();
901
902 let config = PasswordStoreConfig {
904 encryption: EncryptionInfo {
905 algorithm: "aes-256-gcm".to_string(),
906 kdf: "argon2id".to_string(),
907 salt: salt_str.clone(),
908 version: "v0".to_string(),
909 argon2_m_cost: Some(argon2_m_cost),
910 argon2_t_cost: Some(argon2_t_cost),
911 argon2_p_cost: Some(argon2_p_cost),
912 },
913 wrapped_config: EncryptedFragment {
914 ciphertext: wrapped_config_ciphertext,
915 nonce: config_nonce.to_vec(),
916 },
917 };
918
919 self.set_config(config.clone().into()).await?;
921
922 let password_cache = Password {
924 salt: salt_str,
925 password,
926 argon2_m_cost,
927 argon2_t_cost,
928 argon2_p_cost,
929 };
930
931 let encryptor = Box::new(PasswordEncryptor::new(
933 password_cache.clone(),
934 self.name.clone(),
935 self.transaction.database_id().to_string().as_bytes(),
936 ));
937 self.transaction.register_encryptor(&self.name, encryptor)?;
938
939 self.config = Some(config);
941 self.cached_password = Some(password_cache);
942 self.wrapped_info = Some(wrapped_info);
943
944 Ok(())
945 }
946
947 pub fn open(&mut self, password: impl Into<String>) -> Result<()> {
967 match self.state() {
969 PasswordStoreState::Uninitialized => {
970 return Err(StoreError::InvalidOperation {
971 store: self.name.clone(),
972 operation: "open".to_string(),
973 reason: "Store is not initialized - call initialize() first".to_string(),
974 }
975 .into());
976 }
977 PasswordStoreState::Unlocked => {
978 return Err(StoreError::InvalidOperation {
979 store: self.name.clone(),
980 operation: "open".to_string(),
981 reason: "Store is already open".to_string(),
982 }
983 .into());
984 }
985 PasswordStoreState::Locked => {}
986 }
987
988 let config = self.config.as_ref().expect("Locked state requires config");
989 let password = password.into();
990
991 let argon2_m_cost = config
993 .encryption
994 .argon2_m_cost
995 .unwrap_or(DEFAULT_ARGON2_M_COST);
996 let argon2_t_cost = config
997 .encryption
998 .argon2_t_cost
999 .unwrap_or(DEFAULT_ARGON2_T_COST);
1000 let argon2_p_cost = config
1001 .encryption
1002 .argon2_p_cost
1003 .unwrap_or(DEFAULT_ARGON2_P_COST);
1004
1005 let mut encryption_key = vec![0u8; 32];
1007 let salt = SaltString::from_b64(&config.encryption.salt).map_err(|e| {
1008 StoreError::ImplementationError {
1009 store: self.name.clone(),
1010 reason: format!("Invalid salt in config: {e}"),
1011 }
1012 })?;
1013
1014 let params =
1016 Params::new(argon2_m_cost, argon2_t_cost, argon2_p_cost, Some(32)).map_err(|e| {
1017 StoreError::ImplementationError {
1018 store: self.name.clone(),
1019 reason: format!("Invalid Argon2 parameters: {e}"),
1020 }
1021 })?;
1022 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
1023
1024 argon2
1025 .hash_password_into(
1026 password.as_bytes(),
1027 salt.as_str().as_bytes(),
1028 &mut encryption_key,
1029 )
1030 .map_err(|e| StoreError::ImplementationError {
1031 store: self.name.clone(),
1032 reason: format!("Failed to derive encryption key: {e}"),
1033 })?;
1034
1035 let cipher = Aes256Gcm::new_from_slice(&encryption_key).map_err(|e| {
1037 StoreError::ImplementationError {
1038 store: self.name.clone(),
1039 reason: format!("Failed to create cipher: {e}"),
1040 }
1041 })?;
1042
1043 if config.wrapped_config.nonce.len() != 12 {
1045 return Err(StoreError::InvalidConfiguration {
1046 store: self.name.clone(),
1047 reason: format!(
1048 "Invalid nonce length: expected 12 bytes, got {}",
1049 config.wrapped_config.nonce.len()
1050 ),
1051 }
1052 .into());
1053 }
1054 let config_nonce = Nonce::from_slice(&config.wrapped_config.nonce);
1055
1056 let decrypted_config = cipher
1057 .decrypt(config_nonce, config.wrapped_config.ciphertext.as_slice())
1058 .map_err(|_| StoreError::ImplementationError {
1059 store: self.name.clone(),
1060 reason: "Failed to decrypt wrapped config - incorrect password?".to_string(),
1061 })?;
1062
1063 encryption_key.zeroize();
1065
1066 let wrapped_info: WrappedStoreInfo =
1068 serde_json::from_slice(&decrypted_config).map_err(|e| {
1069 StoreError::DeserializationFailed {
1070 store: self.name.clone(),
1071 reason: format!("Failed to parse wrapped store info: {e}"),
1072 }
1073 })?;
1074
1075 if !S::supports_type_id(&wrapped_info.type_id) {
1077 return Err(StoreError::TypeMismatch {
1078 store: self.name.clone(),
1079 expected: S::type_id().to_string(),
1080 actual: wrapped_info.type_id,
1081 }
1082 .into());
1083 }
1084
1085 let password_cache = Password {
1087 salt: config.encryption.salt.clone(),
1088 password,
1089 argon2_m_cost,
1090 argon2_t_cost,
1091 argon2_p_cost,
1092 };
1093 self.cached_password = Some(password_cache.clone());
1094 self.wrapped_info = Some(wrapped_info);
1095
1096 let encryptor = Box::new(PasswordEncryptor::new(
1098 password_cache,
1099 self.name.clone(),
1100 self.transaction.database_id().to_string().as_bytes(),
1101 ));
1102 self.transaction.register_encryptor(&self.name, encryptor)?;
1103
1104 Ok(())
1105 }
1106
1107 pub fn is_open(&self) -> bool {
1109 self.state() == PasswordStoreState::Unlocked
1110 }
1111
1112 pub fn is_initialized(&self) -> bool {
1114 self.state() != PasswordStoreState::Uninitialized
1115 }
1116
1117 pub async fn inner(&self) -> Result<S> {
1152 if !self.is_open() {
1153 return Err(StoreError::InvalidOperation {
1154 store: self.name.clone(),
1155 operation: "inner".to_string(),
1156 reason: "Store not opened - call open() first".to_string(),
1157 }
1158 .into());
1159 }
1160
1161 S::load(&self.transaction, self.name.clone()).await
1166 }
1167}
1168#[cfg(test)]
1169mod tests {
1170 use super::*;
1171
1172 fn encryptor(store: &str) -> PasswordEncryptor {
1173 PasswordEncryptor::new(
1174 Password {
1175 salt: "MDEyMzQ1Njc4OUFCQ0RFRg".to_string(),
1176 password: "record-authentication".to_string(),
1177 argon2_m_cost: 8,
1178 argon2_t_cost: 1,
1179 argon2_p_cost: 1,
1180 },
1181 store.to_string(),
1182 b"database",
1183 )
1184 }
1185
1186 #[test]
1187 fn record_keys_and_values_use_separate_domain_material() {
1188 let first = encryptor("first");
1189 let second = encryptor("second");
1190 let master = first
1191 .with_key(|key| Ok(<[u8; 32]>::try_from(key).unwrap()))
1192 .unwrap();
1193 assert_ne!(*first.record_key_material().unwrap(), master);
1194 assert_ne!(*first.record_value_material().unwrap(), master);
1195 assert_ne!(
1196 first.record_key_material().unwrap(),
1197 first.record_value_material().unwrap()
1198 );
1199 assert_eq!(
1200 *first.record_key_material().unwrap(),
1201 *second.record_key_material().unwrap()
1202 );
1203 assert_eq!(
1204 *first.record_value_material().unwrap(),
1205 *second.record_value_material().unwrap()
1206 );
1207 }
1208
1209 #[test]
1210 fn encrypted_records_use_unique_nonces() {
1211 let encryptor = encryptor("first");
1212 let first = encryptor.encrypt_record(b"logical", b"plaintext").unwrap();
1213 let second = encryptor.encrypt_record(b"logical", b"plaintext").unwrap();
1214
1215 assert_ne!(&first[..AES_GCM_NONCE_SIZE], &second[..AES_GCM_NONCE_SIZE]);
1216 assert_ne!(first, second);
1217 }
1218
1219 #[test]
1220 fn encrypted_record_rejects_wrong_store_and_physical_key() {
1221 let first = encryptor("first");
1222 let second = encryptor("second");
1223 let physical_key = first.physical_record_key(b"logical").unwrap();
1224 let ciphertext = first.encrypt_record(b"logical", b"plaintext").unwrap();
1225 assert_eq!(
1226 first.decrypt_record(&physical_key, &ciphertext).unwrap(),
1227 (b"logical".to_vec(), b"plaintext".to_vec())
1228 );
1229 assert!(
1230 first
1231 .decrypt_record(&first.physical_record_key(b"other").unwrap(), &ciphertext)
1232 .is_err(),
1233 "ciphertext moved to another row must be rejected"
1234 );
1235 assert!(
1236 second.decrypt_record(&physical_key, &ciphertext).is_err(),
1237 "ciphertext from another Store must be rejected"
1238 );
1239 }
1240
1241 #[test]
1242 fn encrypted_record_rejects_malformed_and_modified_ciphertext() {
1243 let encryptor = encryptor("first");
1244 let physical_key = encryptor.physical_record_key(b"logical").unwrap();
1245 let mut ciphertext = encryptor.encrypt_record(b"logical", b"plaintext").unwrap();
1246
1247 assert!(
1248 encryptor
1249 .decrypt_record(&physical_key, &ciphertext[..AES_GCM_NONCE_SIZE - 1])
1250 .is_err()
1251 );
1252 *ciphertext.last_mut().unwrap() ^= 1;
1253 assert!(
1254 encryptor
1255 .decrypt_record(&physical_key, &ciphertext)
1256 .is_err()
1257 );
1258 }
1259}