fix: clip sync timestamps, exclusive temp files, zeroized secret reads
- sync: timestamps and tombstones in the shared file are clipped to the local clock, and an edit wins a tie with a deletion, so a forged future-dated record can no longer override every later local change (or keep deleting a re-created profile) for as long as the forged date. - write_private: the temp file is created O_EXCL under a random name, so a symlink pre-placed in a shared folder can't be followed or overwritten. - Vault/keychain reads return Zeroizing<String>; has_sync_password is cached for a few seconds instead of fetching the real value every frame. - generate_ssh_key creates a missing ~/.ssh as 0700. - Honest wording: encrypted exports and the sync file carry login-script values; the audit entry, the Settings hint and the docs now say so.
This commit is contained in:
@@ -43,6 +43,9 @@ pub struct Service {
|
||||
pub store: Arc<Store>,
|
||||
pub secrets: Arc<Secrets>,
|
||||
pub sessions: Arc<Manager>,
|
||||
/// Whether a sync password is stored, probed at most every few seconds: the Settings page
|
||||
/// asks every frame, and each probe is a keychain round trip that fetches the real value.
|
||||
sync_password: parking_lot::Mutex<Option<(std::time::Instant, bool)>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -58,7 +61,7 @@ impl Service {
|
||||
let store = Arc::new(Store::open(&config.db_path)?);
|
||||
let secrets = Arc::new(Secrets::new(Vault::new(config.vault_dir.clone()), Keychain::new(KEYCHAIN_SERVICE)));
|
||||
let sessions = Manager::new(store.clone(), secrets.clone(), config.recordings_dir.clone());
|
||||
Ok(Service { config, store, secrets, sessions })
|
||||
Ok(Service { config, store, secrets, sessions, sync_password: Default::default() })
|
||||
}
|
||||
|
||||
// --- profiles -----------------------------------------------------------------------
|
||||
@@ -115,6 +118,7 @@ impl Service {
|
||||
|
||||
/// Sets (or, when empty, forgets) the profile-sync password.
|
||||
pub fn set_sync_password(&self, password: &str) -> Result<()> {
|
||||
*self.sync_password.lock() = None;
|
||||
if password.is_empty() {
|
||||
self.secrets.delete(SYNC_PASSWORD);
|
||||
return Ok(());
|
||||
@@ -140,7 +144,15 @@ impl Service {
|
||||
}
|
||||
|
||||
pub fn has_sync_password(&self) -> bool {
|
||||
self.secrets.get(SYNC_PASSWORD).is_some()
|
||||
let mut c = self.sync_password.lock();
|
||||
if let Some((at, v)) = *c
|
||||
&& at.elapsed() < std::time::Duration::from_secs(5)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
let v = self.secrets.get(SYNC_PASSWORD).is_some();
|
||||
*c = Some((std::time::Instant::now(), v));
|
||||
v
|
||||
}
|
||||
|
||||
/// Merges profiles with the shared sync file; see [`crate::sync`].
|
||||
@@ -431,7 +443,7 @@ impl Service {
|
||||
path.as_mut_os_string().push(format!(".{}", exportimport::EXTENSION));
|
||||
}
|
||||
exportimport::encrypt_and_export(&ps, password, &path)?;
|
||||
self.store.add_audit("export", "", "", &format!("Exported {} profile(s) without secrets", ps.len()));
|
||||
self.store.add_audit("export", "", "", &format!("Exported {} profile(s) (saved credentials not included)", ps.len()));
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user