Integrate Android biometrics with the shared Rust lease (#98)
Android ARM verification / rust (push) Failing after 47m23s
Android ARM verification / android (push) Failing after 8s
Dependency security audit / rustsec (push) Successful in 23s

This commit is contained in:
hugo committed 2026-10-09 16:32:01 +02:00
1 parent 5c933a99a3
commit 647dc1bb4b
21 files changed
+1473 -118

No files matched your search

+149 -18
View File
@@ -481,10 +481,10 @@ impl MobileAuthentication {
match self.keys.decrypt(ciphertext, &mut provider) {
Ok(plaintext) => {
drop(plaintext);
if biometric_enabled {
handle
.persist_passphrase(key)
.map_err(MobileAuthenticationError::authentication)?;
if biometric_enabled && let Err(error) = handle.persist_passphrase(key)
{
let _ = self.manual_lock();
return Err(MobileAuthenticationError::authentication(error));
}
self.set_active(handle, key.clone())?;
return self.state();
@@ -494,7 +494,10 @@ impl MobileAuthentication {
.manual_lock()
.map_err(MobileAuthenticationError::authentication)?;
}
Err(error) => return Err(key_error(error)),
Err(error) => {
let _ = self.manual_lock();
return Err(key_error(error));
}
}
}
Err(MobileAuthenticationError::new(
@@ -504,6 +507,7 @@ impl MobileAuthentication {
))
}
None if biometric_enabled => {
let mut failure = None;
for key in &candidates {
let handle = match self.session.authenticate(key) {
Ok(handle) => handle,
@@ -512,7 +516,10 @@ impl MobileAuthentication {
AuthenticationError::Cancelled,
));
}
Err(_) => continue,
Err(error) => {
failure = Some(MobileAuthenticationError::authentication(error));
continue;
}
};
let mut provider = KeyOnlyProvider::new(handle.clone(), key);
if let Ok(plaintext) = self.keys.decrypt(ciphertext, &mut provider) {
@@ -523,9 +530,9 @@ impl MobileAuthentication {
let _ = self.session.delete_key_passphrase(key);
let _ = self.session.manual_lock();
}
Err(MobileAuthenticationError::passphrase_required(
Err(failure.unwrap_or_else(|| MobileAuthenticationError::passphrase_required(
"Biometric unlock could not restore this key. Enter its GPG passphrase to recover.",
))
)))
}
None => Err(MobileAuthenticationError::passphrase_required(
"Enter the GPG key passphrase to continue.",
@@ -533,6 +540,103 @@ impl MobileAuthentication {
}
}
/// Explicit enrollment also works before the store contains a password entry.
pub fn enroll_biometric_unlock(
&self,
passphrase: SecretBytes,
) -> Result<MobileAuthenticationState, MobileAuthenticationError> {
self.ensure_repository_idle()?;
self.keys
.validate_secret_passphrase(self.config.default_key().as_str(), Some(&passphrase))
.map_err(|error| match error {
CryptoError::InvalidKeyPassphrase => MobileAuthenticationError::new(
MobileAuthenticationErrorKind::InvalidPassphrase,
"Incorrect Passphrase",
"The GPG key could not be unlocked.",
),
error => key_error(error),
})?;
let fingerprint = self
.keys
.resolve(self.config.default_key().as_str())
.map_err(key_error)?;
let key = self
.keys
.infos()
.find(|key| key.fingerprint() == fingerprint.fingerprint())
.ok_or_else(|| key_error(CryptoError::MissingSecretKey))?;
let handle = self
.session
.authenticate_with_passphrase(&key, passphrase)
.map_err(MobileAuthenticationError::authentication)?;
if let Err(error) = self.persist_biometric_unlock(&handle, &key) {
let _ = self.manual_lock();
return Err(error);
}
self.set_active(handle, key)?;
self.state()
}
/// Restore the configured key's lease for an explicit protected-access request.
pub fn restore_biometric_lease(
&self,
) -> Result<MobileAuthenticationState, MobileAuthenticationError> {
self.ensure_repository_idle()?;
let state = self.state()?;
if state.unlocked() {
return Ok(state);
}
if !state.biometric_unlock_enabled() {
return Err(MobileAuthenticationError::passphrase_required(
"Enter the GPG key passphrase to continue.",
));
}
let fingerprint = self
.keys
.resolve(self.config.default_key().as_str())
.map_err(key_error)?;
let key = self
.keys
.infos()
.find(|key| key.fingerprint() == fingerprint.fingerprint())
.ok_or_else(|| key_error(CryptoError::MissingSecretKey))?;
let mut handle = self
.session
.authenticate(&key)
.map_err(MobileAuthenticationError::authentication)?;
let passphrase = handle.secret_for(&key).map_err(|_| locked_error())?;
if self
.keys
.validate_secret_passphrase(key.fingerprint().as_str(), Some(&passphrase))
.is_err()
{
let _ = self.session.delete_key_passphrase(&key);
let _ = self.manual_lock();
return Err(MobileAuthenticationError::passphrase_required(
"The protected passphrase is unusable. Enter its GPG passphrase to recover.",
));
}
self.set_active(handle, key)?;
self.state()
}
fn persist_biometric_unlock(
&self,
handle: &MobileAuthenticationHandle,
key: &KeyInfo,
) -> Result<(), MobileAuthenticationError> {
handle
.persist_passphrase(key)
.map_err(MobileAuthenticationError::authentication)?;
if let Err(error) = self.config.update_biometric_unlock(true) {
let _ = self.delete_all_key_passphrases();
let _ = self.manual_lock();
return Err(config_error(error));
}
self.status()?.biometric_unlock_enabled = true;
Ok(())
}
pub fn set_biometric_unlock(
&self,
enabled: bool,
@@ -548,17 +652,10 @@ impl MobileAuthentication {
"Unlock a password entry before enabling biometric unlock.",
)
})?;
active
.handle
.persist_passphrase(&active.key)
.map_err(MobileAuthenticationError::authentication)?;
let handle = active.handle.clone();
let key = active.key.clone();
drop(status);
if let Err(error) = self.config.update_biometric_unlock(true) {
let _ = self.delete_all_key_passphrases();
let _ = self.manual_lock();
return Err(config_error(error));
}
self.status()?.biometric_unlock_enabled = true;
self.persist_biometric_unlock(&handle, &key)?;
} else {
self.delete_all_key_passphrases()?;
self.config
@@ -1961,6 +2058,40 @@ mod tests {
watch_preference,
};
#[test]
fn explicit_biometric_enrollment_and_restore_work_without_a_password_entry() {
use crate::authentication::SystemAuthenticationClock;
use std::sync::Arc;
let (_root, private, backend) = crate::mobile_onboarding::android::tests::home_fixture();
let authentication = private
.authentication(backend, Arc::new(SystemAuthenticationClock::default()))
.unwrap();
assert!(
crate::mobile_passwords::navigation::navigate_repository(
&authentication.repository,
"",
None,
None,
false
)
.unwrap()
.rows
.is_empty()
);
assert!(
authentication
.enroll_biometric_unlock(crate::repository::SecretBytes::new(
b"fixture-alice-passphrase".to_vec()
))
.unwrap()
.biometric_unlock_enabled()
);
authentication.manual_lock().unwrap();
assert!(authentication.restore_biometric_lease().unwrap().unlocked());
authentication.set_biometric_unlock(false).unwrap();
assert!(authentication.restore_biometric_lease().is_err());
}
#[test]
fn repository_actions_protect_editors_and_serialize_mutations() {
assert_eq!(