security-rs 0.2.1

Safe Rust bindings for Apple's Security framework — keychain, identity, certificates, trust, authorization, CMS, SecureTransport, and cryptographic primitives on macOS
Documentation
use std::ffi::CString;
use std::ptr;

use apple_cf::CFError;

use crate::error::{Result, SecurityError};
use crate::ffi;

pub struct OwnedCf {
    raw: ffi::CFTypeRef,
}

impl OwnedCf {
    pub const fn new(raw: ffi::CFTypeRef) -> Self {
        Self { raw }
    }

    pub const fn as_ptr(&self) -> ffi::CFTypeRef {
        self.raw
    }

    pub const fn as_string(&self) -> ffi::CFStringRef {
        self.raw.cast()
    }

    pub const fn as_data(&self) -> ffi::CFDataRef {
        self.raw.cast()
    }

    pub const fn as_array(&self) -> ffi::CFArrayRef {
        self.raw.cast()
    }

    pub const fn as_dictionary(&self) -> ffi::CFDictionaryRef {
        self.raw.cast()
    }

    pub const fn as_mut_dictionary(&self) -> ffi::CFMutableDictionaryRef {
        self.raw.cast_mut()
    }
}

impl Drop for OwnedCf {
    fn drop(&mut self) {
        if !self.raw.is_null() {
            unsafe { ffi::CFRelease(self.raw) };
        }
    }
}

pub fn checked_cf(raw: ffi::CFTypeRef, operation: &'static str) -> Result<OwnedCf> {
    if raw.is_null() {
        return Err(SecurityError::CoreFoundation(CFError::new(operation)));
    }
    Ok(OwnedCf::new(raw))
}

pub fn cf_string(value: &str) -> Result<OwnedCf> {
    let c_string = CString::new(value).map_err(|error| {
        SecurityError::InvalidArgument(format!("string contains interior NUL: {error}"))
    })?;
    let raw = unsafe {
        ffi::CFStringCreateWithCString(
            ffi::kCFAllocatorDefault,
            c_string.as_ptr(),
            ffi::kCFStringEncodingUTF8,
        )
    };
    checked_cf(raw.cast(), "CFStringCreateWithCString")
}

pub fn cf_data(bytes: &[u8]) -> Result<OwnedCf> {
    let length = isize::try_from(bytes.len()).map_err(|_| {
        SecurityError::InvalidArgument("byte slice is too large for CFDataCreate".to_owned())
    })?;
    let raw = unsafe { ffi::CFDataCreate(ffi::kCFAllocatorDefault, bytes.as_ptr(), length) };
    checked_cf(raw.cast(), "CFDataCreate")
}

pub fn cf_array(values: &[ffi::CFTypeRef]) -> Result<OwnedCf> {
    let count = isize::try_from(values.len()).map_err(|_| {
        SecurityError::InvalidArgument("array is too large for CFArrayCreate".to_owned())
    })?;
    let raw = unsafe {
        ffi::CFArrayCreate(
            ffi::kCFAllocatorDefault,
            values.as_ptr().cast(),
            count,
            &raw const ffi::kCFTypeArrayCallBacks,
        )
    };
    checked_cf(raw.cast(), "CFArrayCreate")
}

pub fn cf_mutable_dictionary(capacity: usize) -> Result<OwnedCf> {
    let capacity = isize::try_from(capacity).map_err(|_| {
        SecurityError::InvalidArgument("dictionary capacity exceeds CFIndex".to_owned())
    })?;
    let raw = unsafe {
        ffi::CFDictionaryCreateMutable(
            ffi::kCFAllocatorDefault,
            capacity,
            &raw const ffi::kCFTypeDictionaryKeyCallBacks,
            &raw const ffi::kCFTypeDictionaryValueCallBacks,
        )
    };
    checked_cf(raw.cast(), "CFDictionaryCreateMutable")
}

pub unsafe fn cf_dictionary_set_value(
    dictionary: ffi::CFMutableDictionaryRef,
    key: ffi::CFStringRef,
    value: ffi::CFTypeRef,
) {
    ffi::CFDictionarySetValue(dictionary, key.cast(), value.cast());
}

pub fn cf_dictionary_get_value(
    dictionary: ffi::CFDictionaryRef,
    key: ffi::CFStringRef,
) -> ffi::CFTypeRef {
    unsafe { ffi::CFDictionaryGetValue(dictionary, key.cast()).cast() }
}

pub fn cf_dictionary_entries(
    dictionary: ffi::CFDictionaryRef,
) -> Vec<(ffi::CFTypeRef, ffi::CFTypeRef)> {
    let count = unsafe { ffi::CFDictionaryGetCount(dictionary) };
    let Ok(count) = usize::try_from(count) else {
        return Vec::new();
    };
    let mut keys = vec![ptr::null(); count];
    let mut values = vec![ptr::null(); count];
    unsafe {
        ffi::CFDictionaryGetKeysAndValues(dictionary, keys.as_mut_ptr(), values.as_mut_ptr());
    };
    keys.into_iter().zip(values).collect()
}

pub fn cf_string_to_string(value: ffi::CFStringRef) -> Option<String> {
    if value.is_null() {
        return None;
    }
    let length = unsafe { ffi::CFStringGetLength(value) };
    let length = usize::try_from(length).ok()?;
    let capacity = length.checked_mul(4)?.checked_add(1)?;
    let capacity_index = isize::try_from(capacity).ok()?;
    let mut buffer = vec![0_u8; capacity];
    let ok = unsafe {
        ffi::CFStringGetCString(
            value,
            buffer.as_mut_ptr().cast(),
            capacity_index,
            ffi::kCFStringEncodingUTF8,
        )
    };
    if !ok {
        return None;
    }
    if let Some(end) = buffer.iter().position(|&byte| byte == 0) {
        buffer.truncate(end);
    }
    String::from_utf8(buffer).ok()
}

pub fn cf_data_to_vec(value: ffi::CFDataRef) -> Vec<u8> {
    if value.is_null() {
        return Vec::new();
    }
    let length = unsafe { ffi::CFDataGetLength(value) };
    let Ok(length) = usize::try_from(length) else {
        return Vec::new();
    };
    let bytes = unsafe { ffi::CFDataGetBytePtr(value) };
    if bytes.is_null() {
        return Vec::new();
    }
    unsafe { std::slice::from_raw_parts(bytes, length) }.to_vec()
}

pub fn cf_number_to_i64(value: ffi::CFNumberRef) -> Option<i64> {
    if value.is_null() {
        return None;
    }
    let mut out = 0_i64;
    let ok = unsafe {
        ffi::CFNumberGetValue(
            value,
            ffi::kCFNumberSInt64Type,
            std::ptr::addr_of_mut!(out).cast(),
        )
    };
    ok.then_some(out)
}

pub fn cf_boolean_to_bool(value: ffi::CFBooleanRef) -> bool {
    if value.is_null() {
        return false;
    }
    unsafe { ffi::CFBooleanGetValue(value) }
}

pub fn cf_type_id(value: ffi::CFTypeRef) -> ffi::CFTypeID {
    unsafe { ffi::CFGetTypeID(value) }
}

pub fn sec_error_message(status: ffi::OSStatus) -> String {
    let raw = unsafe { ffi::SecCopyErrorMessageString(status, ptr::null_mut()) };
    if raw.is_null() {
        return format!("OSStatus {status}");
    }
    let owned = OwnedCf::new(raw.cast());
    cf_string_to_string(owned.as_string()).unwrap_or_else(|| format!("OSStatus {status}"))
}

pub fn cf_error_description(error: ffi::CFErrorRef) -> String {
    if error.is_null() {
        return "trust evaluation failed".to_owned();
    }
    let raw = unsafe { ffi::CFErrorCopyDescription(error) };
    let description = if raw.is_null() {
        "trust evaluation failed".to_owned()
    } else {
        let owned = OwnedCf::new(raw.cast());
        cf_string_to_string(owned.as_string())
            .unwrap_or_else(|| "trust evaluation failed".to_owned())
    };
    unsafe { ffi::CFRelease(error.cast()) };
    description
}