keyring-manager 0.9.0

Cross-platform library for managing passwords
Documentation
use crate::error::{KeyringError, Result};
use crate::*;
use security_framework::base::Error as SfError;
use security_framework::item::{ItemClass, ItemSearchOptions, Limit};
use security_framework::passwords::{
    delete_generic_password, get_generic_password, set_generic_password,
};

pub type OSStatus = i32;

#[allow(non_upper_case_globals)]
const errSecItemNotFound: OSStatus = -25300;

#[allow(non_upper_case_globals)]
#[inline(always)]
fn map_keyring_error(err: SfError) -> KeyringError {
    match err.code() {
        errSecItemNotFound => KeyringError::NoPasswordFound,
        err => KeyringError::from(err),
    }
}

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>> {
        // Matching on an empty service attribute is unreliable, and an iOS app only sees its own
        // keychain access group, so enumerate generic passwords and filter by the account prefix.
        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,
            // No matching items is an empty list, not an error.
            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();
        set_generic_password("", &account_name, value.as_bytes()).map_err(map_keyring_error)?;
        Ok(())
    }

    fn get_value(&self) -> Result<String> {
        let account_name = self.make_key_string();
        let data = get_generic_password("", &account_name).map_err(map_keyring_error)?;
        let mut buf = Vec::new();
        buf.extend_from_slice(&data);
        String::from_utf8(buf).map_err(|e| {
            KeyringError::from(format!("couldn't convert keychain data to string: {}", e))
        })
    }

    fn delete_value(&mut self) -> Result<()> {
        let account_name = self.make_key_string();
        delete_generic_password("", &account_name).map_err(map_keyring_error)?;
        Ok(())
    }
}