Implement biometric-protected GPG unlock
This commit is contained in:
1 parent
3295761bcf
commit
873db91204
15 files changed
+2043
-59
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user