use std::sync::Mutex;
use zeroize::Zeroizing;
use super::{
KeyStore, KeyStoreError, MAX_KEY_LEN, MAX_KEYS, OrphanDetection, SERVICE, TEST_SERVICE,
valid_name,
};
pub(in crate::store) struct OsKeyStore {
service: &'static str,
}
static CREATE_LOCK: Mutex<()> = Mutex::new(());
impl OsKeyStore {
pub(in crate::store) fn new(test: bool) -> Self {
Self {
service: selected_service(test, std::env::var_os("QG_DESKTOP_TEST_KEYCHAIN").is_some()),
}
}
fn entry(&self, name: &str) -> Result<keyring::Entry, KeyStoreError> {
check_name(name)?;
keyring::Entry::new(self.service, name).map_err(keyring_error)
}
}
fn selected_service(test: bool, desktop_test: bool) -> &'static str {
if test || desktop_test {
TEST_SERVICE
} else {
SERVICE
}
}
fn check_name(name: &str) -> Result<(), KeyStoreError> {
if valid_name(name, "key:") {
Ok(())
} else {
Err(KeyStoreError::InvalidInput)
}
}
fn keyring_error(error: keyring::Error) -> KeyStoreError {
match error {
keyring::Error::NoStorageAccess(_) => KeyStoreError::AccessDenied,
keyring::Error::Invalid(_, _) | keyring::Error::TooLong(_, _) => {
KeyStoreError::InvalidInput
}
keyring::Error::BadEncoding(bytes) | keyring::Error::BadDataFormat(bytes, _) => {
drop(Zeroizing::new(bytes));
KeyStoreError::Unavailable
}
_ => KeyStoreError::Unavailable,
}
}
fn secret_result(
result: keyring::Result<Vec<u8>>,
) -> Result<Option<Zeroizing<Vec<u8>>>, KeyStoreError> {
match result.map(Zeroizing::new) {
Ok(bytes) if !bytes.is_empty() && bytes.len() <= MAX_KEY_LEN => Ok(Some(bytes)),
Ok(_) => Err(KeyStoreError::InvalidInput),
Err(keyring::Error::NoEntry) => Ok(None),
Err(error) => Err(keyring_error(error)),
}
}
impl KeyStore for OsKeyStore {
fn orphan_detection(&self) -> OrphanDetection {
platform::ORPHAN_DETECTION
}
fn put(&self, name: &str, der: &[u8]) -> Result<(), KeyStoreError> {
check_name(name)?;
if der.is_empty() || der.len() > MAX_KEY_LEN {
return Err(KeyStoreError::InvalidInput);
}
let _creation = CREATE_LOCK.lock().map_err(|_| KeyStoreError::Unavailable)?;
if self.exists(name)? {
return Err(KeyStoreError::AlreadyExists);
}
if self.orphan_detection() == OrphanDetection::Available && self.names()?.len() >= MAX_KEYS
{
return Err(KeyStoreError::InvalidInput);
}
self.entry(name)?.set_secret(der).map_err(keyring_error)
}
fn get(&self, name: &str) -> Result<Option<Zeroizing<Vec<u8>>>, KeyStoreError> {
secret_result(self.entry(name)?.get_secret())
}
fn delete(&self, name: &str) -> Result<(), KeyStoreError> {
match self.entry(name)?.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(error) => Err(keyring_error(error)),
}
}
fn exists(&self, name: &str) -> Result<bool, KeyStoreError> {
check_name(name)?;
platform::exists(self.service, name)
}
fn names(&self) -> Result<Vec<String>, KeyStoreError> {
let mut names = platform::names(self.service)?;
names.sort();
names.dedup();
Ok(names)
}
}
#[cfg(any(unix, test))]
fn push_name(names: &mut Vec<String>, name: String) -> Result<(), KeyStoreError> {
check_name(&name)?;
if names.len() >= MAX_KEYS {
return Err(KeyStoreError::InvalidInput);
}
names.push(name);
Ok(())
}
#[cfg(any(all(unix, not(target_os = "macos")), test))]
fn check_match_count(unlocked: usize, locked: usize) -> Result<(), KeyStoreError> {
if unlocked.saturating_add(locked) > MAX_KEYS {
return Err(KeyStoreError::InvalidInput);
}
Ok(())
}
#[cfg(all(unix, not(target_os = "macos")))]
mod platform {
use std::collections::HashMap;
use secret_service::{EncryptionType, blocking::SecretService};
use super::*;
pub(super) const ORPHAN_DETECTION: OrphanDetection = OrphanDetection::Available;
fn service_error(error: secret_service::Error) -> KeyStoreError {
match error {
secret_service::Error::Locked
| secret_service::Error::Prompt
| secret_service::Error::PromptDisconnected => KeyStoreError::AccessDenied,
_ => KeyStoreError::Unavailable,
}
}
pub(super) fn exists(service: &str, name: &str) -> Result<bool, KeyStoreError> {
let ss = SecretService::connect(EncryptionType::Dh).map_err(service_error)?;
let found = ss
.search_items(HashMap::from([("service", service), ("username", name)]))
.map_err(service_error)?;
check_match_count(found.unlocked.len(), found.locked.len())?;
Ok(!found.unlocked.is_empty() || !found.locked.is_empty())
}
pub(super) fn names(service: &str) -> Result<Vec<String>, KeyStoreError> {
let ss = SecretService::connect(EncryptionType::Dh).map_err(service_error)?;
let found = ss
.search_items(HashMap::from([("service", service)]))
.map_err(service_error)?;
check_match_count(found.unlocked.len(), found.locked.len())?;
let mut names = Vec::new();
for item in found.unlocked.into_iter().chain(found.locked) {
let mut attributes = item.get_attributes().map_err(service_error)?;
if attributes.get("service").map(String::as_str) != Some(service) {
return Err(KeyStoreError::InvalidInput);
}
let name = attributes
.remove("username")
.ok_or(KeyStoreError::InvalidInput)?;
push_name(&mut names, name)?;
}
Ok(names)
}
}
#[cfg(target_os = "macos")]
mod platform {
use security_framework::item::{ItemClass, ItemSearchOptions};
use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain};
use super::*;
pub(super) const ORPHAN_DETECTION: OrphanDetection = OrphanDetection::Available;
fn service_error(error: security_framework::base::Error) -> KeyStoreError {
match error.code() {
-25293 | -25308 | -128 => KeyStoreError::AccessDenied,
_ => KeyStoreError::Unavailable,
}
}
fn search(
service: &str,
name: Option<&str>,
limit: i64,
) -> Result<Vec<security_framework::item::SearchResult>, KeyStoreError> {
let keychains = [
SecKeychain::default_for_domain(SecPreferencesDomain::User).map_err(service_error)?
];
let mut options = ItemSearchOptions::new();
options
.keychains(&keychains)
.class(ItemClass::generic_password())
.service(service)
.load_attributes(true)
.limit(limit);
if let Some(name) = name {
options.account(name);
}
match options.search() {
Ok(items) => Ok(items),
Err(error) => match error.code() {
-25300 => Ok(Vec::new()), _ => Err(service_error(error)),
},
}
}
pub(super) fn exists(service: &str, name: &str) -> Result<bool, KeyStoreError> {
Ok(!search(service, Some(name), 1)?.is_empty())
}
pub(super) fn names(service: &str) -> Result<Vec<String>, KeyStoreError> {
let found = search(service, None, MAX_KEYS as i64 + 1)?;
if found.len() > MAX_KEYS {
return Err(KeyStoreError::InvalidInput);
}
let mut names = Vec::new();
for item in found {
let mut attributes = item.simplify_dict().ok_or(KeyStoreError::InvalidInput)?;
if attributes.get("svce").map(String::as_str) != Some(service) {
return Err(KeyStoreError::InvalidInput);
}
let name = attributes
.remove("acct")
.ok_or(KeyStoreError::InvalidInput)?;
push_name(&mut names, name)?;
}
Ok(names)
}
}
#[cfg(any(windows, test))]
mod windows {
use super::*;
pub(super) const ORPHAN_DETECTION: OrphanDetection = OrphanDetection::Unavailable;
#[cfg(windows)]
pub(super) fn exists(service: &str, name: &str) -> Result<bool, KeyStoreError> {
let entry = keyring::Entry::new(service, name).map_err(keyring_error)?;
match entry.inner.get_attributes() {
Ok(_) => Ok(true),
Err(keyring::Error::NoEntry) => Ok(false),
Err(error) => Err(keyring_error(error)),
}
}
pub(super) fn names(_: &str) -> Result<Vec<String>, KeyStoreError> {
Err(KeyStoreError::EnumerationUnavailable)
}
}
#[cfg(windows)]
use windows as platform;
#[cfg(not(any(unix, windows)))]
mod platform {
use super::*;
pub(super) const ORPHAN_DETECTION: OrphanDetection = OrphanDetection::Unavailable;
pub(super) fn exists(_: &str, _: &str) -> Result<bool, KeyStoreError> {
Err(KeyStoreError::Unavailable)
}
pub(super) fn names(_: &str) -> Result<Vec<String>, KeyStoreError> {
Err(KeyStoreError::Unavailable)
}
}
#[cfg(test)]
mod tests;