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

+68 -1
View File
@@ -11,6 +11,15 @@ use std::sync::Arc;
#[cfg(any(target_os = "ios", target_os = "macos"))]
use std::collections::HashMap;
#[cfg(any(target_os = "ios", target_os = "macos"))]
use security_framework::{
access_control::{ProtectionMode, SecAccessControl},
passwords::{
AccessControlOptions, PasswordOptions, delete_generic_password_options, generic_password,
set_generic_password_options,
},
};
use keyring_core::{CredentialStore, Entry};
use super::{SecretLocator, SecretProtection, SecretStoreBackend, SecretStoreError};
@@ -77,7 +86,7 @@ impl NativeSecretBackend {
let (service, user) = locator.service_and_user();
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
if protection == SecretProtection::RequireUserPresence {
if protection != SecretProtection::DeviceUnlocked {
return Err(SecretStoreError::UnsupportedProtection);
}
self.store.build(service, &user, None).map_err(map_error)
@@ -101,6 +110,7 @@ impl NativeSecretBackend {
.build(service, &user, modifiers.as_ref())
.map_err(map_error)
}
SecretProtection::BiometryCurrentSet => Err(SecretStoreError::Unavailable),
}
}
#[cfg(not(any(
@@ -123,6 +133,10 @@ impl SecretStoreBackend for NativeSecretBackend {
protection: SecretProtection,
value: &[u8],
) -> Result<(), SecretStoreError> {
#[cfg(any(target_os = "ios", target_os = "macos"))]
if protection == SecretProtection::BiometryCurrentSet {
return strict_set(locator, value);
}
let entry = self.entry(locator, protection)?;
match entry.get_secret() {
Ok(existing) => {
@@ -139,6 +153,12 @@ impl SecretStoreBackend for NativeSecretBackend {
locator: &SecretLocator,
protection: SecretProtection,
) -> Result<SecretBytes, SecretStoreError> {
#[cfg(any(target_os = "ios", target_os = "macos"))]
if protection == SecretProtection::BiometryCurrentSet {
return generic_password(strict_query(locator))
.map(SecretBytes::new)
.map_err(map_security_error);
}
self.entry(locator, protection)?
.get_secret()
.map(SecretBytes::new)
@@ -151,6 +171,11 @@ impl SecretStoreBackend for NativeSecretBackend {
protection: SecretProtection,
value: &[u8],
) -> Result<(), SecretStoreError> {
#[cfg(any(target_os = "ios", target_os = "macos"))]
if protection == SecretProtection::BiometryCurrentSet {
delete_generic_password_options(strict_query(locator)).map_err(map_security_error)?;
return strict_set(locator, value);
}
let entry = self.entry(locator, protection)?;
let existing = entry.get_secret().map_err(map_error)?;
drop(SecretBytes::new(existing));
@@ -162,6 +187,11 @@ impl SecretStoreBackend for NativeSecretBackend {
locator: &SecretLocator,
protection: SecretProtection,
) -> Result<(), SecretStoreError> {
#[cfg(any(target_os = "ios", target_os = "macos"))]
if protection == SecretProtection::BiometryCurrentSet {
return delete_generic_password_options(strict_query(locator))
.map_err(map_security_error);
}
self.entry(locator, protection)?
.delete_credential()
.map_err(map_error)
@@ -175,6 +205,43 @@ fn presence_modifiers(protection: SecretProtection) -> Option<HashMap<&'static s
SecretProtection::RequireUserPresence => {
Some(HashMap::from([("access-policy", "require-user-presence")]))
}
SecretProtection::BiometryCurrentSet => None,
}
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
fn strict_query(locator: &SecretLocator) -> PasswordOptions {
let (service, account) = locator.service_and_user();
let mut options = PasswordOptions::new_generic_password(service, &account);
options.use_protected_keychain();
options
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
fn strict_create_options(locator: &SecretLocator) -> Result<PasswordOptions, SecretStoreError> {
let access = SecAccessControl::create_with_protection(
Some(ProtectionMode::AccessibleWhenPasscodeSetThisDeviceOnly),
AccessControlOptions::BIOMETRY_CURRENT_SET.bits(),
)
.map_err(map_security_error)?;
let mut options = strict_query(locator);
options.set_access_control(access);
Ok(options)
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
fn strict_set(locator: &SecretLocator, value: &[u8]) -> Result<(), SecretStoreError> {
set_generic_password_options(value, strict_create_options(locator)?).map_err(map_security_error)
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
fn map_security_error(error: security_framework::base::Error) -> SecretStoreError {
match error.code() {
-25300 => SecretStoreError::Missing,
-128 => SecretStoreError::Cancelled,
-25293 | -25308 => SecretStoreError::Denied,
-50 => SecretStoreError::UnsupportedProtection,
_ => SecretStoreError::Unavailable,
}
}