#[doc(inline)]
pub use crate::passwords_options::{AccessControlOptions, PasswordOptions};
use crate::base::Result;
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_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,
};
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)
}
pub fn set_generic_password_options(password: &[u8], mut options: PasswordOptions) -> Result<()> {
set_password_internal(&mut options, password)
}
#[doc(hidden)]
pub fn get_generic_password(service: &str, account: &str) -> Result<Vec<u8>> {
generic_password(PasswordOptions::new_generic_password(service, account))
}
pub fn generic_password(mut options: PasswordOptions) -> Result<Vec<u8>> {
unsafe { options.push_query(kSecReturnData, CFBoolean::from(true)); }
let params = options.to_dictionary();
let mut ret: CFTypeRef = std::ptr::null();
cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?;
get_password_and_release(ret)
}
pub fn delete_generic_password(service: &str, account: &str) -> Result<()> {
let options = PasswordOptions::new_generic_password(service, account);
delete_generic_password_options(options)
}
pub fn delete_generic_password_options(options: PasswordOptions) -> Result<()> {
let params = options.to_dictionary();
cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
}
#[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)
}
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,
);
unsafe { options.push_query(kSecReturnData, CFBoolean::from(true)); }
let params = options.to_dictionary();
let mut ret: CFTypeRef = std::ptr::null();
cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?;
get_password_and_release(ret)
}
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 = options.to_dictionary();
cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
}
fn set_password_internal(options: &mut PasswordOptions, password: &[u8]) -> Result<()> {
#[allow(deprecated)]
let query_without_password = options.query.len();
unsafe { options.push_query(kSecValueData, CFData::from_buffer(password)); }
let params = options.to_dictionary();
let mut ret = std::ptr::null();
let status = unsafe { SecItemAdd(params.as_concrete_TypeRef(), &mut ret) };
if status == errSecDuplicateItem {
#[allow(deprecated)]
let (query, pass) = options.query.split_at(query_without_password);
let params = CFDictionary::from_CFType_pairs(query);
let update = CFDictionary::from_CFType_pairs(pass);
cvt(unsafe { SecItemUpdate(params.as_concrete_TypeRef(), update.as_concrete_TypeRef()) })
} else {
cvt(status)
}
}
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();
if !val.is_empty() {
vec.extend_from_slice(val.bytes());
}
return Ok(vec);
}
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(()) => (), 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]
#[cfg(feature = "OSX_10_12")]
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(()) => (), 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);
let _ = delete_internet_password(server, domain, account, path, port, protocol, auth);
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");
}
}