Skip to main content

security/keychain/
mod.rs

1use bitflags::bitflags;
2
3use crate::bridge::{self, Handle};
4use crate::error::Result;
5
6bitflags! {
7    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8    /// Mirrors `SecAccessControlCreateFlags`.
9    pub struct AccessControlFlags: u64 {
10        /// Mirrors a `SecAccessControlCreateFlags` bit.
11        const DEFAULTS = 0;
12        /// Mirrors a `SecAccessControlCreateFlags` bit.
13        const USER_PRESENCE = 1 << 0;
14        /// Mirrors a `SecAccessControlCreateFlags` bit.
15        const BIOMETRY_ANY = 1 << 1;
16        /// Mirrors a `SecAccessControlCreateFlags` bit.
17        const BIOMETRY_CURRENT_SET = 1 << 3;
18        /// Mirrors a `SecAccessControlCreateFlags` bit.
19        const DEVICE_PASSCODE = 1 << 4;
20        /// Mirrors a `SecAccessControlCreateFlags` bit.
21        const COMPANION = 1 << 5;
22        /// Mirrors a `SecAccessControlCreateFlags` bit.
23        const OR = 1 << 14;
24        /// Mirrors a `SecAccessControlCreateFlags` bit.
25        const AND = 1 << 15;
26        /// Mirrors a `SecAccessControlCreateFlags` bit.
27        const PRIVATE_KEY_USAGE = 1 << 30;
28        /// Mirrors a `SecAccessControlCreateFlags` bit.
29        const APPLICATION_PASSWORD = 1 << 31;
30    }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34/// Mirrors protection classes used by `SecAccessControlCreateWithFlags`.
35pub enum AccessControlProtection {
36    /// Mirrors a `SecAccessControl` protection-class constant.
37    WhenUnlocked,
38    /// Mirrors a `SecAccessControl` protection-class constant.
39    AfterFirstUnlock,
40    /// Mirrors a `SecAccessControl` protection-class constant.
41    WhenPasscodeSetThisDeviceOnly,
42    /// Mirrors a `SecAccessControl` protection-class constant.
43    WhenUnlockedThisDeviceOnly,
44    /// Mirrors a `SecAccessControl` protection-class constant.
45    AfterFirstUnlockThisDeviceOnly,
46}
47
48impl AccessControlProtection {
49    const fn as_bridge_name(self) -> &'static str {
50        match self {
51            Self::WhenUnlocked => "when_unlocked",
52            Self::AfterFirstUnlock => "after_first_unlock",
53            Self::WhenPasscodeSetThisDeviceOnly => "when_passcode_set_this_device_only",
54            Self::WhenUnlockedThisDeviceOnly => "when_unlocked_this_device_only",
55            Self::AfterFirstUnlockThisDeviceOnly => "after_first_unlock_this_device_only",
56        }
57    }
58}
59
60#[derive(Debug)]
61/// Wraps `SecAccessControlRef`.
62pub struct AccessControl {
63    handle: Handle,
64}
65
66impl AccessControl {
67    /// Wraps the corresponding `SecAccessControlRef` operation.
68    pub fn type_id() -> usize {
69        unsafe { bridge::security_access_control_get_type_id() }
70    }
71
72    /// Wraps the corresponding `SecAccessControlRef` operation.
73    pub fn create(protection: AccessControlProtection, flags: AccessControlFlags) -> Result<Self> {
74        let protection = bridge::cstring(protection.as_bridge_name())?;
75        let mut status = 0;
76        let mut error = std::ptr::null_mut();
77        let raw = unsafe {
78            bridge::security_access_control_create(
79                protection.as_ptr(),
80                flags.bits(),
81                &mut status,
82                &mut error,
83            )
84        };
85        bridge::required_handle("security_access_control_create", raw, status, error)
86            .map(|handle| Self { handle })
87    }
88
89    /// Wraps the corresponding `SecAccessControlRef` operation.
90    pub fn is_valid(&self) -> bool {
91        !self.handle.as_ptr().is_null()
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
96/// Wraps a generic-password identity used with `SecItem` queries.
97pub struct KeychainEntry {
98    account: String,
99    service: String,
100}
101
102impl KeychainEntry {
103    /// Wraps the corresponding generic-password operation built on `SecItem`.
104    pub fn new(account: impl Into<String>, service: impl Into<String>) -> Self {
105        Self {
106            account: account.into(),
107            service: service.into(),
108        }
109    }
110
111    /// Wraps the corresponding generic-password operation built on `SecItem`.
112    pub fn account(&self) -> &str {
113        &self.account
114    }
115
116    /// Wraps the corresponding generic-password operation built on `SecItem`.
117    pub fn service(&self) -> &str {
118        &self.service
119    }
120
121    /// Wraps the corresponding generic-password operation built on `SecItem`.
122    pub fn set(&self, password: &str) -> Result<()> {
123        Keychain::set(&self.account, &self.service, password)
124    }
125
126    /// Wraps the corresponding generic-password operation built on `SecItem`.
127    pub fn get(&self) -> Result<String> {
128        Keychain::get(&self.account, &self.service)
129    }
130
131    /// Wraps the corresponding generic-password operation built on `SecItem`.
132    pub fn delete(&self) -> Result<()> {
133        Keychain::delete(&self.account, &self.service)
134    }
135}
136
137/// Wraps generic-password operations built on `SecItem` APIs.
138pub struct Keychain;
139
140impl Keychain {
141    /// Wraps the corresponding generic-password `SecItem` operation.
142    pub fn entry(account: impl Into<String>, service: impl Into<String>) -> KeychainEntry {
143        KeychainEntry::new(account, service)
144    }
145
146    /// Wraps the corresponding generic-password `SecItem` operation.
147    pub fn set(account: &str, service: &str, password: &str) -> Result<()> {
148        let account = bridge::cstring(account)?;
149        let service = bridge::cstring(service)?;
150        let password = bridge::cstring(password)?;
151        let mut error = std::ptr::null_mut();
152        let status = unsafe {
153            bridge::security_keychain_set_password(
154                account.as_ptr(),
155                service.as_ptr(),
156                password.as_ptr(),
157                &mut error,
158            )
159        };
160        bridge::status_result("security_keychain_set_password", status, error)
161    }
162
163    /// Wraps the corresponding generic-password `SecItem` operation.
164    pub fn get(account: &str, service: &str) -> Result<String> {
165        let account = bridge::cstring(account)?;
166        let service = bridge::cstring(service)?;
167        let mut status = 0;
168        let mut error = std::ptr::null_mut();
169        let raw = unsafe {
170            bridge::security_keychain_get_password(
171                account.as_ptr(),
172                service.as_ptr(),
173                &mut status,
174                &mut error,
175            )
176        };
177        bridge::required_string("security_keychain_get_password", raw, status, error)
178    }
179
180    /// Wraps the corresponding generic-password `SecItem` operation.
181    pub fn delete(account: &str, service: &str) -> Result<()> {
182        let account = bridge::cstring(account)?;
183        let service = bridge::cstring(service)?;
184        let mut error = std::ptr::null_mut();
185        let status = unsafe {
186            bridge::security_keychain_delete_password(
187                account.as_ptr(),
188                service.as_ptr(),
189                &mut error,
190            )
191        };
192        bridge::status_result("security_keychain_delete_password", status, error)
193    }
194
195    /// Wraps the corresponding generic-password `SecItem` operation.
196    pub fn list_accounts(service: &str) -> Result<Vec<String>> {
197        let service = bridge::cstring(service)?;
198        let mut status = 0;
199        let mut error = std::ptr::null_mut();
200        let raw = unsafe {
201            bridge::security_keychain_list_accounts(service.as_ptr(), &mut status, &mut error)
202        };
203        bridge::required_json("security_keychain_list_accounts", raw, status, error)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn access_control_protection_base_bridge_names_are_stable() {
213        assert_eq!(
214            AccessControlProtection::WhenUnlocked.as_bridge_name(),
215            "when_unlocked"
216        );
217        assert_eq!(
218            AccessControlProtection::AfterFirstUnlock.as_bridge_name(),
219            "after_first_unlock"
220        );
221    }
222
223    #[test]
224    fn access_control_protection_device_only_bridge_names_are_stable() {
225        assert_eq!(
226            AccessControlProtection::WhenPasscodeSetThisDeviceOnly.as_bridge_name(),
227            "when_passcode_set_this_device_only"
228        );
229        assert_eq!(
230            AccessControlProtection::WhenUnlockedThisDeviceOnly.as_bridge_name(),
231            "when_unlocked_this_device_only"
232        );
233        assert_eq!(
234            AccessControlProtection::AfterFirstUnlockThisDeviceOnly.as_bridge_name(),
235            "after_first_unlock_this_device_only"
236        );
237    }
238
239    #[test]
240    fn access_control_flags_round_trip_through_bits() {
241        let flags = AccessControlFlags::USER_PRESENCE
242            | AccessControlFlags::PRIVATE_KEY_USAGE
243            | AccessControlFlags::APPLICATION_PASSWORD;
244
245        assert_eq!(AccessControlFlags::from_bits(flags.bits()), Some(flags));
246    }
247
248    #[test]
249    fn keychain_entry_new_preserves_account_and_service() {
250        let entry = KeychainEntry::new("alice", "security-rs.tests");
251
252        assert_eq!(entry.account(), "alice");
253        assert_eq!(entry.service(), "security-rs.tests");
254    }
255
256    #[test]
257    fn keychain_entry_convenience_constructor_matches_new() {
258        let entry = Keychain::entry("alice", "security-rs.tests");
259
260        assert_eq!(entry, KeychainEntry::new("alice", "security-rs.tests"));
261    }
262}