use crate::error::{KeyringError, Result};
use crate::*;
use core_foundation::array::CFArray;
use core_foundation::base::{CFGetTypeID, CFRelease, CFType, CFTypeRef, TCFType};
use core_foundation::boolean::CFBoolean;
use core_foundation::data::CFData;
use core_foundation::dictionary::CFDictionary;
use core_foundation::string::{CFString, CFStringRef};
use security_framework::base::Error as SfError;
use security_framework::item::{ItemClass, ItemSearchOptions, Limit};
use security_framework::passwords::delete_generic_password;
use security_framework_sys::access_control::{
kSecAttrAccessibleAfterFirstUnlock, kSecAttrAccessibleWhenUnlocked,
};
use security_framework_sys::base::{errSecDuplicateItem, errSecItemNotFound};
use security_framework_sys::item::{
kSecAttrAccount, kSecAttrService, kSecClass, kSecClassGenericPassword, kSecMatchLimit,
kSecMatchLimitAll, kSecReturnAttributes, kSecReturnData, kSecValueData,
};
use security_framework_sys::keychain_item::{SecItemAdd, SecItemCopyMatching, SecItemUpdate};
use std::sync::atomic::{AtomicU64, Ordering};
pub type OSStatus = i32;
#[allow(non_upper_case_globals)]
#[link(name = "Security", kind = "framework")]
extern "C" {
static kSecAttrAccessible: CFStringRef;
}
#[allow(non_upper_case_globals)]
const errSecInteractionNotAllowed: OSStatus = -25308;
static PROMOTIONS: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "keyring_manager_ios_tests")]
pub(crate) fn promotion_count() -> u64 {
PROMOTIONS.load(Ordering::Relaxed)
}
#[allow(non_upper_case_globals)]
fn map_status(status: OSStatus) -> KeyringError {
match status {
errSecItemNotFound => KeyringError::NoPasswordFound,
errSecInteractionNotAllowed => KeyringError::Locked,
status => KeyringError::from(status),
}
}
#[inline(always)]
fn map_keyring_error(err: SfError) -> KeyringError {
map_status(err.code())
}
fn cf_static(key: CFStringRef) -> CFString {
unsafe { CFString::wrap_under_get_rule(key) }
}
fn item_query(account_name: &str) -> Vec<(CFString, CFType)> {
vec![
(
cf_static(unsafe { kSecClass }),
cf_static(unsafe { kSecClassGenericPassword }).into_CFType(),
),
(
cf_static(unsafe { kSecAttrService }),
CFString::from("").into_CFType(),
),
(
cf_static(unsafe { kSecAttrAccount }),
CFString::from(account_name).into_CFType(),
),
]
}
fn target_accessibility() -> CFString {
cf_static(unsafe { kSecAttrAccessibleAfterFirstUnlock })
}
fn accessible_pair(class: CFString) -> (CFString, CFType) {
(
cf_static(unsafe { kSecAttrAccessible }),
class.into_CFType(),
)
}
fn data_pair(value: &[u8]) -> (CFString, CFType) {
(
cf_static(unsafe { kSecValueData }),
CFData::from_buffer(value).into_CFType(),
)
}
fn flag_pair(key: CFStringRef) -> (CFString, CFType) {
(cf_static(key), CFBoolean::from(true).into_CFType())
}
fn accessibility_is_current(item: &CFDictionary<CFType, CFType>) -> Option<bool> {
item.find(cf_static(unsafe { kSecAttrAccessible }).into_CFType())
.and_then(|v| v.downcast::<CFString>())
.map(|v| v == target_accessibility())
}
fn all_current(items: &[CFDictionary<CFType, CFType>]) -> Option<bool> {
let mut result = Some(true);
for item in items {
match accessibility_is_current(item) {
Some(false) => return Some(false),
Some(true) => {}
None => result = None,
}
}
result
}
fn item_data(item: &CFDictionary<CFType, CFType>) -> Result<Vec<u8>> {
item.find(cf_static(unsafe { kSecValueData }).into_CFType())
.and_then(|v| v.downcast::<CFData>())
.map(|v| v.bytes().to_vec())
.ok_or_else(|| map_to_generic("keychain item has no data"))
}
fn copy_all(mut query: Vec<(CFString, CFType)>) -> Result<Vec<CFDictionary<CFType, CFType>>> {
query.push((
cf_static(unsafe { kSecMatchLimit }),
cf_static(unsafe { kSecMatchLimitAll }).into_CFType(),
));
let params = CFDictionary::from_CFType_pairs(&query);
let mut ret: CFTypeRef = std::ptr::null();
let status = unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) };
if status != 0 {
return Err(map_status(status));
}
if ret.is_null() {
return Ok(Vec::new());
}
if unsafe { CFGetTypeID(ret) } != CFArray::<CFType>::type_id() {
unsafe { CFRelease(ret) };
return Err(map_to_generic(
"keychain returned an unexpected result type",
));
}
let array: CFArray<CFType> = unsafe { CFArray::wrap_under_create_rule(ret as _) };
let mut items = Vec::with_capacity(array.len() as usize);
for entry in array.iter() {
if !entry.instance_of::<CFDictionary<CFType, CFType>>() {
return Err(map_to_generic("keychain returned an unexpected item type"));
}
items.push(unsafe { CFDictionary::wrap_under_get_rule(entry.as_CFTypeRef() as _) });
}
Ok(items)
}
fn copy_attributes(account_name: &str) -> Result<Vec<CFDictionary<CFType, CFType>>> {
let mut query = item_query(account_name);
query.push(flag_pair(unsafe { kSecReturnAttributes }));
copy_all(query)
}
fn keychain_unlocked() -> bool {
let account = format!("\u{1}keyring-manager-probe\t{}", probe_nonce());
let mut attrs = item_query(&account);
attrs.push(accessible_pair(cf_static(unsafe {
kSecAttrAccessibleWhenUnlocked
})));
attrs.push(data_pair(&[0u8]));
let params = CFDictionary::from_CFType_pairs(&attrs);
let status = unsafe { SecItemAdd(params.as_concrete_TypeRef(), std::ptr::null_mut()) };
if status == 0 {
let _ = delete_generic_password("", &account);
return true;
}
if status == errSecDuplicateItem {
let _ = delete_generic_password("", &account);
}
warn!("keychain probe failed, absence is unproven: {}", status);
false
}
fn probe_nonce() -> u64 {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
nanos ^ (COUNTER.fetch_add(1, Ordering::Relaxed) << 40) ^ ((std::process::id() as u64) << 20)
}
fn absence_or_locked() -> KeyringError {
if keychain_unlocked() {
KeyringError::NoPasswordFound
} else {
KeyringError::Locked
}
}
pub(crate) fn promote_accessibility(account_name: &str) -> bool {
PROMOTIONS.fetch_add(1, Ordering::Relaxed);
let query = CFDictionary::from_CFType_pairs(&item_query(account_name));
let update = CFDictionary::from_CFType_pairs(&[accessible_pair(target_accessibility())]);
let status =
unsafe { SecItemUpdate(query.as_concrete_TypeRef(), update.as_concrete_TypeRef()) };
if status != 0 {
warn!("keychain accessibility update failed: {}", status);
return false;
}
match copy_attributes(account_name).map(|items| all_current(&items)) {
Ok(Some(true)) => {
info!("keychain accessibility updated");
true
}
Ok(Some(false)) => {
error!("keychain accepted an accessibility update that did not take; the item stays unreadable while locked");
false
}
Ok(None) | Err(_) => {
warn!("keychain accessibility update could not be verified");
false
}
}
}
pub struct IosKeyringManager {
application: String,
}
impl IosKeyringManager {
pub fn new(application: &str) -> Result<Self> {
Ok(IosKeyringManager {
application: application.to_owned(),
})
}
pub fn with_keyring<F, T>(&self, service: &str, key: &str, func: F) -> Result<T>
where
F: FnOnce(&mut dyn Keyring) -> Result<T>,
{
let mut kr = IosKeyring::new(&self.application, service, key)?;
func(&mut kr)
}
pub fn list_keys(&self, service: &str) -> Result<Vec<String>> {
let prefix = format!(
"{}\t{}\t",
crate::escape(&self.application),
crate::escape(service)
);
let results = match ItemSearchOptions::new()
.class(ItemClass::generic_password())
.limit(Limit::All)
.load_attributes(true)
.search()
{
Ok(r) => r,
Err(e) if e.code() == errSecItemNotFound => Vec::new(),
Err(e) => return Err(map_keyring_error(e)),
};
results
.iter()
.filter_map(|r| r.simplify_dict()?.get("acct").cloned())
.filter_map(|acct| acct.strip_prefix(&prefix).map(str::to_owned))
.map(|key| crate::unescape(&key).map_err(map_to_generic))
.collect()
}
}
pub struct IosKeyring<'a> {
application: &'a str,
service: &'a str,
key: &'a str,
}
impl<'a> IosKeyring<'a> {
fn new(application: &'a str, service: &'a str, key: &'a str) -> Result<IosKeyring<'a>> {
Ok(IosKeyring {
application,
service,
key,
})
}
fn make_key_string(&self) -> String {
[
crate::escape(self.application),
crate::escape(self.service),
crate::escape(self.key),
]
.join("\t")
}
}
impl<'a> Keyring for IosKeyring<'a> {
fn set_value(&mut self, value: &str) -> Result<()> {
let account_name = self.make_key_string();
for _ in 0..3 {
let mut attrs = item_query(&account_name);
attrs.push(accessible_pair(target_accessibility()));
attrs.push(data_pair(value.as_bytes()));
let params = CFDictionary::from_CFType_pairs(&attrs);
let status = unsafe { SecItemAdd(params.as_concrete_TypeRef(), std::ptr::null_mut()) };
if status == 0 {
return Ok(());
}
if status != errSecDuplicateItem {
return Err(map_status(status));
}
let query = CFDictionary::from_CFType_pairs(&item_query(&account_name));
let update = CFDictionary::from_CFType_pairs(&[data_pair(value.as_bytes())]);
let status =
unsafe { SecItemUpdate(query.as_concrete_TypeRef(), update.as_concrete_TypeRef()) };
if status == errSecItemNotFound {
continue;
}
if status != 0 {
return Err(map_status(status));
}
promote_accessibility(&account_name);
return Ok(());
}
Err(map_to_generic("keychain item changed under the write"))
}
fn get_value(&self) -> Result<String> {
let account_name = self.make_key_string();
let mut query = item_query(&account_name);
query.push(flag_pair(unsafe { kSecReturnData }));
query.push(flag_pair(unsafe { kSecReturnAttributes }));
let items = match copy_all(query) {
Ok(items) => items,
Err(KeyringError::NoPasswordFound) => return Err(absence_or_locked()),
Err(e) => return Err(e),
};
if items.is_empty() {
return Err(absence_or_locked());
}
let mut values = Vec::with_capacity(items.len());
for item in &items {
values.push(item_data(item)?);
}
if values.iter().any(|v| v != &values[0]) {
return Err(map_to_generic(
"keychain holds conflicting items for this key",
));
}
let value = String::from_utf8(values.swap_remove(0)).map_err(|e| {
KeyringError::from(format!("couldn't convert keychain data to string: {}", e))
})?;
let current = match all_current(&items) {
None => copy_attributes(&account_name)
.ok()
.and_then(|attrs| all_current(&attrs)),
known => known,
};
if current != Some(true) {
promote_accessibility(&account_name);
}
Ok(value)
}
fn delete_value(&mut self) -> Result<()> {
let account_name = self.make_key_string();
delete_generic_password("", &account_name).map_err(|e| match map_keyring_error(e) {
KeyringError::NoPasswordFound => absence_or_locked(),
e => e,
})?;
Ok(())
}
}