Integrate Android biometrics with the shared Rust lease (#98)
This commit is contained in:
1 parent
5c933a99a3
commit
647dc1bb4b
21 files changed
+1473
-118
No files matched your search
@@ -1762,6 +1762,27 @@ impl MobileAuthentication {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn enroll_biometric_unlock(
|
||||
&self,
|
||||
passphrase: String,
|
||||
) -> Result<MobileAuthenticationState, MobileAuthenticationFfiError> {
|
||||
self.authentication
|
||||
.enroll_biometric_unlock(ironstorage::repository::SecretBytes::new(
|
||||
passphrase.into_bytes(),
|
||||
))
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn restore_biometric_lease(
|
||||
&self,
|
||||
) -> Result<MobileAuthenticationState, MobileAuthenticationFfiError> {
|
||||
self.authentication
|
||||
.restore_biometric_lease()
|
||||
.map(Into::into)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn touch_user_activity(&self) -> Result<(), MobileAuthenticationFfiError> {
|
||||
self.authentication
|
||||
.touch_user_activity()
|
||||
|
||||
@@ -310,14 +310,18 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> AuthenticationSession<B, C>
|
||||
|
||||
/// Revoke every outstanding handle immediately.
|
||||
pub fn manual_lock(&self) -> Result<(), AuthenticationError> {
|
||||
let interrupted = self.shared.store.cancel_pending();
|
||||
let _operation = self.shared.operation()?;
|
||||
self.shared.revoke(RevocationReason::Manual)
|
||||
self.shared.revoke(RevocationReason::Manual)?;
|
||||
interrupted.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Cancel authentication and revoke any lease created by the pending interaction.
|
||||
pub fn cancel(&self) -> Result<(), AuthenticationError> {
|
||||
let interrupted = self.shared.store.cancel_pending();
|
||||
let _operation = self.shared.operation()?;
|
||||
self.shared.revoke(RevocationReason::Cancelled)
|
||||
self.shared.revoke(RevocationReason::Cancelled)?;
|
||||
interrupted.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Apply a new shared inactivity timeout to this process and any active lease.
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -517,6 +517,11 @@ impl<B: SecretStoreBackend> SecretStore<B> {
|
||||
self.backend.lock()
|
||||
}
|
||||
|
||||
/// Interrupt a platform prompt without waiting for the retrieval's state lock.
|
||||
pub(crate) fn cancel_pending(&self) -> Result<(), SecretStoreError> {
|
||||
self.backend.lock()
|
||||
}
|
||||
|
||||
pub fn create(
|
||||
&self,
|
||||
reference: &SecretReference,
|
||||
|
||||
@@ -98,6 +98,7 @@ fn mobile_context_uses_injected_os_backend_clock_and_lifecycle_without_global_co
|
||||
struct BackendState {
|
||||
values: BTreeMap<SecretLocator, SecretBytes>,
|
||||
fault: Option<SecretStoreError>,
|
||||
protected_fault: Option<SecretStoreError>,
|
||||
retrieves: usize,
|
||||
locks: usize,
|
||||
unlocks: usize,
|
||||
@@ -112,6 +113,10 @@ impl MemoryBackend {
|
||||
self.0.lock().expect("test mutex").fault = Some(error);
|
||||
}
|
||||
|
||||
fn fail_protected(&self, error: SecretStoreError) {
|
||||
self.0.lock().unwrap().protected_fault = Some(error);
|
||||
}
|
||||
|
||||
fn retrieves(&self) -> usize {
|
||||
self.0.lock().expect("test mutex").retrieves
|
||||
}
|
||||
@@ -145,6 +150,11 @@ impl SecretStoreBackend for MemoryBackend {
|
||||
) -> Result<(), SecretStoreError> {
|
||||
let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
|
||||
Self::take_fault(&mut state)?;
|
||||
if matches!(locator, SecretLocator::OpenPgpPassphrase { .. })
|
||||
&& let Some(error) = state.protected_fault.take()
|
||||
{
|
||||
return Err(error);
|
||||
}
|
||||
state.last_protection = Some(protection);
|
||||
if state.values.contains_key(locator) {
|
||||
return Err(SecretStoreError::AlreadyExists);
|
||||
@@ -179,6 +189,11 @@ impl SecretStoreBackend for MemoryBackend {
|
||||
) -> Result<(), SecretStoreError> {
|
||||
let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
|
||||
Self::take_fault(&mut state)?;
|
||||
if matches!(locator, SecretLocator::OpenPgpPassphrase { .. })
|
||||
&& let Some(error) = state.protected_fault.take()
|
||||
{
|
||||
return Err(error);
|
||||
}
|
||||
state.last_protection = Some(protection);
|
||||
let existing = state
|
||||
.values
|
||||
@@ -529,3 +544,283 @@ fn verified_manual_recovery_enrolls_current_biometry_and_reestablishes_the_lease
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_enrollment_proves_the_key_commits_only_after_protection_and_recovers() -> TestResult {
|
||||
use ironstorage::{
|
||||
config::Config,
|
||||
mobile_authentication::{MobileAuthentication, MobileAuthenticationErrorKind},
|
||||
};
|
||||
let fixture = FixtureSet::load()?;
|
||||
let vault = fixture.materialize_store("basic")?;
|
||||
let root = tempfile::tempdir()?;
|
||||
let source = root.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&source,
|
||||
format!(
|
||||
"vault = {:?}\nkey_material = {:?}\ndefault_key = {:?}\n",
|
||||
vault.path().to_str().unwrap(),
|
||||
fixture.path("keys").to_str().unwrap(),
|
||||
fixture.key("alice")?.primary_fingerprint
|
||||
),
|
||||
)?;
|
||||
let backend = MemoryBackend::default();
|
||||
let clock = ManualClock::default();
|
||||
let make = || -> TestResult<MobileAuthentication> {
|
||||
Ok(MobileAuthentication::with_platform(
|
||||
Config::load(Some(&source))?,
|
||||
Arc::new(backend.clone()),
|
||||
Arc::new(clock.clone()),
|
||||
)?)
|
||||
};
|
||||
let authentication = make()?;
|
||||
let password =
|
||||
|| SecretBytes::new(fixture.key("alice").unwrap().passphrase.as_bytes().to_vec());
|
||||
assert_eq!(
|
||||
authentication
|
||||
.enroll_biometric_unlock(SecretBytes::new(b"incorrect".to_vec()))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
MobileAuthenticationErrorKind::InvalidPassphrase
|
||||
);
|
||||
assert!(backend.0.lock().unwrap().values.is_empty());
|
||||
for (fault, kind) in [
|
||||
(
|
||||
SecretStoreError::Cancelled,
|
||||
MobileAuthenticationErrorKind::Cancelled,
|
||||
),
|
||||
(
|
||||
SecretStoreError::UnsupportedProtection,
|
||||
MobileAuthenticationErrorKind::BiometryUnavailable,
|
||||
),
|
||||
] {
|
||||
backend.fail_protected(fault);
|
||||
assert_eq!(
|
||||
authentication
|
||||
.enroll_biometric_unlock(password())
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
kind
|
||||
);
|
||||
assert!(!authentication.state()?.unlocked());
|
||||
assert!(!Config::load(Some(&source))?.biometric_unlock_enabled());
|
||||
assert!(backend.0.lock().unwrap().values.is_empty());
|
||||
}
|
||||
let state = authentication.enroll_biometric_unlock(password())?;
|
||||
assert!(state.unlocked() && state.biometric_unlock_enabled());
|
||||
assert_eq!(
|
||||
backend.last_protection(),
|
||||
Some(SecretProtection::BiometryCurrentSet)
|
||||
);
|
||||
authentication.manual_lock()?;
|
||||
assert!(
|
||||
authentication
|
||||
.unlock_entry("email/personal", None)?
|
||||
.unlocked()
|
||||
);
|
||||
let reads = backend.retrieves();
|
||||
authentication.unlock_entry("email/personal", None)?;
|
||||
assert_eq!(
|
||||
reads,
|
||||
backend.retrieves(),
|
||||
"an active shared lease needs no second prompt"
|
||||
);
|
||||
authentication.manual_lock()?;
|
||||
backend.fail_protected(SecretStoreError::Cancelled);
|
||||
assert_eq!(
|
||||
authentication
|
||||
.unlock_entry("email/personal", Some(password()))
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
MobileAuthenticationErrorKind::Cancelled
|
||||
);
|
||||
let cancelled = authentication.state()?;
|
||||
assert!(!cancelled.unlocked());
|
||||
assert_eq!(cancelled.remaining_seconds(), 0);
|
||||
for (fault, kind) in [
|
||||
(
|
||||
SecretStoreError::Lockout,
|
||||
MobileAuthenticationErrorKind::BiometryUnavailable,
|
||||
),
|
||||
(
|
||||
SecretStoreError::Denied,
|
||||
MobileAuthenticationErrorKind::BiometryUnavailable,
|
||||
),
|
||||
(
|
||||
SecretStoreError::Cancelled,
|
||||
MobileAuthenticationErrorKind::Cancelled,
|
||||
),
|
||||
(
|
||||
SecretStoreError::Invalidated,
|
||||
MobileAuthenticationErrorKind::PassphraseRequired,
|
||||
),
|
||||
(
|
||||
SecretStoreError::DeviceKeyMissing,
|
||||
MobileAuthenticationErrorKind::PassphraseRequired,
|
||||
),
|
||||
] {
|
||||
backend.fail_next(fault);
|
||||
assert_eq!(
|
||||
authentication
|
||||
.unlock_entry("email/personal", None)
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
kind
|
||||
);
|
||||
assert!(!authentication.state()?.unlocked());
|
||||
}
|
||||
backend.invalidate_enrollment();
|
||||
assert_eq!(
|
||||
authentication
|
||||
.unlock_entry("email/personal", None)
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
MobileAuthenticationErrorKind::PassphraseRequired
|
||||
);
|
||||
assert!(
|
||||
authentication
|
||||
.enroll_biometric_unlock(password())?
|
||||
.unlocked()
|
||||
);
|
||||
authentication.manual_lock()?;
|
||||
drop(authentication);
|
||||
let recreated = make()?;
|
||||
assert!(
|
||||
!recreated.state()?.unlocked(),
|
||||
"process recreation cannot restore a lease"
|
||||
);
|
||||
assert!(recreated.unlock_entry("email/personal", None)?.unlocked());
|
||||
recreated.set_biometric_unlock(false)?;
|
||||
assert!(!recreated.state()?.unlocked());
|
||||
assert!(backend.0.lock().unwrap().values.is_empty());
|
||||
assert!(!Config::load(Some(&source))?.biometric_unlock_enabled());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mobile_expiry_and_user_input_never_reveal_old_editor_handles() -> TestResult {
|
||||
use ironstorage::{config::Config, mobile_authentication::MobileAuthentication};
|
||||
let fixture = FixtureSet::load()?;
|
||||
let vault = fixture.materialize_store("basic")?;
|
||||
let root = tempfile::tempdir()?;
|
||||
let source = root.path().join("config.toml");
|
||||
std::fs::write(
|
||||
&source,
|
||||
format!(
|
||||
"vault = {:?}\nkey_material = {:?}\ndefault_key = {:?}\n",
|
||||
vault.path().to_str().unwrap(),
|
||||
fixture.path("keys").to_str().unwrap(),
|
||||
fixture.key("alice")?.primary_fingerprint
|
||||
),
|
||||
)?;
|
||||
let backend = MemoryBackend::default();
|
||||
let clock = ManualClock::default();
|
||||
let authentication = MobileAuthentication::with_platform(
|
||||
Config::load(Some(&source))?,
|
||||
Arc::new(backend),
|
||||
Arc::new(clock.clone()),
|
||||
)?;
|
||||
authentication.set_authentication_timeout(2)?;
|
||||
authentication.unlock_entry(
|
||||
"email/personal",
|
||||
Some(SecretBytes::new(
|
||||
fixture.key("alice")?.passphrase.as_bytes().to_vec(),
|
||||
)),
|
||||
)?;
|
||||
let editor = authentication.begin_entry_editor("email/personal")?;
|
||||
clock.advance(Duration::from_secs(1));
|
||||
assert_eq!(authentication.state()?.remaining_seconds(), 1);
|
||||
authentication.touch_user_activity()?;
|
||||
assert_eq!(authentication.state()?.remaining_seconds(), 2);
|
||||
clock.advance(Duration::from_secs(2));
|
||||
assert!(!authentication.state()?.unlocked());
|
||||
assert!(authentication.entry_editor(editor.id()).is_err());
|
||||
assert!(authentication.entry_page("email/personal", 0).is_err());
|
||||
assert!(authentication.touch_user_activity().is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locking_interrupts_an_os_prompt_before_waiting_for_the_lease_mutex() -> TestResult {
|
||||
struct PromptBackend {
|
||||
inner: MemoryBackend,
|
||||
started: std::sync::mpsc::Sender<()>,
|
||||
cancelled: (Mutex<bool>, std::sync::Condvar),
|
||||
}
|
||||
impl SecretStoreBackend for PromptBackend {
|
||||
fn create(
|
||||
&self,
|
||||
locator: &SecretLocator,
|
||||
protection: SecretProtection,
|
||||
value: &[u8],
|
||||
) -> Result<(), SecretStoreError> {
|
||||
self.inner.create(locator, protection, value)
|
||||
}
|
||||
fn replace(
|
||||
&self,
|
||||
locator: &SecretLocator,
|
||||
protection: SecretProtection,
|
||||
value: &[u8],
|
||||
) -> Result<(), SecretStoreError> {
|
||||
self.inner.replace(locator, protection, value)
|
||||
}
|
||||
fn delete(
|
||||
&self,
|
||||
locator: &SecretLocator,
|
||||
protection: SecretProtection,
|
||||
) -> Result<(), SecretStoreError> {
|
||||
self.inner.delete(locator, protection)
|
||||
}
|
||||
fn retrieve(
|
||||
&self,
|
||||
_: &SecretLocator,
|
||||
_: SecretProtection,
|
||||
) -> Result<SecretBytes, SecretStoreError> {
|
||||
self.started.send(()).unwrap();
|
||||
let cancelled = self.cancelled.0.lock().unwrap();
|
||||
let (cancelled, wait) = self
|
||||
.cancelled
|
||||
.1
|
||||
.wait_timeout_while(cancelled, Duration::from_secs(3), |cancelled| !*cancelled)
|
||||
.unwrap();
|
||||
assert!(
|
||||
!wait.timed_out() && *cancelled,
|
||||
"lock must interrupt the OS prompt"
|
||||
);
|
||||
Err(SecretStoreError::Cancelled)
|
||||
}
|
||||
fn lock(&self) -> Result<(), SecretStoreError> {
|
||||
*self.cancelled.0.lock().unwrap() = true;
|
||||
self.cancelled.1.notify_all();
|
||||
self.inner.lock()
|
||||
}
|
||||
fn unlock(&self) -> Result<(), SecretStoreError> {
|
||||
self.inner.unlock()
|
||||
}
|
||||
}
|
||||
let fixture = FixtureSet::load()?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let alice = fixture_key(&fixture, &keys, "alice")?;
|
||||
let (started, ready) = std::sync::mpsc::channel();
|
||||
let backend = Arc::new(PromptBackend {
|
||||
inner: MemoryBackend::default(),
|
||||
started,
|
||||
cancelled: (Mutex::new(false), std::sync::Condvar::new()),
|
||||
});
|
||||
let session = AuthenticationSession::with_clock(
|
||||
backend,
|
||||
SecretProtectionPolicy::current_biometry_for_openpgp(),
|
||||
AuthenticationTimeout::default(),
|
||||
ManualClock::default(),
|
||||
);
|
||||
let worker_session = session.clone();
|
||||
let worker = std::thread::spawn(move || worker_session.authenticate(&alice));
|
||||
ready.recv_timeout(Duration::from_secs(2))?;
|
||||
session.manual_lock()?;
|
||||
assert!(matches!(
|
||||
worker.join().unwrap(),
|
||||
Err(AuthenticationError::Cancelled)
|
||||
));
|
||||
assert_eq!(session.remaining_time()?, None);
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user