1use crate::error::{Result, SecurityError};
4use crate::ffi;
5use crate::private::{
6 cf_data, cf_data_to_vec, cf_dictionary_get_value, cf_dictionary_set_value,
7 cf_mutable_dictionary, cf_string, cf_string_to_string, sec_error_message, OwnedCf,
8};
9
10fn generic_password_query(account: Option<&str>, service: &str) -> Result<OwnedCf> {
11 let dictionary = cf_mutable_dictionary(3)?;
12 let service = cf_string(service)?;
13 unsafe {
14 cf_dictionary_set_value(
15 dictionary.as_mut_dictionary(),
16 ffi::kSecClass,
17 ffi::kSecClassGenericPassword.cast(),
18 );
19 cf_dictionary_set_value(
20 dictionary.as_mut_dictionary(),
21 ffi::kSecAttrService,
22 service.as_ptr(),
23 );
24 }
25
26 if let Some(account) = account {
27 let account = cf_string(account)?;
28 unsafe {
29 cf_dictionary_set_value(
30 dictionary.as_mut_dictionary(),
31 ffi::kSecAttrAccount,
32 account.as_ptr(),
33 );
34 }
35 }
36
37 Ok(dictionary)
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub struct KeychainEntry {
43 account: String,
44 service: String,
45}
46
47impl KeychainEntry {
48 #[must_use]
50 pub fn new(account: impl Into<String>, service: impl Into<String>) -> Self {
51 Self {
52 account: account.into(),
53 service: service.into(),
54 }
55 }
56
57 #[must_use]
59 pub fn account(&self) -> &str {
60 &self.account
61 }
62
63 #[must_use]
65 pub fn service(&self) -> &str {
66 &self.service
67 }
68
69 pub fn set(&self, password: &str) -> Result<()> {
75 Keychain::set(&self.account, &self.service, password)
76 }
77
78 pub fn get(&self) -> Result<String> {
84 Keychain::get(&self.account, &self.service)
85 }
86
87 pub fn delete(&self) -> Result<()> {
93 Keychain::delete(&self.account, &self.service)
94 }
95}
96
97pub struct Keychain;
99
100impl Keychain {
101 #[must_use]
103 pub fn entry(account: impl Into<String>, service: impl Into<String>) -> KeychainEntry {
104 KeychainEntry::new(account, service)
105 }
106
107 pub fn set(account: &str, service: &str, password: &str) -> Result<()> {
113 let search_query = generic_password_query(Some(account), service)?;
114 let add_query = generic_password_query(Some(account), service)?;
115 let password_data = cf_data(password.as_bytes())?;
116 unsafe {
117 cf_dictionary_set_value(
118 add_query.as_mut_dictionary(),
119 ffi::kSecValueData,
120 password_data.as_ptr(),
121 );
122 }
123
124 let status = unsafe { ffi::SecItemAdd(add_query.as_dictionary(), std::ptr::null_mut()) };
125 match status {
126 ffi::status::SUCCESS => Ok(()),
127 ffi::status::DUPLICATE_ITEM => {
128 let attributes = cf_mutable_dictionary(1)?;
129 unsafe {
130 cf_dictionary_set_value(
131 attributes.as_mut_dictionary(),
132 ffi::kSecValueData,
133 password_data.as_ptr(),
134 );
135 }
136 let status = unsafe {
137 ffi::SecItemUpdate(search_query.as_dictionary(), attributes.as_dictionary())
138 };
139 if status == ffi::status::SUCCESS {
140 Ok(())
141 } else {
142 Err(SecurityError::from_status(
143 "SecItemUpdate",
144 status,
145 sec_error_message(status),
146 ))
147 }
148 }
149 _ => Err(SecurityError::from_status(
150 "SecItemAdd",
151 status,
152 sec_error_message(status),
153 )),
154 }
155 }
156
157 pub fn get(account: &str, service: &str) -> Result<String> {
163 let query = generic_password_query(Some(account), service)?;
164 unsafe {
165 cf_dictionary_set_value(
166 query.as_mut_dictionary(),
167 ffi::kSecReturnData,
168 ffi::kCFBooleanTrue.cast(),
169 );
170 cf_dictionary_set_value(
171 query.as_mut_dictionary(),
172 ffi::kSecMatchLimit,
173 ffi::kSecMatchLimitOne.cast(),
174 );
175 }
176
177 let mut result = std::ptr::null();
178 let status = unsafe { ffi::SecItemCopyMatching(query.as_dictionary(), &mut result) };
179 if status != ffi::status::SUCCESS {
180 let context = format!(
181 "generic password {account:?} @ {service:?}: {}",
182 sec_error_message(status)
183 );
184 return Err(SecurityError::from_status(
185 "SecItemCopyMatching",
186 status,
187 context,
188 ));
189 }
190
191 let data = OwnedCf::new(result);
192 if crate::private::cf_type_id(data.as_ptr()) != unsafe { ffi::CFDataGetTypeID() } {
193 return Err(SecurityError::UnexpectedType {
194 operation: "SecItemCopyMatching",
195 expected: "CFData",
196 });
197 }
198
199 String::from_utf8(cf_data_to_vec(data.as_data())).map_err(|error| {
200 SecurityError::InvalidArgument(format!("keychain password is not valid UTF-8: {error}"))
201 })
202 }
203
204 pub fn delete(account: &str, service: &str) -> Result<()> {
212 let query = generic_password_query(Some(account), service)?;
213 let status = unsafe { ffi::SecItemDelete(query.as_dictionary()) };
214 match status {
215 ffi::status::SUCCESS | ffi::status::ITEM_NOT_FOUND => Ok(()),
216 _ => Err(SecurityError::from_status(
217 "SecItemDelete",
218 status,
219 sec_error_message(status),
220 )),
221 }
222 }
223
224 pub fn list_accounts(service: &str) -> Result<Vec<String>> {
230 let query = generic_password_query(None, service)?;
231 unsafe {
232 cf_dictionary_set_value(
233 query.as_mut_dictionary(),
234 ffi::kSecReturnAttributes,
235 ffi::kCFBooleanTrue.cast(),
236 );
237 cf_dictionary_set_value(
238 query.as_mut_dictionary(),
239 ffi::kSecMatchLimit,
240 ffi::kSecMatchLimitAll.cast(),
241 );
242 }
243
244 let mut result = std::ptr::null();
245 let status = unsafe { ffi::SecItemCopyMatching(query.as_dictionary(), &mut result) };
246 if status == ffi::status::ITEM_NOT_FOUND {
247 return Ok(Vec::new());
248 }
249 if status != ffi::status::SUCCESS {
250 return Err(SecurityError::from_status(
251 "SecItemCopyMatching",
252 status,
253 sec_error_message(status),
254 ));
255 }
256
257 let result = OwnedCf::new(result);
258 let dictionary_type = unsafe { ffi::CFDictionaryGetTypeID() };
259 let array_type = unsafe { ffi::CFArrayGetTypeID() };
260 let result_type = crate::private::cf_type_id(result.as_ptr());
261 let mut accounts = Vec::new();
262
263 if result_type == dictionary_type {
264 if let Some(account) = account_from_attributes(result.as_dictionary()) {
265 accounts.push(account);
266 }
267 } else if result_type == array_type {
268 let count = unsafe { ffi::CFArrayGetCount(result.as_array()) };
269 let count = usize::try_from(count).unwrap_or_default();
270 for index in 0..count {
271 let Ok(index) = isize::try_from(index) else {
272 continue;
273 };
274 let value = unsafe { ffi::CFArrayGetValueAtIndex(result.as_array(), index) };
275 if value.is_null() {
276 continue;
277 }
278 if let Some(account) = account_from_attributes(value.cast()) {
279 accounts.push(account);
280 }
281 }
282 } else {
283 return Err(SecurityError::UnexpectedType {
284 operation: "SecItemCopyMatching",
285 expected: "CFDictionary or CFArray",
286 });
287 }
288
289 accounts.sort();
290 accounts.dedup();
291 Ok(accounts)
292 }
293}
294
295fn account_from_attributes(dictionary: ffi::CFDictionaryRef) -> Option<String> {
296 let value = cf_dictionary_get_value(dictionary, unsafe { ffi::kSecAttrAccount });
297 if value.is_null() || crate::private::cf_type_id(value) != unsafe { ffi::CFStringGetTypeID() } {
298 return None;
299 }
300 cf_string_to_string(value.cast())
301}