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(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum KeyRole {
53 Primary,
55 NextRotation,
57 DelegatedAgent,
59}
60
61impl std::fmt::Display for KeyRole {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 KeyRole::Primary => write!(f, "primary"),
65 KeyRole::NextRotation => write!(f, "next_rotation"),
66 KeyRole::DelegatedAgent => write!(f, "delegated_agent"),
67 }
68 }
69}
70
71impl std::str::FromStr for KeyRole {
72 type Err = String;
73 fn from_str(s: &str) -> Result<Self, Self::Err> {
74 match s {
75 "primary" => Ok(KeyRole::Primary),
76 "next_rotation" => Ok(KeyRole::NextRotation),
77 "delegated_agent" => Ok(KeyRole::DelegatedAgent),
78 other => Err(format!("unknown key role: {other}")),
79 }
80 }
81}
82
83#[derive(
93 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
94)]
95#[serde(transparent)]
96#[repr(transparent)]
97pub struct KeyAlias(String);
98
99impl KeyAlias {
100 pub fn new<S: Into<String>>(s: S) -> Result<Self, AgentError> {
104 let s = s.into();
105 if s.is_empty() {
106 return Err(AgentError::InvalidInput(
107 "key alias must not be empty".into(),
108 ));
109 }
110 if s.contains('\0') {
111 return Err(AgentError::InvalidInput(
112 "key alias must not contain null bytes".into(),
113 ));
114 }
115 Ok(Self(s))
116 }
117
118 pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
120 Self(s.into())
121 }
122
123 pub fn as_str(&self) -> &str {
125 &self.0
126 }
127
128 pub fn into_inner(self) -> String {
130 self.0
131 }
132}
133
134impl fmt::Display for KeyAlias {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(&self.0)
137 }
138}
139
140impl Deref for KeyAlias {
141 type Target = str;
142 fn deref(&self) -> &Self::Target {
143 &self.0
144 }
145}
146
147impl AsRef<str> for KeyAlias {
148 fn as_ref(&self) -> &str {
149 &self.0
150 }
151}
152
153impl Borrow<str> for KeyAlias {
154 fn borrow(&self) -> &str {
155 &self.0
156 }
157}
158
159impl From<KeyAlias> for String {
160 fn from(alias: KeyAlias) -> String {
161 alias.0
162 }
163}
164
165impl PartialEq<str> for KeyAlias {
166 fn eq(&self, other: &str) -> bool {
167 self.0 == other
168 }
169}
170
171impl PartialEq<&str> for KeyAlias {
172 fn eq(&self, other: &&str) -> bool {
173 self.0 == *other
174 }
175}
176
177impl PartialEq<String> for KeyAlias {
178 fn eq(&self, other: &String) -> bool {
179 self.0 == *other
180 }
181}
182
183pub trait KeyStorage: Send + Sync {
187 fn store_key(
189 &self,
190 alias: &KeyAlias,
191 identity_did: &IdentityDID,
192 role: KeyRole,
193 encrypted_key_data: &[u8],
194 ) -> Result<(), AgentError>;
195
196 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError>;
198
199 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError>;
201
202 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError>;
204
205 fn list_aliases_for_identity(
207 &self,
208 identity_did: &IdentityDID,
209 ) -> Result<Vec<KeyAlias>, AgentError>;
210
211 fn list_aliases_for_identity_with_role(
213 &self,
214 identity_did: &IdentityDID,
215 role: KeyRole,
216 ) -> Result<Vec<KeyAlias>, AgentError> {
217 let all = self.list_aliases_for_identity(identity_did)?;
218 let mut filtered = Vec::new();
219 for alias in all {
220 if let Ok((_, r, _)) = self.load_key(&alias)
221 && r == role
222 {
223 filtered.push(alias);
224 }
225 }
226 Ok(filtered)
227 }
228
229 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError>;
231
232 fn backend_name(&self) -> &'static str;
234}
235
236pub fn extract_public_key_bytes(
253 keychain: &dyn KeyStorage,
254 alias: &KeyAlias,
255 passphrase_provider: &dyn crate::signing::PassphraseProvider,
256) -> Result<Vec<u8>, AgentError> {
257 use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
258
259 let (_, _role, encrypted) = keychain.load_key(alias)?;
260 let passphrase = passphrase_provider
261 .get_passphrase(&format!("Enter passphrase for key '{alias}':"))
262 .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
263 let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
264 let (_, pubkey) = load_seed_and_pubkey(&pkcs8)?;
265 Ok(pubkey.to_vec())
266}
267
268pub fn get_platform_keychain_with_config(
283 config: &EnvironmentConfig,
284) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
285 if let Some(ref backend) = config.keychain.backend {
286 return get_backend_by_name(backend, config);
287 }
288 get_platform_default(config)
289}
290
291pub fn get_platform_keychain() -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
308 get_platform_keychain_with_config(&EnvironmentConfig::from_env())
309}
310
311#[allow(unused_variables, unreachable_code)]
313fn get_platform_default(
314 config: &EnvironmentConfig,
315) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
316 #[cfg(target_os = "ios")]
317 {
318 return Ok(Box::new(IOSKeychain::new(SERVICE_NAME)));
319 }
320
321 #[cfg(target_os = "macos")]
322 {
323 return Ok(Box::new(MacOSKeychain::new(SERVICE_NAME)));
324 }
325
326 #[cfg(all(target_os = "linux", feature = "keychain-linux-secretservice"))]
327 {
328 match LinuxSecretServiceStorage::new(SERVICE_NAME) {
330 Ok(storage) => return Ok(Box::new(storage)),
331 Err(e) => {
332 warn!("Secret Service unavailable ({}), trying file fallback", e);
333 #[cfg(feature = "keychain-file-fallback")]
334 {
335 return new_encrypted_file_storage(config).map(|s| {
336 let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
337 b
338 });
339 }
340 #[cfg(not(feature = "keychain-file-fallback"))]
341 {
342 return Err(e);
343 }
344 }
345 }
346 }
347
348 #[cfg(all(target_os = "linux", not(feature = "keychain-linux-secretservice")))]
349 {
350 #[cfg(feature = "keychain-file-fallback")]
352 {
353 return new_encrypted_file_storage(config).map(|s| {
354 let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
355 b
356 });
357 }
358 }
359
360 #[cfg(all(target_os = "windows", feature = "keychain-windows"))]
361 {
362 return Ok(Box::new(WindowsCredentialStorage::new(SERVICE_NAME)?));
363 }
364
365 #[cfg(target_os = "android")]
366 {
367 return Ok(Box::new(AndroidKeystoreStorage::new(SERVICE_NAME)?));
368 }
369
370 #[allow(unused_variables)]
372 let _ = config;
373 #[allow(unreachable_code)]
374 {
375 warn!("Using in-memory keychain (not recommended for production)");
376 Ok(Box::new(MemoryKeychainHandle))
377 }
378}
379
380fn get_backend_by_name(
382 name: &str,
383 config: &EnvironmentConfig,
384) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
385 match name.to_lowercase().as_str() {
386 "memory" => {
387 info!("Using in-memory keychain (AUTHS_KEYCHAIN_BACKEND=memory)");
388 Ok(Box::new(MemoryKeychainHandle))
389 }
390 "file" => {
391 info!("Using encrypted file storage (AUTHS_KEYCHAIN_BACKEND=file)");
392 let storage = new_encrypted_file_storage(config)?;
393 Ok(Box::new(storage))
394 }
395 #[cfg(feature = "keychain-pkcs11")]
396 "hsm" | "pkcs11" => {
397 info!("Using PKCS#11 HSM backend (AUTHS_KEYCHAIN_BACKEND={name})");
398 let pkcs11_config =
399 config
400 .pkcs11
401 .as_ref()
402 .ok_or_else(|| AgentError::BackendInitFailed {
403 backend: "pkcs11",
404 error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
405 })?;
406 let storage = super::pkcs11::Pkcs11KeyRef::new(pkcs11_config)?;
407 Ok(Box::new(storage))
408 }
409 _ => {
410 warn!(
411 "Unknown keychain backend '{}', using platform default",
412 name
413 );
414 get_platform_default(config)
415 }
416 }
417}
418
419fn new_encrypted_file_storage(
425 config: &EnvironmentConfig,
426) -> Result<EncryptedFileStorage, AgentError> {
427 let storage = if let Some(ref path) = config.keychain.file_path {
428 EncryptedFileStorage::with_path(path.clone())?
429 } else {
430 let home =
431 auths_home_with_config(config).map_err(|e| AgentError::StorageError(e.to_string()))?;
432 EncryptedFileStorage::new(&home)?
433 };
434
435 if let Some(ref passphrase) = config.keychain.passphrase {
436 storage.set_password(Zeroizing::new(passphrase.clone()));
437 }
438
439 Ok(storage)
440}
441
442#[cfg(feature = "keychain-pkcs11")]
457pub fn get_pkcs11_signer(
458 config: &EnvironmentConfig,
459) -> Result<Option<Box<dyn crate::signing::SecureSigner>>, AgentError> {
460 let is_pkcs11 = config
461 .keychain
462 .backend
463 .as_deref()
464 .map(|b| matches!(b.to_lowercase().as_str(), "hsm" | "pkcs11"))
465 .unwrap_or(false);
466
467 if !is_pkcs11 {
468 return Ok(None);
469 }
470
471 let pkcs11_config = config
472 .pkcs11
473 .as_ref()
474 .ok_or_else(|| AgentError::BackendInitFailed {
475 backend: "pkcs11",
476 error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
477 })?;
478
479 let signer = super::pkcs11::Pkcs11Signer::new(pkcs11_config)?;
480 Ok(Some(Box::new(signer)))
481}
482
483impl KeyStorage for Arc<dyn KeyStorage + Send + Sync> {
484 fn store_key(
485 &self,
486 alias: &KeyAlias,
487 identity_did: &IdentityDID,
488 role: KeyRole,
489 encrypted_key_data: &[u8],
490 ) -> Result<(), AgentError> {
491 self.as_ref()
492 .store_key(alias, identity_did, role, encrypted_key_data)
493 }
494 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
495 self.as_ref().load_key(alias)
496 }
497 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
498 self.as_ref().delete_key(alias)
499 }
500 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
501 self.as_ref().list_aliases()
502 }
503 fn list_aliases_for_identity(
504 &self,
505 identity_did: &IdentityDID,
506 ) -> Result<Vec<KeyAlias>, AgentError> {
507 self.as_ref().list_aliases_for_identity(identity_did)
508 }
509 fn list_aliases_for_identity_with_role(
510 &self,
511 identity_did: &IdentityDID,
512 role: KeyRole,
513 ) -> Result<Vec<KeyAlias>, AgentError> {
514 self.as_ref()
515 .list_aliases_for_identity_with_role(identity_did, role)
516 }
517 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
518 self.as_ref().get_identity_for_alias(alias)
519 }
520 fn backend_name(&self) -> &'static str {
521 self.as_ref().backend_name()
522 }
523}
524
525impl KeyStorage for Box<dyn KeyStorage + Send + Sync> {
526 fn store_key(
527 &self,
528 alias: &KeyAlias,
529 identity_did: &IdentityDID,
530 role: KeyRole,
531 encrypted_key_data: &[u8],
532 ) -> Result<(), AgentError> {
533 self.as_ref()
534 .store_key(alias, identity_did, role, encrypted_key_data)
535 }
536 fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
537 self.as_ref().load_key(alias)
538 }
539 fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
540 self.as_ref().delete_key(alias)
541 }
542 fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
543 self.as_ref().list_aliases()
544 }
545 fn list_aliases_for_identity(
546 &self,
547 identity_did: &IdentityDID,
548 ) -> Result<Vec<KeyAlias>, AgentError> {
549 self.as_ref().list_aliases_for_identity(identity_did)
550 }
551 fn list_aliases_for_identity_with_role(
552 &self,
553 identity_did: &IdentityDID,
554 role: KeyRole,
555 ) -> Result<Vec<KeyAlias>, AgentError> {
556 self.as_ref()
557 .list_aliases_for_identity_with_role(identity_did, role)
558 }
559 fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
560 self.as_ref().get_identity_for_alias(alias)
561 }
562 fn backend_name(&self) -> &'static str {
563 self.as_ref().backend_name()
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570
571 #[test]
572 fn test_service_name_constant() {
573 assert_eq!(SERVICE_NAME, "dev.auths.agent");
574 }
575
576 #[test]
577 fn test_get_backend_by_name_memory() {
578 let env = EnvironmentConfig::default();
579 let backend = get_backend_by_name("memory", &env).unwrap();
580 assert_eq!(backend.backend_name(), "Memory");
581 }
582
583 #[test]
584 fn test_get_backend_by_name_case_insensitive() {
585 let env = EnvironmentConfig::default();
586 let backend = get_backend_by_name("MEMORY", &env).unwrap();
587 assert_eq!(backend.backend_name(), "Memory");
588 }
589
590 #[test]
591 fn test_key_role_serde_roundtrip() {
592 let roles = [
593 KeyRole::Primary,
594 KeyRole::NextRotation,
595 KeyRole::DelegatedAgent,
596 ];
597 for role in &roles {
598 let json = serde_json::to_string(role).unwrap();
599 let parsed: KeyRole = serde_json::from_str(&json).unwrap();
600 assert_eq!(*role, parsed);
601 }
602 }
603
604 #[test]
605 fn test_key_role_display_and_parse() {
606 assert_eq!(KeyRole::Primary.to_string(), "primary");
607 assert_eq!(KeyRole::NextRotation.to_string(), "next_rotation");
608 assert_eq!(KeyRole::DelegatedAgent.to_string(), "delegated_agent");
609 assert_eq!("primary".parse::<KeyRole>().unwrap(), KeyRole::Primary);
610 assert_eq!(
611 "delegated_agent".parse::<KeyRole>().unwrap(),
612 KeyRole::DelegatedAgent
613 );
614 assert!("unknown".parse::<KeyRole>().is_err());
615 }
616
617 #[test]
618 fn test_list_aliases_with_role_filter() {
619 use super::super::memory::IsolatedKeychainHandle;
620
621 let keychain = IsolatedKeychainHandle::new();
622 #[allow(clippy::disallowed_methods)]
623 let did = IdentityDID::new_unchecked("did:keri:Etest".to_string());
625
626 keychain
627 .store_key(
628 &KeyAlias::new_unchecked("operator"),
629 &did,
630 KeyRole::Primary,
631 b"key1",
632 )
633 .unwrap();
634 keychain
635 .store_key(
636 &KeyAlias::new_unchecked("operator--next-0"),
637 &did,
638 KeyRole::NextRotation,
639 b"key2",
640 )
641 .unwrap();
642 keychain
643 .store_key(
644 &KeyAlias::new_unchecked("deploy-agent"),
645 &did,
646 KeyRole::DelegatedAgent,
647 b"key3",
648 )
649 .unwrap();
650
651 let primary = keychain
652 .list_aliases_for_identity_with_role(&did, KeyRole::Primary)
653 .unwrap();
654 assert_eq!(primary.len(), 1);
655 assert_eq!(primary[0].as_str(), "operator");
656
657 let agents = keychain
658 .list_aliases_for_identity_with_role(&did, KeyRole::DelegatedAgent)
659 .unwrap();
660 assert_eq!(agents.len(), 1);
661 assert_eq!(agents[0].as_str(), "deploy-agent");
662 }
663}