diff options
Diffstat (limited to 'vendor/security-framework/src/passwords.rs')
-rw-r--r-- | vendor/security-framework/src/passwords.rs | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/vendor/security-framework/src/passwords.rs b/vendor/security-framework/src/passwords.rs new file mode 100644 index 000000000..83dad6d28 --- /dev/null +++ b/vendor/security-framework/src/passwords.rs @@ -0,0 +1,332 @@ +//! 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<Vec<u8>> { + 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<u16>, + 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<u16>, + protocol: SecProtocolType, + authentication_type: SecAuthenticationType, +) -> Result<Vec<u8>> { + 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<u16>, + 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<Vec<u8>> { + 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"); + } +} |