1use crate::config::EnvironmentConfig;
4use crate::error::AgentError;
5use crate::paths::auths_home_with_config;
6use log::{info, warn};
7use std::sync::Arc;
8
9#[cfg(target_os = "ios")]
10use super::ios_keychain::IOSKeychain;
11
12#[cfg(target_os = "macos")]
13use super::macos_keychain::MacOSKeychain;
14
15#[cfg(all(target_os = "linux", feature = "keychain-linux-secretservice"))]
16use super::linux_secret_service::LinuxSecretServiceStorage;
17
18#[cfg(all(target_os = "windows", feature = "keychain-windows"))]
19use super::windows_credential::WindowsCredentialStorage;
20
21#[cfg(target_os = "android")]
22use super::android_keystore::AndroidKeystoreStorage;
23
24use super::encrypted_file::EncryptedFileStorage;
25use super::memory::MemoryKeychainHandle;
26use std::borrow::Borrow;
27use std::fmt;
28use std::ops::Deref;
29use zeroize::Zeroizing;
30
31#[cfg_attr(
34 not(any(
35 target_os = "macos",
36 target_os = "ios",
37 target_os = "android",
38 all(target_os = "linux", feature = "keychain-linux-secretservice"),
39 all(target_os = "windows", feature = "keychain-windows"),
40 test,
41 )),
42 allow(dead_code)
43)]
44const SERVICE_NAME: &str = "dev.auths.agent";
45
46pub use auths_verifier::IdentityDID;
48
49#[derive(
59 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
60)]
61#[serde(transparent)]
62#[repr(transparent)]
63pub struct KeyAlias(String);
64
65impl KeyAlias {
66 pub fn new<S: Into<String>>(s: S) -> Result<Self, AgentError> {
70 let s = s.into();
71 if s.is_empty() {
72 return Err(AgentError::InvalidInput(
73 "key alias must not be empty".into(),
74 ));
75 }
76 if s.contains('\0') {
77 return Err(AgentError::InvalidInput(
78 "key alias must not contain null bytes".into(),
79 ));
80 }
81 Ok(Self(s))
82 }
83
84 pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
86 Self(s.into())
87 }
88
89 pub fn as_str(&self) -> &str {
91 &self.0
92 }
93
94 pub fn into_inner(self) -> String {
96 self.0
97 }
98}
99
100impl fmt::Display for KeyAlias {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 f.write_str(&self.0)
103 }
104}
105
106impl Deref for KeyAlias {
107 type Target = str;
108 fn deref(&self) -> &Self::Target {
109 &self.0
110 }
111}
112
113impl AsRef<str> for KeyAlias {
114 fn as_ref(&self) -> &str {
115 &self.0
116 }
117}
118
119impl Borrow<str> for KeyAlias {
120 fn borrow(&self) -> &str {
121 &self.0
122 }
123}
124
125impl From<KeyAlias> for String {
126 fn from(alias: KeyAlias) -> String {
127 alias.0
128 }
129}
130
131impl PartialEq<str> for KeyAlias {
132 fn eq(&self, other: &str) -> bool {
133 self.0 == other
134 }
135}
136
137impl PartialEq<&str> for KeyAlias {
138 fn eq(&self, other: &&str) -> bool {
139 self.0 == *other
140 }
141}
142
143impl PartialEq<String> for KeyAlias {
144 fn eq(&self, other: &String) -> bool {
145 self.0 == *other
146 }
147}
148
149pub trait KeyStorage: Send + Sync {
153 fn store_key(
155 &self,
156 alias: &KeyAlias,
157 identity_did: &IdentityDID,
158 encrypted_key_data: &[u8],
159 ) -> Result<(), AgentError>;
160
161 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, Vec<u8>), AgentError>;
163
164 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError>;
166
167 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError>;
169
170 fn list_aliases_for_identity(
172 &self,
173 identity_did: &IdentityDID,
174 ) -> Result<Vec<KeyAlias>, AgentError>;
175
176 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError>;
178
179 fn backend_name(&self) -> &'static str;
181}
182
183pub fn extract_public_key_bytes(
200 keychain: &dyn KeyStorage,
201 alias: &KeyAlias,
202 passphrase_provider: &dyn crate::signing::PassphraseProvider,
203) -> Result<Vec<u8>, AgentError> {
204 use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
205
206 let (_, encrypted) = keychain.load_key(alias)?;
207 let passphrase = passphrase_provider
208 .get_passphrase(&format!("Enter passphrase for key '{alias}':"))
209 .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
210 let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
211 let (_, pubkey) = load_seed_and_pubkey(&pkcs8)?;
212 Ok(pubkey.to_vec())
213}
214
215pub fn get_platform_keychain_with_config(
230 config: &EnvironmentConfig,
231) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
232 if let Some(ref backend) = config.keychain.backend {
233 return get_backend_by_name(backend, config);
234 }
235 get_platform_default(config)
236}
237
238pub fn get_platform_keychain() -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
255 get_platform_keychain_with_config(&EnvironmentConfig::from_env())
256}
257
258#[allow(unused_variables, unreachable_code)]
260fn get_platform_default(
261 config: &EnvironmentConfig,
262) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
263 #[cfg(target_os = "ios")]
264 {
265 return Ok(Box::new(IOSKeychain::new(SERVICE_NAME)));
266 }
267
268 #[cfg(target_os = "macos")]
269 {
270 return Ok(Box::new(MacOSKeychain::new(SERVICE_NAME)));
271 }
272
273 #[cfg(all(target_os = "linux", feature = "keychain-linux-secretservice"))]
274 {
275 match LinuxSecretServiceStorage::new(SERVICE_NAME) {
277 Ok(storage) => return Ok(Box::new(storage)),
278 Err(e) => {
279 warn!("Secret Service unavailable ({}), trying file fallback", e);
280 #[cfg(feature = "keychain-file-fallback")]
281 {
282 return new_encrypted_file_storage(config).map(|s| {
283 let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
284 b
285 });
286 }
287 #[cfg(not(feature = "keychain-file-fallback"))]
288 {
289 return Err(e);
290 }
291 }
292 }
293 }
294
295 #[cfg(all(target_os = "linux", not(feature = "keychain-linux-secretservice")))]
296 {
297 #[cfg(feature = "keychain-file-fallback")]
299 {
300 return new_encrypted_file_storage(config).map(|s| {
301 let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
302 b
303 });
304 }
305 }
306
307 #[cfg(all(target_os = "windows", feature = "keychain-windows"))]
308 {
309 return Ok(Box::new(WindowsCredentialStorage::new(SERVICE_NAME)?));
310 }
311
312 #[cfg(target_os = "android")]
313 {
314 return Ok(Box::new(AndroidKeystoreStorage::new(SERVICE_NAME)?));
315 }
316
317 #[allow(unused_variables)]
319 let _ = config;
320 #[allow(unreachable_code)]
321 {
322 warn!("Using in-memory keychain (not recommended for production)");
323 Ok(Box::new(MemoryKeychainHandle))
324 }
325}
326
327fn get_backend_by_name(
329 name: &str,
330 config: &EnvironmentConfig,
331) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
332 match name.to_lowercase().as_str() {
333 "memory" => {
334 info!("Using in-memory keychain (AUTHS_KEYCHAIN_BACKEND=memory)");
335 Ok(Box::new(MemoryKeychainHandle))
336 }
337 "file" => {
338 info!("Using encrypted file storage (AUTHS_KEYCHAIN_BACKEND=file)");
339 let storage = new_encrypted_file_storage(config)?;
340 Ok(Box::new(storage))
341 }
342 _ => {
343 warn!(
344 "Unknown keychain backend '{}', using platform default",
345 name
346 );
347 get_platform_default(config)
348 }
349 }
350}
351
352fn new_encrypted_file_storage(
358 config: &EnvironmentConfig,
359) -> Result<EncryptedFileStorage, AgentError> {
360 let storage = if let Some(ref path) = config.keychain.file_path {
361 EncryptedFileStorage::with_path(path.clone())?
362 } else {
363 let home =
364 auths_home_with_config(config).map_err(|e| AgentError::StorageError(e.to_string()))?;
365 EncryptedFileStorage::new(&home)?
366 };
367
368 if let Some(ref passphrase) = config.keychain.passphrase {
369 storage.set_password(Zeroizing::new(passphrase.clone()));
370 }
371
372 Ok(storage)
373}
374
375impl KeyStorage for Arc<dyn KeyStorage + Send + Sync> {
376 fn store_key(
377 &self,
378 alias: &KeyAlias,
379 identity_did: &IdentityDID,
380 encrypted_key_data: &[u8],
381 ) -> Result<(), AgentError> {
382 self.as_ref()
383 .store_key(alias, identity_did, encrypted_key_data)
384 }
385 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, Vec<u8>), AgentError> {
386 self.as_ref().load_key(alias)
387 }
388 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
389 self.as_ref().delete_key(alias)
390 }
391 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
392 self.as_ref().list_aliases()
393 }
394 fn list_aliases_for_identity(
395 &self,
396 identity_did: &IdentityDID,
397 ) -> Result<Vec<KeyAlias>, AgentError> {
398 self.as_ref().list_aliases_for_identity(identity_did)
399 }
400 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
401 self.as_ref().get_identity_for_alias(alias)
402 }
403 fn backend_name(&self) -> &'static str {
404 self.as_ref().backend_name()
405 }
406}
407
408impl KeyStorage for Box<dyn KeyStorage + Send + Sync> {
409 fn store_key(
410 &self,
411 alias: &KeyAlias,
412 identity_did: &IdentityDID,
413 encrypted_key_data: &[u8],
414 ) -> Result<(), AgentError> {
415 self.as_ref()
416 .store_key(alias, identity_did, encrypted_key_data)
417 }
418 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, Vec<u8>), AgentError> {
419 self.as_ref().load_key(alias)
420 }
421 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
422 self.as_ref().delete_key(alias)
423 }
424 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
425 self.as_ref().list_aliases()
426 }
427 fn list_aliases_for_identity(
428 &self,
429 identity_did: &IdentityDID,
430 ) -> Result<Vec<KeyAlias>, AgentError> {
431 self.as_ref().list_aliases_for_identity(identity_did)
432 }
433 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
434 self.as_ref().get_identity_for_alias(alias)
435 }
436 fn backend_name(&self) -> &'static str {
437 self.as_ref().backend_name()
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn test_service_name_constant() {
447 assert_eq!(SERVICE_NAME, "dev.auths.agent");
448 }
449
450 #[test]
451 fn test_get_backend_by_name_memory() {
452 let env = EnvironmentConfig::default();
453 let backend = get_backend_by_name("memory", &env).unwrap();
454 assert_eq!(backend.backend_name(), "Memory");
455 }
456
457 #[test]
458 fn test_get_backend_by_name_case_insensitive() {
459 let env = EnvironmentConfig::default();
460 let backend = get_backend_by_name("MEMORY", &env).unwrap();
461 assert_eq!(backend.backend_name(), "Memory");
462 }
463}