1#[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
24pub 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
31pub fn set_generic_password_options(password: &[u8], mut options: PasswordOptions) -> Result<()> {
34 set_password_internal(&mut options, password)
35}
36
37#[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
44pub 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
61pub 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#[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
94pub 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
121pub 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
145fn 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
166fn 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 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(()) => (), 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(()) => (), 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 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}