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::{PasswordOptions, AccessControlOptions};
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    let params = options.to_dictionary();
66    cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
67}
68
69/// Set an internet password for the given endpoint parameters.
70/// Creates or updates a keychain entry.
71#[allow(clippy::too_many_arguments)]
72pub fn set_internet_password(
73    server: &str,
74    security_domain: Option<&str>,
75    account: &str,
76    path: &str,
77    port: Option<u16>,
78    protocol: SecProtocolType,
79    authentication_type: SecAuthenticationType,
80    password: &[u8],
81) -> Result<()> {
82    let mut options = PasswordOptions::new_internet_password(
83        server,
84        security_domain,
85        account,
86        path,
87        port,
88        protocol,
89        authentication_type,
90    );
91    set_password_internal(&mut options, password)
92}
93
94/// Get the internet password for the given endpoint parameters.  If no matching
95/// keychain entry exists, fails with error code `errSecItemNotFound`.
96pub fn get_internet_password(
97    server: &str,
98    security_domain: Option<&str>,
99    account: &str,
100    path: &str,
101    port: Option<u16>,
102    protocol: SecProtocolType,
103    authentication_type: SecAuthenticationType,
104) -> Result<Vec<u8>> {
105    let mut options = PasswordOptions::new_internet_password(
106        server,
107        security_domain,
108        account,
109        path,
110        port,
111        protocol,
112        authentication_type,
113    );
114    unsafe { options.push_query(kSecReturnData, CFBoolean::from(true)); }
115    let params = options.to_dictionary();
116    let mut ret: CFTypeRef = std::ptr::null();
117    cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?;
118    get_password_and_release(ret)
119}
120
121/// Delete the internet password for the given endpoint parameters.
122/// If none exists, fails with error code `errSecItemNotFound`.
123pub fn delete_internet_password(
124    server: &str,
125    security_domain: Option<&str>,
126    account: &str,
127    path: &str,
128    port: Option<u16>,
129    protocol: SecProtocolType,
130    authentication_type: SecAuthenticationType,
131) -> Result<()> {
132    let options = PasswordOptions::new_internet_password(
133        server,
134        security_domain,
135        account,
136        path,
137        port,
138        protocol,
139        authentication_type,
140    );
141    let params = options.to_dictionary();
142    cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
143}
144
145// This starts by trying to create the password with the given query params.
146// If the creation attempt reveals that one exists, its password is updated.
147fn set_password_internal(options: &mut PasswordOptions, password: &[u8]) -> Result<()> {
148    #[allow(deprecated)]
149    let query_without_password = options.query.len();
150    unsafe { options.push_query(kSecValueData, CFData::from_buffer(password)); }
151
152    let params = options.to_dictionary();
153    let mut ret = std::ptr::null();
154    let status = unsafe { SecItemAdd(params.as_concrete_TypeRef(), &mut ret) };
155    if status == errSecDuplicateItem {
156        #[allow(deprecated)]
157        let (query, pass) = options.query.split_at(query_without_password);
158        let params = CFDictionary::from_CFType_pairs(query);
159        let update = CFDictionary::from_CFType_pairs(pass);
160        cvt(unsafe { SecItemUpdate(params.as_concrete_TypeRef(), update.as_concrete_TypeRef()) })
161    } else {
162        cvt(status)
163    }
164}
165
166// Having retrieved a password entry, this copies and returns the password.
167//
168// # Safety
169// The data element passed in is assumed to have been returned from a Copy
170// call, so it's released after we are done with it.
171fn get_password_and_release(data: CFTypeRef) -> Result<Vec<u8>> {
172    if !data.is_null() {
173        let type_id = unsafe { CFGetTypeID(data) };
174        if type_id == CFData::type_id() {
175            let val = unsafe { CFData::wrap_under_create_rule(data as CFDataRef) };
176            let mut vec = Vec::new();
177            if !val.is_empty() {
178                vec.extend_from_slice(val.bytes());
179            }
180            return Ok(vec);
181        }
182        // unexpected: we got a reference to some other type.
183        // Release it to make sure there's no leak, but
184        // we can't return the password in this case.
185        unsafe { CFRelease(data) };
186    }
187    Err(Error::from_code(errSecParam))
188}
189
190#[cfg(test)]
191mod test {
192    use super::*;
193    use security_framework_sys::base::errSecItemNotFound;
194
195    #[test]
196    fn missing_generic() {
197        let name = "a string not likely to already be in the keychain as service or account";
198        let result = delete_generic_password(name, name);
199        match result {
200            Ok(()) => (), // this is ok because the name _might_ be in the keychain
201            Err(err) if err.code() == errSecItemNotFound => (),
202            Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()),
203        };
204        let result = get_generic_password(name, name);
205        match result {
206            Ok(bytes) => panic!("missing_generic: get returned {bytes:?}"),
207            Err(err) if err.code() == errSecItemNotFound => (),
208            Err(err) => panic!("missing_generic: get failed with status: {}", err.code()),
209        };
210        let result = delete_generic_password(name, name);
211        match result {
212            Ok(()) => panic!("missing_generic: second delete found a password"),
213            Err(err) if err.code() == errSecItemNotFound => (),
214            Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()),
215        };
216    }
217
218    #[test]
219    fn roundtrip_generic() {
220        let name = "roundtrip_generic";
221        set_generic_password(name, name, name.as_bytes()).expect("set_generic_password");
222        let pass = get_generic_password(name, name).expect("get_generic_password");
223        assert_eq!(name.as_bytes(), pass);
224        delete_generic_password(name, name).expect("delete_generic_password");
225    }
226
227    #[test]
228    #[cfg(feature = "OSX_10_12")]
229    fn update_generic() {
230        let name = "update_generic";
231        set_generic_password(name, name, name.as_bytes()).expect("set_generic_password");
232        let alternate = "update_generic_alternate";
233        set_generic_password(name, name, alternate.as_bytes()).expect("set_generic_password");
234        let pass = get_generic_password(name, name).expect("get_generic_password");
235        assert_eq!(pass, alternate.as_bytes());
236        delete_generic_password(name, name).expect("delete_generic_password");
237    }
238
239    #[test]
240    fn missing_internet() {
241        let name = "a string not likely to already be in the keychain as service or account";
242        let (server, domain, account, path, port, protocol, auth) = (
243            name,
244            None,
245            name,
246            "/",
247            Some(8080u16),
248            SecProtocolType::HTTP,
249            SecAuthenticationType::Any,
250        );
251        let result = delete_internet_password(server, domain, account, path, port, protocol, auth);
252        match result {
253            Ok(()) => (), // this is ok because the name _might_ be in the keychain
254            Err(err) if err.code() == errSecItemNotFound => (),
255            Err(err) => panic!(
256                "missing_internet: delete failed with status: {}",
257                err.code()
258            ),
259        };
260        let result = get_internet_password(server, domain, account, path, port, protocol, auth);
261        match result {
262            Ok(bytes) => panic!("missing_internet: get returned {bytes:?}"),
263            Err(err) if err.code() == errSecItemNotFound => (),
264            Err(err) => panic!("missing_internet: get failed with status: {}", err.code()),
265        };
266        let result = delete_internet_password(server, domain, account, path, port, protocol, auth);
267        match result {
268            Ok(()) => panic!("missing_internet: second delete found a password"),
269            Err(err) if err.code() == errSecItemNotFound => (),
270            Err(err) => panic!(
271                "missing_internet: delete failed with status: {}",
272                err.code()
273            ),
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,
282            None,
283            name,
284            "/",
285            Some(8080u16),
286            SecProtocolType::HTTP,
287            SecAuthenticationType::Any,
288        );
289        set_internet_password(
290            server,
291            domain,
292            account,
293            path,
294            port,
295            protocol,
296            auth,
297            name.as_bytes(),
298        )
299        .expect("set_internet_password");
300        let pass = get_internet_password(server, domain, account, path, port, protocol, auth)
301            .expect("get_internet_password");
302        assert_eq!(name.as_bytes(), pass);
303        delete_internet_password(server, domain, account, path, port, protocol, auth)
304            .expect("delete_internet_password");
305    }
306
307    #[test]
308    fn update_internet() {
309        let name = "update_internet";
310        let (server, domain, account, path, port, protocol, auth) = (
311            name,
312            None,
313            name,
314            "/",
315            Some(8080u16),
316            SecProtocolType::HTTP,
317            SecAuthenticationType::Any,
318        );
319
320        // cleanup after failed test
321        let _ = delete_internet_password(server, domain, account, path, port, protocol, auth);
322
323        set_internet_password(
324            server,
325            domain,
326            account,
327            path,
328            port,
329            protocol,
330            auth,
331            name.as_bytes(),
332        )
333        .expect("set_internet_password");
334        let alternate = "alternate_internet_password";
335        set_internet_password(
336            server,
337            domain,
338            account,
339            path,
340            port,
341            protocol,
342            auth,
343            alternate.as_bytes(),
344        )
345        .expect("set_internet_password");
346        let pass = get_internet_password(server, domain, account, path, port, protocol, auth)
347            .expect("get_internet_password");
348        assert_eq!(pass, alternate.as_bytes());
349        delete_internet_password(server, domain, account, path, port, protocol, auth)
350            .expect("delete_internet_password");
351    }
352}