use core::{
fmt::Display,
ptr::{self, NonNull},
slice,
};
use anyhow::{Context, Result};
use objc2::runtime::AnyObject;
use objc2_core_foundation::{
CFDictionary, CFMutableData, CFRetained, CFString, CFType, kCFBooleanTrue,
};
use objc2_foundation::NSString;
use objc2_local_authentication::LAContext;
use objc2_security::{
SecAccessControl, SecAccessControlCreateFlags, SecItemAdd, SecItemCopyMatching, SecItemDelete,
errSecItemNotFound, errSecSuccess, errSecUserCanceled, kSecAttrAccessControl,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAttrAccount, kSecAttrService, kSecClass,
kSecClassGenericPassword, kSecReturnData, kSecUseAuthenticationContext, kSecValueData,
};
use zeroize::{Zeroize, ZeroizeOnDrop};
pub(crate) struct Entry {
pub service: CFRetained<CFString>,
pub account: CFRetained<CFString>,
}
impl Entry {
pub fn new(service: &str, account: &str) -> Result<Self> {
Ok(Entry {
service: CFString::from_str(service),
account: CFString::from_str(account),
})
}
pub fn set_password(&self, password: &str) -> Result<()> {
let access_control = unsafe {
SecAccessControl::with_flags(
None,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
SecAccessControlCreateFlags::BiometryAny,
ptr::null_mut(),
)
}
.context("failed creating access control")?;
let password_bytes = password.as_bytes();
let password = SecureData(
CFMutableData::new(None, password_bytes.len() as isize)
.context("failed to allocate buffer")?,
);
unsafe {
CFMutableData::append_bytes(
Some(&password.0),
password_bytes.as_ptr(),
password_bytes.len() as isize,
);
}
let query = unsafe {
CFDictionary::from_slices(
&[
kSecClass,
kSecAttrService,
kSecAttrAccount,
kSecValueData,
kSecAttrAccessControl,
],
&[
kSecClassGenericPassword as &CFType,
&self.service,
&self.account,
&password.0,
&access_control,
],
)
};
let status = unsafe { SecItemAdd(query.as_opaque(), ptr::null_mut()) };
if status != errSecSuccess {
anyhow::bail!("failed to set password: {status}");
}
Ok(())
}
pub fn get_password(&self) -> core::result::Result<String, Error> {
let reason = NSString::from_str("load your seed password");
let context = unsafe { LAContext::new() };
unsafe { context.setLocalizedReason(&reason) };
let context = unsafe { core::mem::transmute::<&AnyObject, &CFType>(&context) };
let query = unsafe {
CFDictionary::from_slices(
&[
kSecClass,
kSecAttrService,
kSecAttrAccount,
kSecReturnData,
kSecUseAuthenticationContext,
],
&[
kSecClassGenericPassword as &CFType,
&self.service,
&self.account,
kCFBooleanTrue.unwrap(),
context,
],
)
};
let mut result: *const CFType = ptr::null();
let status =
unsafe { SecItemCopyMatching(query.as_opaque(), &mut result as *mut *const CFType) };
if status == errSecUserCanceled {
return Err(Error::Other(anyhow::anyhow!("Authentication canceled")));
} else if status == errSecItemNotFound {
return Err(Error::NoEntry);
} else if status != errSecSuccess {
return Err(Error::Other(anyhow::anyhow!(
"failed to load password: {status:?}"
)));
}
let result = NonNull::new(result as *mut CFMutableData)
.context("nil result from keychain")
.map_err(Error::Other)?;
let result = SecureData(unsafe { CFRetained::from_raw(result) });
let password = str::from_utf8(unsafe { result.0.as_bytes_unchecked() })
.context("non-utf8 password; delete with -r")
.map_err(Error::Other)?;
Ok(String::from(password))
}
pub fn delete_credential(&self) -> core::result::Result<(), Error> {
let query = unsafe {
CFDictionary::from_slices(
&[kSecClass, kSecAttrService, kSecAttrAccount],
&[kSecClassGenericPassword, &self.service, &self.account],
)
};
let status = unsafe { SecItemDelete(query.as_opaque()) };
if status == errSecItemNotFound {
return Err(Error::NoEntry);
} else if status != errSecSuccess {
return Err(Error::Other(anyhow::anyhow!(
"failed to delete password: {status:?}"
)));
}
Ok(())
}
}
#[derive(Debug)]
pub(crate) enum Error {
NoEntry,
Other(anyhow::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Error::NoEntry => write!(f, "entry not found"),
Error::Other(err) => err.fmt(f),
}
}
}
impl core::error::Error for Error {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Error::Other(err) => Some(err.as_ref()),
_ => None,
}
}
}
struct SecureData(CFRetained<CFMutableData>);
impl Drop for SecureData {
fn drop(&mut self) {
unsafe {
let ptr = CFMutableData::mutable_byte_ptr(Some(&self.0));
let len = self.0.len();
if !ptr.is_null() {
let slice: &mut [u8] = slice::from_raw_parts_mut(ptr, len);
slice.zeroize();
}
}
}
}
impl ZeroizeOnDrop for SecureData {}