//! Support for password entries in the keychain. Works on both iOS and macOS. //! //! If you want the extended keychain facilities only available on macOS, use the //! version of these functions in the macOS extensions module. use crate::base::Result; use crate::passwords_options::PasswordOptions; use crate::{cvt, Error}; use core_foundation::base::TCFType; use core_foundation::boolean::CFBoolean; use core_foundation::data::CFData; use core_foundation::dictionary::CFDictionary; use core_foundation::string::CFString; use core_foundation_sys::base::{CFGetTypeID, CFRelease, CFTypeRef}; use core_foundation_sys::data::CFDataRef; use security_framework_sys::base::{errSecDuplicateItem, errSecParam}; use security_framework_sys::item::{kSecReturnData, kSecValueData}; use security_framework_sys::keychain::{SecAuthenticationType, SecProtocolType}; use security_framework_sys::keychain_item::{ SecItemAdd, SecItemCopyMatching, SecItemDelete, SecItemUpdate, }; /// Set a generic password for the given service and account. /// Creates or updates a keychain entry. pub fn set_generic_password(service: &str, account: &str, password: &[u8]) -> Result<()> { let mut options = PasswordOptions::new_generic_password(service, account); set_password_internal(&mut options, password) } /// Get the generic password for the given service and account. If no matching /// keychain entry exists, fails with error code `errSecItemNotFound`. pub fn get_generic_password(service: &str, account: &str) -> Result> { let mut options = PasswordOptions::new_generic_password(service, account); options.query.push(( unsafe { CFString::wrap_under_get_rule(kSecReturnData) }, CFBoolean::from(true).into_CFType(), )); let params = CFDictionary::from_CFType_pairs(&options.query); let mut ret: CFTypeRef = std::ptr::null(); cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?; get_password_and_release(ret) } /// Delete the generic password keychain entry for the given service and account. /// If none exists, fails with error code `errSecItemNotFound`. pub fn delete_generic_password(service: &str, account: &str) -> Result<()> { let options = PasswordOptions::new_generic_password(service, account); let params = CFDictionary::from_CFType_pairs(&options.query); cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) }) } /// Set an internet password for the given endpoint parameters. /// Creates or updates a keychain entry. #[allow(clippy::too_many_arguments)] pub fn set_internet_password( server: &str, security_domain: Option<&str>, account: &str, path: &str, port: Option, protocol: SecProtocolType, authentication_type: SecAuthenticationType, password: &[u8], ) -> Result<()> { let mut options = PasswordOptions::new_internet_password( server, security_domain, account, path, port, protocol, authentication_type, ); set_password_internal(&mut options, password) } /// Get the internet password for the given endpoint parameters. If no matching /// keychain entry exists, fails with error code `errSecItemNotFound`. pub fn get_internet_password( server: &str, security_domain: Option<&str>, account: &str, path: &str, port: Option, protocol: SecProtocolType, authentication_type: SecAuthenticationType, ) -> Result> { let mut options = PasswordOptions::new_internet_password( server, security_domain, account, path, port, protocol, authentication_type, ); options.query.push(( unsafe { CFString::wrap_under_get_rule(kSecReturnData) }, CFBoolean::from(true).into_CFType(), )); let params = CFDictionary::from_CFType_pairs(&options.query); let mut ret: CFTypeRef = std::ptr::null(); cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?; get_password_and_release(ret) } /// Delete the internet password for the given endpoint parameters. /// If none exists, fails with error code `errSecItemNotFound`. pub fn delete_internet_password( server: &str, security_domain: Option<&str>, account: &str, path: &str, port: Option, protocol: SecProtocolType, authentication_type: SecAuthenticationType, ) -> Result<()> { let options = PasswordOptions::new_internet_password( server, security_domain, account, path, port, protocol, authentication_type, ); let params = CFDictionary::from_CFType_pairs(&options.query); cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) }) } // This starts by trying to create the password with the given query params. // If the creation attempt reveals that one exists, its password is updated. fn set_password_internal(options: &mut PasswordOptions, password: &[u8]) -> Result<()> { let query_len = options.query.len(); options.query.push(( unsafe { CFString::wrap_under_get_rule(kSecValueData) }, CFData::from_buffer(password).into_CFType(), )); let params = CFDictionary::from_CFType_pairs(&options.query); let mut ret = std::ptr::null(); let status = unsafe { SecItemAdd(params.as_concrete_TypeRef(), &mut ret) }; if status == errSecDuplicateItem { let params = CFDictionary::from_CFType_pairs(&options.query[0..query_len]); let update = CFDictionary::from_CFType_pairs(&options.query[query_len..]); cvt(unsafe { SecItemUpdate(params.as_concrete_TypeRef(), update.as_concrete_TypeRef()) }) } else { cvt(status) } } // Having retrieved a password entry, this copies and returns the password. // // # Safety // The data element passed in is assumed to have been returned from a Copy // call, so it's released after we are done with it. fn get_password_and_release(data: CFTypeRef) -> Result> { if !data.is_null() { let type_id = unsafe { CFGetTypeID(data) }; if type_id == CFData::type_id() { let val = unsafe { CFData::wrap_under_create_rule(data as CFDataRef) }; let mut vec = Vec::new(); vec.extend_from_slice(val.bytes()); return Ok(vec); } else { // unexpected: we got a reference to some other type. // Release it to make sure there's no leak, but // we can't return the password in this case. unsafe { CFRelease(data) }; } } Err(Error::from_code(errSecParam)) } #[cfg(test)] mod test { use super::*; use security_framework_sys::base::errSecItemNotFound; #[test] fn missing_generic() { let name = "a string not likely to already be in the keychain as service or account"; let result = delete_generic_password(name, name); match result { Ok(()) => (), // this is ok because the name _might_ be in the keychain Err(err) if err.code() == errSecItemNotFound => (), Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()), }; let result = get_generic_password(name, name); match result { Ok(bytes) => panic!("missing_generic: get returned {:?}", bytes), Err(err) if err.code() == errSecItemNotFound => (), Err(err) => panic!("missing_generic: get failed with status: {}", err.code()), }; let result = delete_generic_password(name, name); match result { Ok(()) => panic!("missing_generic: second delete found a password"), Err(err) if err.code() == errSecItemNotFound => (), Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()), }; } #[test] fn roundtrip_generic() { let name = "roundtrip_generic"; set_generic_password(name, name, name.as_bytes()).expect("set_generic_password"); let pass = get_generic_password(name, name).expect("get_generic_password"); assert_eq!(name.as_bytes(), pass); delete_generic_password(name, name).expect("delete_generic_password") } #[test] fn update_generic() { let name = "update_generic"; set_generic_password(name, name, name.as_bytes()).expect("set_generic_password"); let alternate = "update_generic_alternate"; set_generic_password(name, name, alternate.as_bytes()).expect("set_generic_password"); let pass = get_generic_password(name, name).expect("get_generic_password"); assert_eq!(pass, alternate.as_bytes()); delete_generic_password(name, name).expect("delete_generic_password") } #[test] fn missing_internet() { let name = "a string not likely to already be in the keychain as service or account"; let (server, domain, account, path, port, protocol, auth) = ( name, None, name, "/", Some(8080u16), SecProtocolType::HTTP, SecAuthenticationType::Any, ); let result = delete_internet_password(server, domain, account, path, port, protocol, auth); match result { Ok(()) => (), // this is ok because the name _might_ be in the keychain Err(err) if err.code() == errSecItemNotFound => (), Err(err) => panic!( "missing_internet: delete failed with status: {}", err.code() ), }; let result = get_internet_password(server, domain, account, path, port, protocol, auth); match result { Ok(bytes) => panic!("missing_internet: get returned {:?}", bytes), Err(err) if err.code() == errSecItemNotFound => (), Err(err) => panic!("missing_internet: get failed with status: {}", err.code()), }; let result = delete_internet_password(server, domain, account, path, port, protocol, auth); match result { Ok(()) => panic!("missing_internet: second delete found a password"), Err(err) if err.code() == errSecItemNotFound => (), Err(err) => panic!( "missing_internet: delete failed with status: {}", err.code() ), }; } #[test] fn roundtrip_internet() { let name = "roundtrip_internet"; let (server, domain, account, path, port, protocol, auth) = ( name, None, name, "/", Some(8080u16), SecProtocolType::HTTP, SecAuthenticationType::Any, ); set_internet_password( server, domain, account, path, port, protocol, auth, name.as_bytes(), ) .expect("set_internet_password"); let pass = get_internet_password(server, domain, account, path, port, protocol, auth) .expect("get_internet_password"); assert_eq!(name.as_bytes(), pass); delete_internet_password(server, domain, account, path, port, protocol, auth) .expect("delete_internet_password"); } #[test] fn update_internet() { let name = "update_internet"; let (server, domain, account, path, port, protocol, auth) = ( name, None, name, "/", Some(8080u16), SecProtocolType::HTTP, SecAuthenticationType::Any, ); set_internet_password( server, domain, account, path, port, protocol, auth, name.as_bytes(), ) .expect("set_internet_password"); let alternate = "alternate_internet_password"; set_internet_password( server, domain, account, path, port, protocol, auth, alternate.as_bytes(), ) .expect("set_internet_password"); let pass = get_internet_password(server, domain, account, path, port, protocol, auth) .expect("get_internet_password"); assert_eq!(pass, alternate.as_bytes()); delete_internet_password(server, domain, account, path, port, protocol, auth) .expect("delete_internet_password"); } }