Implement biometric-protected GPG unlock

This commit is contained in:
hugo committed 2026-08-11 18:32:53 +02:00
1 parent 3295761bcf
commit 873db91204
15 files changed
+2043 -59

No files matched your search

+85 -30
View File
@@ -227,36 +227,7 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> AuthenticationSession<B, C>
}
let _operation = self.shared.operation()?;
self.shared.expire_if_needed()?;
let now = self.shared.clock.now();
let deadline = now
.checked_add(self.shared.timeout.duration())
.ok_or(AuthenticationError::ClockOverflow)?;
let (generation, was_active) = {
let mut state = self.shared.state()?;
match state.active.as_mut() {
Some(active) => {
active.deadline = deadline;
(active.generation, true)
}
None => {
state.next_generation = state.next_generation.wrapping_add(1);
let generation = state.next_generation;
state.active = Some(ActiveLease {
generation,
deadline,
unlock_material: BTreeMap::new(),
});
(generation, false)
}
}
};
if !was_active && let Err(error) = self.shared.store.unlock() {
self.shared
.revoke_local(generation, RevocationReason::AuthenticationFailed)?;
return Err(error.into());
}
let generation = self.shared.begin_lease()?;
if key.requires_passphrase()
&& let Err(error) = self.shared.cache_key_passphrase(generation, key)
@@ -273,6 +244,40 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> AuthenticationSession<B, C>
})
}
/// Establish a lease from a manually entered passphrase without persisting it.
pub fn authenticate_with_passphrase(
&self,
key: &KeyInfo,
passphrase: SecretBytes,
) -> Result<AuthenticationHandle<B, C>, AuthenticationError> {
if !key.has_secret() || !key.requires_passphrase() || passphrase.expose().is_empty() {
return Err(AuthenticationError::Locked);
}
let _operation = self.shared.operation()?;
self.shared.expire_if_needed()?;
let generation = self.shared.begin_lease()?;
let fingerprint = key.fingerprint().as_str().to_owned();
self.shared.with_active(generation, |active| {
active.unlock_material.insert(fingerprint, passphrase);
})?;
Ok(AuthenticationHandle {
shared: Arc::clone(&self.shared),
generation,
})
}
/// Remove this key's OS-protected passphrase. A missing item is already disabled.
pub fn delete_key_passphrase(&self, key: &KeyInfo) -> Result<(), AuthenticationError> {
let _operation = self.shared.operation()?;
self.shared.expire_if_needed()?;
self.shared.store.unlock()?;
let reference = SecretReference::openpgp_passphrase(key.fingerprint().as_str())?;
match self.shared.store.delete_openpgp_passphrase(&reference) {
Ok(()) | Err(SecretStoreError::Missing) => Ok(()),
Err(error) => Err(error.into()),
}
}
/// Remaining active time. Calling this from a timer never extends the lease.
pub fn remaining_time(&self) -> Result<Option<Duration>, AuthenticationError> {
let _operation = self.shared.operation()?;
@@ -360,6 +365,25 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> AuthenticationHandle<B, C> {
active.deadline.saturating_sub(now)
})
}
/// Persist the passphrase already verified by a successful crypto operation.
pub fn persist_passphrase(&self, key: &KeyInfo) -> Result<(), AuthenticationError> {
let _operation = self.shared.operation()?;
self.shared.expire_if_needed()?;
let fingerprint = key.fingerprint().as_str();
let value = self.shared.with_active(self.generation, |active| {
active
.unlock_material
.get(fingerprint)
.map(|value| SecretBytes::new(value.expose().to_vec()))
})?;
let value = value.ok_or(AuthenticationError::Locked)?;
let reference = SecretReference::openpgp_passphrase(fingerprint)?;
self.shared
.store
.store_openpgp_passphrase(&reference, value)
.map_err(Into::into)
}
}
impl<B: SecretStoreBackend, C: AuthenticationClock> SecretProvider for AuthenticationHandle<B, C> {
@@ -408,6 +432,37 @@ impl<B: SecretStoreBackend, C: AuthenticationClock> Shared<B, C> {
.map_err(|_| AuthenticationError::SecretStore(SecretStoreError::Unavailable))
}
fn begin_lease(&self) -> Result<u64, AuthenticationError> {
let now = self.clock.now();
let deadline = now
.checked_add(self.timeout.duration())
.ok_or(AuthenticationError::ClockOverflow)?;
let (generation, was_active) = {
let mut state = self.state()?;
match state.active.as_mut() {
Some(active) => {
active.deadline = deadline;
(active.generation, true)
}
None => {
state.next_generation = state.next_generation.wrapping_add(1);
let generation = state.next_generation;
state.active = Some(ActiveLease {
generation,
deadline,
unlock_material: BTreeMap::new(),
});
(generation, false)
}
}
};
if !was_active && let Err(error) = self.store.unlock() {
self.revoke_local(generation, RevocationReason::AuthenticationFailed)?;
return Err(error.into());
}
Ok(generation)
}
fn expire_if_needed(&self) -> Result<bool, AuthenticationError> {
let now = self.clock.now();
let expired = self