apple_security/
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 crate::base::Result;
7use crate::passwords_options::PasswordOptions;
8use crate::{cvt, Error};
9use core_foundation::base::TCFType;
10use core_foundation::boolean::CFBoolean;
11use core_foundation::data::CFData;
12use core_foundation::dictionary::CFDictionary;
13use core_foundation::string::CFString;
14use core_foundation_sys::base::{CFGetTypeID, CFRelease, CFTypeRef};
15use core_foundation_sys::data::CFDataRef;
16use apple_security_sys::base::{errSecDuplicateItem, errSecParam};
17use apple_security_sys::item::{kSecReturnData, kSecValueData};
18use apple_security_sys::keychain::{SecAuthenticationType, SecProtocolType};
19use apple_security_sys::keychain_item::{
20    SecItemAdd, SecItemCopyMatching, SecItemDelete, SecItemUpdate,
21};
22
23/// Set a generic password for the given service and account.
24/// Creates or updates a keychain entry.
25pub fn set_generic_password(service: &str, account: &str, password: &[u8]) -> Result<()> {
26    let mut options = PasswordOptions::new_generic_password(service, account);
27    set_password_internal(&mut options, password)
28}
29
30/// Get the generic password for the given service and account.  If no matching
31/// keychain entry exists, fails with error code `errSecItemNotFound`.
32pub fn get_generic_password(service: &str, account: &str) -> Result<Vec<u8>> {
33    let mut options = PasswordOptions::new_generic_password(service, account);
34    options.query.push((
35        unsafe { CFString::wrap_under_get_rule(kSecReturnData) },
36        CFBoolean::from(true).into_CFType(),
37    ));
38    let params = CFDictionary::from_CFType_pairs(&options.query);
39    let mut ret: CFTypeRef = std::ptr::null();
40    cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?;
41    get_password_and_release(ret)
42}
43
44/// Delete the generic password keychain entry for the given service and account.
45/// If none exists, fails with error code `errSecItemNotFound`.
46pub fn delete_generic_password(service: &str, account: &str) -> Result<()> {
47    let options = PasswordOptions::new_generic_password(service, account);
48    let params = CFDictionary::from_CFType_pairs(&options.query);
49    cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
50}
51
52/// Set an internet password for the given endpoint parameters.
53/// Creates or updates a keychain entry.
54#[allow(clippy::too_many_arguments)]
55pub fn set_internet_password(
56    server: &str,
57    security_domain: Option<&str>,
58    account: &str,
59    path: &str,
60    port: Option<u16>,
61    protocol: SecProtocolType,
62    authentication_type: SecAuthenticationType,
63    password: &[u8],
64) -> Result<()> {
65    let mut options = PasswordOptions::new_internet_password(
66        server,
67        security_domain,
68        account,
69        path,
70        port,
71        protocol,
72        authentication_type,
73    );
74    set_password_internal(&mut options, password)
75}
76
77/// Get the internet password for the given endpoint parameters.  If no matching
78/// keychain entry exists, fails with error code `errSecItemNotFound`.
79pub fn get_internet_password(
80    server: &str,
81    security_domain: Option<&str>,
82    account: &str,
83    path: &str,
84    port: Option<u16>,
85    protocol: SecProtocolType,
86    authentication_type: SecAuthenticationType,
87) -> Result<Vec<u8>> {
88    let mut options = PasswordOptions::new_internet_password(
89        server,
90        security_domain,
91        account,
92        path,
93        port,
94        protocol,
95        authentication_type,
96    );
97    options.query.push((
98        unsafe { CFString::wrap_under_get_rule(kSecReturnData) },
99        CFBoolean::from(true).into_CFType(),
100    ));
101    let params = CFDictionary::from_CFType_pairs(&options.query);
102    let mut ret: CFTypeRef = std::ptr::null();
103    cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?;
104    get_password_and_release(ret)
105}
106
107/// Delete the internet password for the given endpoint parameters.
108/// If none exists, fails with error code `errSecItemNotFound`.
109pub fn delete_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<()> {
118    let options = PasswordOptions::new_internet_password(
119        server,
120        security_domain,
121        account,
122        path,
123        port,
124        protocol,
125        authentication_type,
126    );
127    let params = CFDictionary::from_CFType_pairs(&options.query);
128    cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
129}
130
131// This starts by trying to create the password with the given query params.
132// If the creation attempt reveals that one exists, its password is updated.
133fn set_password_internal(options: &mut PasswordOptions, password: &[u8]) -> Result<()> {
134    let query_len = options.query.len();
135    options.query.push((
136        unsafe { CFString::wrap_under_get_rule(kSecValueData) },
137        CFData::from_buffer(password).into_CFType(),
138    ));
139
140    let params = CFDictionary::from_CFType_pairs(&options.query);
141    let mut ret = std::ptr::null();
142    let status = unsafe { SecItemAdd(params.as_concrete_TypeRef(), &mut ret) };
143    if status == errSecDuplicateItem {
144        let params = CFDictionary::from_CFType_pairs(&options.query[0..query_len]);
145        let update = CFDictionary::from_CFType_pairs(&options.query[query_len..]);
146        cvt(unsafe { SecItemUpdate(params.as_concrete_TypeRef(), update.as_concrete_TypeRef()) })
147    } else {
148        cvt(status)
149    }
150}
151
152// Having retrieved a password entry, this copies and returns the password.
153//
154// # Safety
155// The data element passed in is assumed to have been returned from a Copy
156// call, so it's released after we are done with it.
157fn get_password_and_release(data: CFTypeRef) -> Result<Vec<u8>> {
158    if !data.is_null() {
159        let type_id = unsafe { CFGetTypeID(data) };
160        if type_id == CFData::type_id() {
161            let val = unsafe { CFData::wrap_under_create_rule(data as CFDataRef) };
162            let mut vec = Vec::new();
163            vec.extend_from_slice(val.bytes());
164            return Ok(vec);
165        } else {
166            // unexpected: we got a reference to some other type.
167            // Release it to make sure there's no leak, but
168            // we can't return the password in this case.
169            unsafe { CFRelease(data) };
170        }
171    }
172    Err(Error::from_code(errSecParam))
173}
174
175#[cfg(test)]
176mod test {
177    use super::*;
178    use apple_security_sys::base::errSecItemNotFound;
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}