security_framework/
passwords.rs

1//! Support for password entries in the keychain.  Works on both iOS and macOS.
2//!
3//! If you want the extended keychain facilities only available on macOS, use the
4//! version of these functions in the macOS extensions module.
5
6#[doc(inline)]
7pub use crate::passwords_options::{AccessControlOptions, PasswordOptions};
8
9use crate::base::Result;
10use crate::{cvt, Error};
11use core_foundation::base::TCFType;
12use core_foundation::boolean::CFBoolean;
13use core_foundation::data::CFData;
14use core_foundation::dictionary::CFDictionary;
15use core_foundation_sys::base::{CFGetTypeID, CFRelease, CFTypeRef};
16use core_foundation_sys::data::CFDataRef;
17use security_framework_sys::base::{errSecDuplicateItem, errSecParam};
18use security_framework_sys::item::{kSecReturnData, kSecValueData};
19use security_framework_sys::keychain::{SecAuthenticationType, SecProtocolType};
20use security_framework_sys::keychain_item::{
21    SecItemAdd, SecItemCopyMatching, SecItemDelete, SecItemUpdate,
22};
23
24/// Set a generic password for the given service and account.
25/// Creates or updates a keychain entry.
26pub fn set_generic_password(service: &str, account: &str, password: &[u8]) -> Result<()> {
27    let mut options = PasswordOptions::new_generic_password(service, account);
28    set_password_internal(&mut options, password)
29}
30
31/// Set a generic password using the given password options.
32/// Creates or updates a keychain entry.
33pub fn set_generic_password_options(password: &[u8], mut options: PasswordOptions) -> Result<()> {
34    set_password_internal(&mut options, password)
35}
36
37/// Get the generic password for the given service and account.  If no matching
38/// keychain entry exists, fails with error code `errSecItemNotFound`.
39#[doc(hidden)]
40pub fn get_generic_password(service: &str, account: &str) -> Result<Vec<u8>> {
41    generic_password(PasswordOptions::new_generic_password(service, account))
42}
43
44/// Get the generic password for the given service and account.  If no matching
45/// keychain entry exists, fails with error code `errSecItemNotFound`.
46///
47/// See [`PasswordOptions`] and [`new_generic_password`](PasswordOptions::new_generic_password).
48///
49/// ```rust
50/// use security_framework::passwords::{generic_password, PasswordOptions};
51/// generic_password(PasswordOptions::new_generic_password("service", "account"));
52/// ```
53pub fn generic_password(mut options: PasswordOptions) -> Result<Vec<u8>> {
54    unsafe { options.push_query(kSecReturnData, CFBoolean::from(true)); }
55    let params = options.to_dictionary();
56    let mut ret: CFTypeRef = std::ptr::null();
57    cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?;
58    get_password_and_release(ret)
59}
60
61/// Delete the generic password keychain entry for the given service and account.
62/// If none exists, fails with error code `errSecItemNotFound`.
63pub fn delete_generic_password(service: &str, account: &str) -> Result<()> {
64    let options = PasswordOptions::new_generic_password(service, account);
65    delete_generic_password_options(options)
66}
67
68/// Delete the generic password keychain entry for the given service and account.
69/// If none exists, fails with error code `errSecItemNotFound`.
70///
71/// See [`PasswordOptions`] and [`new_generic_password`](PasswordOptions::new_generic_password).
72///
73/// ```rust
74/// use security_framework::passwords::{delete_generic_password_options, PasswordOptions};
75/// delete_generic_password_options(PasswordOptions::new_generic_password("service", "account"));
76/// ```
77pub fn delete_generic_password_options(options: PasswordOptions) -> Result<()> {
78    let params = options.to_dictionary();
79    cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
80}
81
82/// Set an internet password for the given endpoint parameters.
83/// Creates or updates a keychain entry.
84#[allow(clippy::too_many_arguments)]
85pub fn set_internet_password(
86    server: &str,
87    security_domain: Option<&str>,
88    account: &str,
89    path: &str,
90    port: Option<u16>,
91    protocol: SecProtocolType,
92    authentication_type: SecAuthenticationType,
93    password: &[u8],
94) -> Result<()> {
95    let mut options = PasswordOptions::new_internet_password(
96        server,
97        security_domain,
98        account,
99        path,
100        port,
101        protocol,
102        authentication_type,
103    );
104    set_password_internal(&mut options, password)
105}
106
107/// Get the internet password for the given endpoint parameters.  If no matching
108/// keychain entry exists, fails with error code `errSecItemNotFound`.
109pub fn get_internet_password(
110    server: &str,
111    security_domain: Option<&str>,
112    account: &str,
113    path: &str,
114    port: Option<u16>,
115    protocol: SecProtocolType,
116    authentication_type: SecAuthenticationType,
117) -> Result<Vec<u8>> {
118    let mut options = PasswordOptions::new_internet_password(
119        server,
120        security_domain,
121        account,
122        path,
123        port,
124        protocol,
125        authentication_type,
126    );
127    unsafe { options.push_query(kSecReturnData, CFBoolean::from(true)); }
128    let params = options.to_dictionary();
129    let mut ret: CFTypeRef = std::ptr::null();
130    cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?;
131    get_password_and_release(ret)
132}
133
134/// Delete the internet password for the given endpoint parameters.
135/// If none exists, fails with error code `errSecItemNotFound`.
136pub fn delete_internet_password(
137    server: &str,
138    security_domain: Option<&str>,
139    account: &str,
140    path: &str,
141    port: Option<u16>,
142    protocol: SecProtocolType,
143    authentication_type: SecAuthenticationType,
144) -> Result<()> {
145    let options = PasswordOptions::new_internet_password(
146        server,
147        security_domain,
148        account,
149        path,
150        port,
151        protocol,
152        authentication_type,
153    );
154    let params = options.to_dictionary();
155    cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
156}
157
158// This starts by trying to create the password with the given query params.
159// If the creation attempt reveals that one exists, its password is updated.
160fn set_password_internal(options: &mut PasswordOptions, password: &[u8]) -> Result<()> {
161    #[allow(deprecated)]
162    let query_without_password = options.query.len();
163    unsafe { options.push_query(kSecValueData, CFData::from_buffer(password)); }
164
165    let params = options.to_dictionary();
166    let mut ret = std::ptr::null();
167    let status = unsafe { SecItemAdd(params.as_concrete_TypeRef(), &mut ret) };
168    if status == errSecDuplicateItem {
169        #[allow(deprecated)]
170        let (query, pass) = options.query.split_at(query_without_password);
171        let params = CFDictionary::from_CFType_pairs(query);
172        let update = CFDictionary::from_CFType_pairs(pass);
173        cvt(unsafe { SecItemUpdate(params.as_concrete_TypeRef(), update.as_concrete_TypeRef()) })
174    } else {
175        cvt(status)
176    }
177}
178
179// Having retrieved a password entry, this copies and returns the password.
180//
181// # Safety
182// The data element passed in is assumed to have been returned from a Copy
183// call, so it's released after we are done with it.
184fn get_password_and_release(data: CFTypeRef) -> Result<Vec<u8>> {
185    if !data.is_null() {
186        let type_id = unsafe { CFGetTypeID(data) };
187        if type_id == CFData::type_id() {
188            let val = unsafe { CFData::wrap_under_create_rule(data as CFDataRef) };
189            let mut vec = Vec::new();
190            if !val.is_empty() {
191                vec.extend_from_slice(val.bytes());
192            }
193            return Ok(vec);
194        }
195        // unexpected: we got a reference to some other type.
196        // Release it to make sure there's no leak, but
197        // we can't return the password in this case.
198        unsafe { CFRelease(data) };
199    }
200    Err(Error::from_code(errSecParam))
201}
202
203#[cfg(test)]
204mod test {
205    use super::*;
206    use security_framework_sys::base::errSecItemNotFound;
207
208    #[test]
209    fn missing_generic() {
210        let name = "a string not likely to already be in the keychain as service or account";
211        let result = delete_generic_password(name, name);
212        match result {
213            Ok(()) => (), // this is ok because the name _might_ be in the keychain
214            Err(err) if err.code() == errSecItemNotFound => (),
215            Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()),
216        }
217        let result = get_generic_password(name, name);
218        match result {
219            Ok(bytes) => panic!("missing_generic: get returned {bytes:?}"),
220            Err(err) if err.code() == errSecItemNotFound => (),
221            Err(err) => panic!("missing_generic: get failed with status: {}", err.code()),
222        }
223        let result = delete_generic_password(name, name);
224        match result {
225            Ok(()) => panic!("missing_generic: second delete found a password"),
226            Err(err) if err.code() == errSecItemNotFound => (),
227            Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()),
228        }
229    }
230
231    #[test]
232    fn roundtrip_generic() {
233        let name = "roundtrip_generic";
234        set_generic_password(name, name, name.as_bytes()).expect("set_generic_password");
235        let pass = get_generic_password(name, name).expect("get_generic_password");
236        assert_eq!(name.as_bytes(), pass);
237        delete_generic_password(name, name).expect("delete_generic_password");
238    }
239
240    #[test]
241    #[cfg(feature = "OSX_10_12")]
242    fn update_generic() {
243        let name = "update_generic";
244        set_generic_password(name, name, name.as_bytes()).expect("set_generic_password");
245        let alternate = "update_generic_alternate";
246        set_generic_password(name, name, alternate.as_bytes()).expect("set_generic_password");
247        let pass = get_generic_password(name, name).expect("get_generic_password");
248        assert_eq!(pass, alternate.as_bytes());
249        delete_generic_password(name, name).expect("delete_generic_password");
250    }
251
252    #[test]
253    fn missing_internet() {
254        let name = "a string not likely to already be in the keychain as service or account";
255        let (server, domain, account, path, port, protocol, auth) =
256            (name, None, name, "/", Some(8080u16), SecProtocolType::HTTP, SecAuthenticationType::Any);
257        let result = delete_internet_password(server, domain, account, path, port, protocol, auth);
258        match result {
259            Ok(()) => (), // this is ok because the name _might_ be in the keychain
260            Err(err) if err.code() == errSecItemNotFound => (),
261            Err(err) => panic!("missing_internet: delete failed with status: {}", err.code()),
262        }
263        let result = get_internet_password(server, domain, account, path, port, protocol, auth);
264        match result {
265            Ok(bytes) => panic!("missing_internet: get returned {bytes:?}"),
266            Err(err) if err.code() == errSecItemNotFound => (),
267            Err(err) => panic!("missing_internet: get failed with status: {}", err.code()),
268        }
269        let result = delete_internet_password(server, domain, account, path, port, protocol, auth);
270        match result {
271            Ok(()) => panic!("missing_internet: second delete found a password"),
272            Err(err) if err.code() == errSecItemNotFound => (),
273            Err(err) => panic!("missing_internet: delete failed with status: {}", err.code()),
274        }
275    }
276
277    #[test]
278    fn roundtrip_internet() {
279        let name = "roundtrip_internet";
280        let (server, domain, account, path, port, protocol, auth) =
281            (name, None, name, "/", Some(8080u16), SecProtocolType::HTTP, SecAuthenticationType::Any);
282        set_internet_password(server, domain, account, path, port, protocol, auth, name.as_bytes())
283            .expect("set_internet_password");
284        let pass =
285            get_internet_password(server, domain, account, path, port, protocol, auth).expect("get_internet_password");
286        assert_eq!(name.as_bytes(), pass);
287        delete_internet_password(server, domain, account, path, port, protocol, auth)
288            .expect("delete_internet_password");
289    }
290
291    #[test]
292    fn update_internet() {
293        let name = "update_internet";
294        let (server, domain, account, path, port, protocol, auth) =
295            (name, None, name, "/", Some(8080u16), SecProtocolType::HTTP, SecAuthenticationType::Any);
296
297        // cleanup after failed test
298        let _ = delete_internet_password(server, domain, account, path, port, protocol, auth);
299
300        set_internet_password(server, domain, account, path, port, protocol, auth, name.as_bytes())
301            .expect("set_internet_password");
302        let alternate = "alternate_internet_password";
303        set_internet_password(server, domain, account, path, port, protocol, auth, alternate.as_bytes())
304            .expect("set_internet_password");
305        let pass =
306            get_internet_password(server, domain, account, path, port, protocol, auth).expect("get_internet_password");
307        assert_eq!(pass, alternate.as_bytes());
308        delete_internet_password(server, domain, account, path, port, protocol, auth)
309            .expect("delete_internet_password");
310    }
311}