1use 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
22pub 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
29pub 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
43pub 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#[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
76pub 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
106pub 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
130fn 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
151fn 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 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(()) => (), 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}