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