kv-wasm: forget the Secret Key on logout and on any failed unlock or login
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -79,8 +79,10 @@ struct Unlocked {
|
||||
vault_key: Arc<SymKey>,
|
||||
/// OPAQUE password, kept while unlocked so an expired session can be renewed without prompting.
|
||||
auth_input: Option<Zeroizing<[u8; 32]>>,
|
||||
/// The Secret Key, for setup payloads (Emergency Kit, "Add a device", scan-to-join).
|
||||
secret_key: Zeroizing<[u8; 16]>,
|
||||
/// The Secret Key, for setup payloads (Emergency Kit, "Add a device", scan-to-join). Set only
|
||||
/// once an unlock or login proved it (the keyset unwrapped); dropped on `lock`, `logout` and any
|
||||
/// failed unlock or login. Never persisted in any form, and no getter exposes it.
|
||||
secret_key: Option<Zeroizing<[u8; 16]>>,
|
||||
}
|
||||
|
||||
struct Device {
|
||||
@@ -191,12 +193,16 @@ impl WebCore {
|
||||
keyset: Arc::new(keyset),
|
||||
vault_key: Arc::new(vault_key),
|
||||
auth_input: Some(derived.auth_input),
|
||||
secret_key: Zeroizing::new(*secret_key.expose()),
|
||||
secret_key: Some(Zeroizing::new(*secret_key.expose())),
|
||||
});
|
||||
Ok(secret_key.to_display().to_string())
|
||||
}
|
||||
|
||||
pub fn unlock(&self, master_password: &str, secret_key: &str) -> Result<()> {
|
||||
self.unlock_inner(master_password, secret_key).inspect_err(|_| self.forget_secret_key())
|
||||
}
|
||||
|
||||
fn unlock_inner(&self, master_password: &str, secret_key: &str) -> Result<()> {
|
||||
let mut inner = self.lock_inner();
|
||||
let acct = inner.account.clone().ok_or(CoreError::NoAccount)?;
|
||||
let sk = SecretKey::parse(secret_key)?;
|
||||
@@ -208,7 +214,7 @@ impl WebCore {
|
||||
keyset: Arc::new(keyset),
|
||||
vault_key: Arc::new(vault_key),
|
||||
auth_input: Some(derived.auth_input),
|
||||
secret_key: Zeroizing::new(*sk.expose()),
|
||||
secret_key: Some(Zeroizing::new(*sk.expose())),
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
@@ -496,7 +502,7 @@ impl WebCore {
|
||||
/// Logs in with the master password and Secret Key. A browser with no account fetches it from the
|
||||
/// server; call `sync_now` afterwards to download the vault.
|
||||
pub async fn login(&self, master_password: &str, secret_key: &str, totp: Option<String>) -> Result<LoginResult> {
|
||||
let sk = SecretKey::parse(secret_key)?;
|
||||
let sk = SecretKey::parse(secret_key).inspect_err(|_| self.forget_secret_key())?;
|
||||
self.login_inner(master_password, sk, totp, None, None).await
|
||||
}
|
||||
|
||||
@@ -508,6 +514,18 @@ impl WebCore {
|
||||
totp: Option<String>,
|
||||
ticket: Option<String>,
|
||||
expect_account: Option<Uuid>,
|
||||
) -> Result<LoginResult> {
|
||||
let r = self.login_unchecked(master_password, sk, totp, ticket, expect_account).await;
|
||||
r.inspect_err(|_| self.forget_secret_key())
|
||||
}
|
||||
|
||||
async fn login_unchecked(
|
||||
&self,
|
||||
master_password: &str,
|
||||
sk: SecretKey,
|
||||
totp: Option<String>,
|
||||
ticket: Option<String>,
|
||||
expect_account: Option<Uuid>,
|
||||
) -> Result<LoginResult> {
|
||||
let api = self.api()?;
|
||||
let local = self.lock_inner().account.clone();
|
||||
@@ -561,7 +579,7 @@ impl WebCore {
|
||||
keyset: Arc::new(keyset),
|
||||
vault_key: Arc::new(vault_key),
|
||||
auth_input: Some(derived.auth_input),
|
||||
secret_key: Zeroizing::new(*sk.expose()),
|
||||
secret_key: Some(Zeroizing::new(*sk.expose())),
|
||||
});
|
||||
Ok(LoginResult::Ok)
|
||||
}
|
||||
@@ -625,8 +643,9 @@ impl WebCore {
|
||||
Ok(self.api()?.revoke_device(parse_id(device_id)?).await?)
|
||||
}
|
||||
|
||||
/// Ends the server session (local data stays).
|
||||
/// Ends the server session (local data stays) and forgets the Secret Key.
|
||||
pub async fn logout(&self) -> Result<()> {
|
||||
self.forget_secret_key();
|
||||
let api = self.api()?;
|
||||
let r = api.logout().await;
|
||||
self.store.delete_meta(META_SESSION)?;
|
||||
@@ -667,6 +686,11 @@ impl WebCore {
|
||||
/// Web only: the browser talks to its own origin (`connect-src 'self'`), so a setup code for
|
||||
/// another server than the configured one is refused instead of switching servers.
|
||||
pub async fn login_with_setup(&self, setup: &SetupInfo, master_password: &str) -> Result<LoginResult> {
|
||||
let r = self.login_with_setup_inner(setup, master_password).await;
|
||||
r.inspect_err(|_| self.forget_secret_key())
|
||||
}
|
||||
|
||||
async fn login_with_setup_inner(&self, setup: &SetupInfo, master_password: &str) -> Result<LoginResult> {
|
||||
let p = from_info(setup)?;
|
||||
let (account, server) = {
|
||||
let inner = self.lock_inner();
|
||||
@@ -805,6 +829,12 @@ impl WebCore {
|
||||
|
||||
// ------------------------------------------------------------ internals
|
||||
|
||||
fn forget_secret_key(&self) {
|
||||
if let Some(u) = self.lock_inner().unlocked.as_mut() {
|
||||
u.secret_key = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn lock_join(&self) -> std::sync::MutexGuard<'_, Option<JoinState>> {
|
||||
self.join.lock().unwrap_or_else(|p| p.into_inner())
|
||||
}
|
||||
@@ -817,7 +847,7 @@ impl WebCore {
|
||||
server_url: kv_sync::transport::normalize_base_url(&server.url)?,
|
||||
spki_pins: server.pins,
|
||||
account_id: acct.account_id,
|
||||
secret_key: SecretKey::from_bytes(*u.secret_key).to_display(),
|
||||
secret_key: SecretKey::from_bytes(**u.secret_key.as_ref().ok_or(CoreError::Locked)?).to_display(),
|
||||
ticket,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user