1use 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
23pub 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
30pub 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
44pub 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#[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
77pub 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
107pub 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
131fn 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
152fn 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 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(()) => (), 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(()) => (), 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}