Implement biometric-protected GPG unlock
This commit is contained in:
@@ -33,6 +33,7 @@ struct BackendState {
|
||||
retrieves: usize,
|
||||
locks: usize,
|
||||
unlocks: usize,
|
||||
last_protection: Option<SecretProtection>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -51,6 +52,14 @@ impl MemoryBackend {
|
||||
self.0.lock().expect("test mutex").locks
|
||||
}
|
||||
|
||||
fn last_protection(&self) -> Option<SecretProtection> {
|
||||
self.0.lock().expect("test mutex").last_protection
|
||||
}
|
||||
|
||||
fn invalidate_enrollment(&self) {
|
||||
self.0.lock().expect("test mutex").values.clear();
|
||||
}
|
||||
|
||||
fn take_fault(state: &mut BackendState) -> Result<(), SecretStoreError> {
|
||||
match state.fault.take() {
|
||||
Some(error) => Err(error),
|
||||
@@ -63,11 +72,12 @@ impl SecretStoreBackend for MemoryBackend {
|
||||
fn create(
|
||||
&self,
|
||||
locator: &SecretLocator,
|
||||
_protection: SecretProtection,
|
||||
protection: SecretProtection,
|
||||
value: &[u8],
|
||||
) -> Result<(), SecretStoreError> {
|
||||
let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
|
||||
Self::take_fault(&mut state)?;
|
||||
state.last_protection = Some(protection);
|
||||
if state.values.contains_key(locator) {
|
||||
return Err(SecretStoreError::AlreadyExists);
|
||||
}
|
||||
@@ -80,10 +90,11 @@ impl SecretStoreBackend for MemoryBackend {
|
||||
fn retrieve(
|
||||
&self,
|
||||
locator: &SecretLocator,
|
||||
_protection: SecretProtection,
|
||||
protection: SecretProtection,
|
||||
) -> Result<SecretBytes, SecretStoreError> {
|
||||
let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
|
||||
Self::take_fault(&mut state)?;
|
||||
state.last_protection = Some(protection);
|
||||
state.retrieves += 1;
|
||||
state
|
||||
.values
|
||||
@@ -95,11 +106,12 @@ impl SecretStoreBackend for MemoryBackend {
|
||||
fn replace(
|
||||
&self,
|
||||
locator: &SecretLocator,
|
||||
_protection: SecretProtection,
|
||||
protection: SecretProtection,
|
||||
value: &[u8],
|
||||
) -> Result<(), SecretStoreError> {
|
||||
let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
|
||||
Self::take_fault(&mut state)?;
|
||||
state.last_protection = Some(protection);
|
||||
let existing = state
|
||||
.values
|
||||
.get_mut(locator)
|
||||
@@ -111,10 +123,11 @@ impl SecretStoreBackend for MemoryBackend {
|
||||
fn delete(
|
||||
&self,
|
||||
locator: &SecretLocator,
|
||||
_protection: SecretProtection,
|
||||
protection: SecretProtection,
|
||||
) -> Result<(), SecretStoreError> {
|
||||
let mut state = self.0.lock().map_err(|_| SecretStoreError::Unavailable)?;
|
||||
Self::take_fault(&mut state)?;
|
||||
state.last_protection = Some(protection);
|
||||
state
|
||||
.values
|
||||
.remove(locator)
|
||||
@@ -356,3 +369,71 @@ fn exact_deadline_races_are_serialized_and_timeout_validation_is_bounded() -> Te
|
||||
assert_eq!(backend.locks(), baseline_locks + 1, "expiry relocks once");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_manual_recovery_enrolls_current_biometry_and_reestablishes_the_lease() -> TestResult {
|
||||
let fixture = FixtureSet::load()?;
|
||||
let store = fixture.materialize_store("basic")?;
|
||||
let repository = Repository::open(store.path())?;
|
||||
let keys = KeyStore::load(fixture.path("keys"))?;
|
||||
let alice = fixture_key(&fixture, &keys, "alice")?;
|
||||
let backend = MemoryBackend::default();
|
||||
let clock = ManualClock::default();
|
||||
let session = AuthenticationSession::with_clock(
|
||||
backend.clone(),
|
||||
SecretProtectionPolicy::current_biometry_for_openpgp(),
|
||||
AuthenticationTimeout::new(Duration::from_secs(30))?,
|
||||
clock.clone(),
|
||||
);
|
||||
let reader = VaultReader::new(&repository, &keys);
|
||||
|
||||
assert!(matches!(
|
||||
session.authenticate(&alice),
|
||||
Err(AuthenticationError::SecretStore(SecretStoreError::Missing))
|
||||
));
|
||||
let mut manual = session.authenticate_with_passphrase(
|
||||
&alice,
|
||||
SecretBytes::new(fixture.key("alice")?.passphrase.as_bytes().to_vec()),
|
||||
)?;
|
||||
assert!(matches!(
|
||||
reader.show(Some("email/personal"), &mut manual)?,
|
||||
ShowResult::Entry(_)
|
||||
));
|
||||
manual.persist_passphrase(&alice)?;
|
||||
assert_eq!(
|
||||
backend.last_protection(),
|
||||
Some(SecretProtection::BiometryCurrentSet)
|
||||
);
|
||||
|
||||
session.manual_lock()?;
|
||||
let mut biometric = session.authenticate(&alice)?;
|
||||
assert!(matches!(
|
||||
reader.show(Some("email/personal"), &mut biometric)?,
|
||||
ShowResult::Entry(_)
|
||||
));
|
||||
clock.advance(Duration::from_secs(30));
|
||||
assert!(session.expire()?);
|
||||
assert_eq!(biometric.ensure_active(), Err(AuthenticationError::Expired));
|
||||
|
||||
backend.invalidate_enrollment();
|
||||
assert!(matches!(
|
||||
session.authenticate(&alice),
|
||||
Err(AuthenticationError::SecretStore(SecretStoreError::Missing))
|
||||
));
|
||||
let mut recovered = session.authenticate_with_passphrase(
|
||||
&alice,
|
||||
SecretBytes::new(fixture.key("alice")?.passphrase.as_bytes().to_vec()),
|
||||
)?;
|
||||
assert!(matches!(
|
||||
reader.show(Some("email/personal"), &mut recovered)?,
|
||||
ShowResult::Entry(_)
|
||||
));
|
||||
recovered.persist_passphrase(&alice)?;
|
||||
session.delete_key_passphrase(&alice)?;
|
||||
session.manual_lock()?;
|
||||
assert!(matches!(
|
||||
session.authenticate(&alice),
|
||||
Err(AuthenticationError::SecretStore(SecretStoreError::Missing))
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -210,6 +210,31 @@ fn mobile_tab_defaults_and_persists_through_storage_configuration() -> TestResul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn biometric_preference_is_secret_free_and_defaults_to_disabled() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fs::create_dir_all(fixture.temporary.path().join("cwd/vault"))?;
|
||||
fixture.write_explicit(fixture.valid_contents())?;
|
||||
let config = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
assert!(!config.biometric_unlock_enabled());
|
||||
|
||||
config.update_biometric_unlock(true)?;
|
||||
let contents = fs::read_to_string(fixture.explicit_path())?;
|
||||
assert!(contents.contains("biometric_unlock_enabled = true"));
|
||||
assert!(!contents.to_ascii_lowercase().contains("passphrase"));
|
||||
let reloaded = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
assert!(reloaded.biometric_unlock_enabled());
|
||||
|
||||
reloaded.update_biometric_unlock(false)?;
|
||||
assert!(
|
||||
!fixture
|
||||
.loader()
|
||||
.load(Some(&fixture.explicit_path()))?
|
||||
.biometric_unlock_enabled()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desktop_vault_switch_preserves_and_reloads_the_shared_configuration() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
|
||||
Reference in New Issue
Block a user