apple_native_keyring_store/
keychain.rs1use std::collections::HashMap;
42use std::sync::Arc;
43use std::time::{SystemTime, UNIX_EPOCH};
44
45use security_framework::base::Error;
46use security_framework::item;
47use security_framework::os::macos::keychain::{SecKeychain, SecPreferencesDomain};
48use security_framework::os::macos::passwords::find_generic_password;
49
50use keyring_core::{
51 Entry,
52 api::{Credential, CredentialApi, CredentialPersistence, CredentialStoreApi},
53 attributes::parse_attributes,
54 error::{Error as ErrorCode, Result},
55};
56
57#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Cred {
64 pub domain: MacKeychainDomain,
65 pub service: String,
66 pub account: String,
67}
68
69impl CredentialApi for Cred {
70 fn set_secret(&self, secret: &[u8]) -> Result<()> {
72 self.get_keychain()?
73 .set_generic_password(&self.service, &self.account, secret)
74 .map_err(decode_error)?;
75 Ok(())
76 }
77
78 fn get_secret(&self) -> Result<Vec<u8>> {
80 let (password_bytes, _) =
81 find_generic_password(Some(&[self.get_keychain()?]), &self.service, &self.account)
82 .map_err(decode_error)?;
83 Ok(password_bytes.to_owned())
84 }
85
86 fn delete_credential(&self) -> Result<()> {
88 let keychains = [self.get_keychain()?];
89 let mut options = item::ItemSearchOptions::new();
90 options
91 .keychains(&keychains)
92 .class(item::ItemClass::generic_password())
93 .service(&self.service)
94 .account(&self.account);
95 options.delete().map_err(decode_error)
96 }
97
98 fn get_credential(&self) -> Result<Option<Arc<Credential>>> {
103 find_generic_password(Some(&[self.get_keychain()?]), &self.service, &self.account)
104 .map_err(decode_error)?;
105 Ok(None)
106 }
107
108 fn get_specifiers(&self) -> Option<(String, String)> {
110 Some((self.service.clone(), self.account.clone()))
111 }
112
113 fn as_any(&self) -> &dyn std::any::Any {
115 self
116 }
117
118 fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 std::fmt::Debug::fmt(self, f)
121 }
122}
123
124impl Cred {
125 pub fn build(keychain: MacKeychainDomain, service: &str, user: &str) -> Result<Entry> {
138 if service.is_empty() {
139 return Err(ErrorCode::Invalid(
140 "service".to_string(),
141 "cannot be empty".to_string(),
142 ));
143 }
144 if user.is_empty() {
145 return Err(ErrorCode::Invalid(
146 "user".to_string(),
147 "cannot be empty".to_string(),
148 ));
149 }
150 let cred = Cred {
151 domain: keychain,
152 service: service.to_string(),
153 account: user.to_string(),
154 };
155 Ok(Entry::new_with_credential(Arc::new(cred)))
156 }
157
158 fn get_keychain(&self) -> Result<SecKeychain> {
159 get_keychain(&self.domain)
160 }
161}
162
163pub struct Store {
165 id: String,
166 keychain: MacKeychainDomain,
167}
168
169impl std::fmt::Debug for Store {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct("Store")
172 .field("vendor", &self.vendor())
173 .field("id", &self.id())
174 .field("domain", &self.keychain)
175 .finish()
176 }
177}
178
179impl Store {
180 pub fn new() -> Result<Arc<Self>> {
182 Ok(Self::new_internal(MacKeychainDomain::User))
183 }
184
185 pub fn new_with_configuration(configuration: &HashMap<&str, &str>) -> Result<Arc<Self>> {
189 let config = parse_attributes(&["keychain"], Some(configuration))?;
190 let mut keychain = MacKeychainDomain::User;
191 if let Some(option) = config.get("keychain") {
192 keychain = option.parse()?;
193 }
194 Ok(Self::new_internal(keychain))
195 }
196
197 fn new_internal(keychain: MacKeychainDomain) -> Arc<Self> {
198 let now = SystemTime::now();
199 let elapsed = if now.lt(&UNIX_EPOCH) {
200 UNIX_EPOCH.duration_since(now).unwrap()
201 } else {
202 now.duration_since(UNIX_EPOCH).unwrap()
203 };
204 Arc::new(Store {
205 id: format!(
206 "Keychain Storage, Crate version {}, Instantiated at {}",
207 env!("CARGO_PKG_VERSION"),
208 elapsed.as_secs_f64()
209 ),
210 keychain,
211 })
212 }
213}
214
215impl CredentialStoreApi for Store {
216 fn vendor(&self) -> String {
218 "macOS Keychain Store, https://crates.io/crates/apple-native-keyring-store".to_string()
219 }
220
221 fn id(&self) -> String {
223 self.id.to_string()
224 }
225
226 fn build(
233 &self,
234 service: &str,
235 user: &str,
236 modifiers: Option<&HashMap<&str, &str>>,
237 ) -> Result<Entry> {
238 let mods = parse_attributes(&["keychain"], modifiers)?;
239 let mut keychain = self.keychain.clone();
240 if let Some(option) = mods.get("keychain") {
241 keychain = option.parse()?;
242 }
243 Cred::build(keychain, service, user)
244 }
245
246 fn search(&self, spec: &HashMap<&str, &str>) -> Result<Vec<Entry>> {
255 let spec = parse_attributes(&["service", "user"], Some(spec))?;
256 let keychains = [get_keychain(&self.keychain)?];
257 let mut options = item::ItemSearchOptions::new();
258 options
259 .keychains(&keychains)
260 .class(item::ItemClass::generic_password())
261 .limit(item::Limit::All)
262 .load_attributes(true);
263 if let Some(service) = spec.get("service") {
264 options.service(service);
265 }
266 if let Some(user) = spec.get("user") {
267 options.account(user);
268 }
269 let items = match options.search().map_err(decode_error) {
270 Ok(items) => items,
271 Err(ErrorCode::NoEntry) => return Ok(Vec::new()),
272 Err(e) => return Err(e),
273 };
274 let mut result = Vec::new();
275 for item in items {
276 if let Some(map) = item.simplify_dict() {
277 if let Some(service) = map.get("svce") {
278 if let Some(account) = map.get("acct") {
279 let cred = Cred {
280 domain: self.keychain.clone(),
281 service: service.to_string(),
282 account: account.to_string(),
283 };
284 result.push(Entry::new_with_credential(Arc::new(cred)))
285 }
286 }
287 }
288 }
289 Ok(result)
290 }
291
292 fn as_any(&self) -> &dyn std::any::Any {
295 self
296 }
297
298 fn persistence(&self) -> CredentialPersistence {
300 CredentialPersistence::UntilDelete
301 }
302
303 fn debug_fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305 std::fmt::Debug::fmt(self, f)
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub enum MacKeychainDomain {
312 User,
313 System,
314 Common,
315 Dynamic,
316}
317
318impl std::fmt::Display for MacKeychainDomain {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 match self {
321 MacKeychainDomain::User => "User".fmt(f),
322 MacKeychainDomain::System => "System".fmt(f),
323 MacKeychainDomain::Common => "Common".fmt(f),
324 MacKeychainDomain::Dynamic => "Dynamic".fmt(f),
325 }
326 }
327}
328
329impl std::str::FromStr for MacKeychainDomain {
330 type Err = ErrorCode;
331
332 fn from_str(s: &str) -> Result<Self> {
337 match s.to_ascii_lowercase().as_str() {
338 "user" => Ok(MacKeychainDomain::User),
339 "system" => Ok(MacKeychainDomain::System),
340 "common" => Ok(MacKeychainDomain::Common),
341 "dynamic" => Ok(MacKeychainDomain::Dynamic),
342 _ => Err(ErrorCode::Invalid(
343 "keychain".to_string(),
344 format!("'{s}' is not User, System, Common, or Dynamic"),
345 )),
346 }
347 }
348}
349
350fn get_keychain(domain: &MacKeychainDomain) -> Result<SecKeychain> {
351 let domain = match domain {
352 MacKeychainDomain::User => SecPreferencesDomain::User,
353 MacKeychainDomain::System => SecPreferencesDomain::System,
354 MacKeychainDomain::Common => SecPreferencesDomain::Common,
355 MacKeychainDomain::Dynamic => SecPreferencesDomain::Dynamic,
356 };
357 match SecKeychain::default_for_domain(domain) {
358 Ok(keychain) => Ok(keychain),
359 Err(err) => Err(decode_error(err)),
360 }
361}
362
363pub fn decode_error(err: Error) -> ErrorCode {
368 match err.code() {
369 -61 => ErrorCode::NoStorageAccess(Box::new(err)), -25244 => ErrorCode::NoStorageAccess(Box::new(err)), -25291 => ErrorCode::NoStorageAccess(Box::new(err)), -25292 => ErrorCode::NoStorageAccess(Box::new(err)), -25294 => ErrorCode::NoStorageAccess(Box::new(err)), -25295 => ErrorCode::NoStorageAccess(Box::new(err)), -25300 => ErrorCode::NoEntry, _ => ErrorCode::PlatformFailure(Box::new(err)),
377 }
378}