diff --git a/crates/chanora_storage/src/lib.rs b/crates/chanora_storage/src/lib.rs index 0ca64d1..596c848 100644 --- a/crates/chanora_storage/src/lib.rs +++ b/crates/chanora_storage/src/lib.rs @@ -253,41 +253,59 @@ impl IdentityFileStore { return Ok(()); } // File-fallback DEK present? Try to migrate into the - // keyring opportunistically (lets users upgrade for free) - // and keep the file as the live source if migration fails. + // keyring opportunistically (lets users upgrade for free). + // We always keep the file around as a stable mirror — + // platform keyring access can flicker (e.g. SSH-launched + // session on Windows hits ERROR_NO_SUCH_LOGON_SESSION even + // though an RDP / console session reached the same store + // fine), and if a transient keyring outage made us + // regenerate the DEK we would silently break decrypt of + // every prior identity.tskey blob (reported on the v1.0.0 + // -rc.7 Windows verification run as "Bridge Error + // connection failed: storage crypto decrypt aead error"). + // The file lives in app-private storage where the platform + // sandbox is the access-control authority, so retaining + // the mirror does not weaken the security posture in any + // meaningful way. if self.dek_path.exists() { if let Ok(key) = read_file_dek(&self.dek_path) { - if self.keyring_save(&key) { - // Best-effort scrub: remove the file copy now - // that the keyring holds the authoritative value. - let _ = fs::remove_file(&self.dek_path); - } + let _ = self.keyring_save(&key); let mut k = key; k.zeroize(); } return Ok(()); } - // Fresh install: generate a new DEK and store it in the - // keyring if we can, else fall back to the file. + // Fresh install: generate a new DEK and persist to BOTH + // the keyring (when reachable) and the file. The file is + // the durable source of truth; the keyring is an optional + // accelerator that platform UX integrates with. let mut key = [0u8; 32]; OsRng.fill_bytes(&mut key); - if !self.keyring_save(&key) { - let mut f = open_private(&self.dek_path)?; - f.write_all(&key) - .map_err(|e| StorageError::Io(format!("write dek: {e}")))?; - f.sync_all() - .map_err(|e| StorageError::Io(format!("sync dek: {e}")))?; - info!(target: "chanora_storage", path = ?self.dek_path, "DEK generated (file fallback)"); - } + let _ = self.keyring_save(&key); + let mut f = open_private(&self.dek_path)?; + f.write_all(&key) + .map_err(|e| StorageError::Io(format!("write dek: {e}")))?; + f.sync_all() + .map_err(|e| StorageError::Io(format!("sync dek: {e}")))?; + info!(target: "chanora_storage", path = ?self.dek_path, "DEK generated (file fallback)"); key.zeroize(); Ok(()) } fn load_dek(&self) -> Result<[u8; 32], StorageError> { + // File is the durable source of truth (see `ensure_dek`). + // Prefer it when present; only consult the keyring as a + // legacy-migration path for installs that lost their file + // mirror before this commit landed. + if self.dek_path.exists() { + return read_file_dek(&self.dek_path); + } if let Some(k) = self.keyring_load()? { return Ok(k); } - read_file_dek(&self.dek_path) + Err(StorageError::Crypto( + "no DEK available (file missing, keyring empty)".into(), + )) } /// Hand out a [`DekCrypto`] anchored on the same DEK that