Skip to main content

auths_core/storage/
keychain.rs

1//! Keychain abstraction.
2
3use 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/// Service name used for all platform keychains.
32/// Used inside cfg-gated blocks (macOS, iOS, Linux, Windows, Android).
33#[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
46// Re-exported from auths-verifier (the leaf dependency shared by all crates).
47pub use auths_verifier::IdentityDID;
48
49/// The role a stored key serves within its identity.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
51#[serde(rename_all = "snake_case")]
52pub enum KeyRole {
53    /// The identity's current active signing key.
54    Primary,
55    /// A pre-committed rotation key (not yet active).
56    NextRotation,
57    /// A key delegated to an autonomous agent.
58    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/// Validated alias for a stored key.
84///
85/// Invariants: non-empty and contains no null bytes.
86///
87/// Usage:
88/// ```ignore
89/// let alias = KeyAlias::new("my-signing-key")?;
90/// keychain.store_key(&alias, &did, &encrypted)?;
91/// ```
92#[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    /// Creates a validated `KeyAlias`.
101    ///
102    /// Rejects empty strings and strings containing null bytes.
103    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    /// Wraps a string without validation (for trusted internal paths).
119    pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
120        Self(s.into())
121    }
122
123    /// Returns the alias as a string slice.
124    pub fn as_str(&self) -> &str {
125        &self.0
126    }
127
128    /// Consumes self and returns the inner `String`.
129    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
183/// Platform-agnostic interface for storing and loading private keys securely.
184///
185/// All implementors must be Send + Sync for thread-safe access.
186pub trait KeyStorage: Send + Sync {
187    /// Stores encrypted key data associated with an alias AND an identity DID.
188    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    /// Loads the encrypted key data AND the associated identity DID for a given alias.
197    fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError>;
198
199    /// Deletes a key by its alias.
200    fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError>;
201
202    /// Lists all aliases stored by this backend for the specific service.
203    fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError>;
204
205    /// Lists aliases associated ONLY with the given identity DID.
206    fn list_aliases_for_identity(
207        &self,
208        identity_did: &IdentityDID,
209    ) -> Result<Vec<KeyAlias>, AgentError>;
210
211    /// List aliases for an identity filtered by role.
212    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    /// Retrieves the identity DID associated with a given alias.
230    fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError>;
231
232    /// Returns the name of the storage backend.
233    fn backend_name(&self) -> &'static str;
234}
235
236/// Decrypt a stored key and return its Ed25519 public key bytes.
237///
238/// Loads the encrypted key for `alias`, calls `passphrase_provider` to obtain
239/// the decryption passphrase, decrypts the PKCS8 blob, and returns the raw
240/// 32-byte public key.
241///
242/// Args:
243/// * `keychain`: The key storage backend holding the encrypted key.
244/// * `alias`: Keychain alias of the stored key.
245/// * `passphrase_provider`: Provider to obtain the decryption passphrase.
246///
247/// Usage:
248/// ```ignore
249/// let pk = extract_public_key_bytes(keychain, "my-key", &provider)?;
250/// let device_did = DeviceDID::from_ed25519(pk.as_slice().try_into()?);
251/// ```
252pub 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
268/// Return a boxed `KeyStorage` implementation driven by the supplied `EnvironmentConfig`.
269///
270/// Uses `config.keychain.backend` to select the backend and `config.keychain.file_path`
271/// / `config.keychain.passphrase` for the encrypted-file backend. Falls back to the
272/// platform default when no override is specified.
273///
274/// Args:
275/// * `config` - The environment configuration carrying keychain settings and home path.
276///
277/// Usage:
278/// ```ignore
279/// let env = EnvironmentConfig::from_env();
280/// let keychain = get_platform_keychain_with_config(&env)?;
281/// ```
282pub 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
291/// Return a boxed KeyStorage implementation for the current platform.
292///
293/// Reads keychain configuration from environment variables via
294/// `EnvironmentConfig::from_env()`. Prefer `get_platform_keychain_with_config`
295/// for new code to keep env-var reads at the process boundary.
296///
297/// # Environment Variable Override
298///
299/// Set `AUTHS_KEYCHAIN_BACKEND` to override the platform default:
300/// - `"file"` - Use encrypted file storage at `~/.auths/keys.enc`
301/// - `"memory"` - Use in-memory storage (for testing only)
302///
303/// Invalid values will log a warning and use the platform default.
304///
305/// # Errors
306/// Returns `AgentError` if the platform keychain fails to initialize.
307pub fn get_platform_keychain() -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
308    get_platform_keychain_with_config(&EnvironmentConfig::from_env())
309}
310
311/// Get the platform-default keychain backend.
312#[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        // Try Secret Service first, fall back to encrypted file storage
329        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        // No Secret Service feature, check for file fallback
351        #[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    // Fallback for unsupported platforms or missing features
371    #[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
380/// Get a keychain backend by name (for environment variable override).
381fn 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
419/// Construct an `EncryptedFileStorage` from the provided config.
420///
421/// Uses `config.keychain.file_path` when set; otherwise resolves the default
422/// path from `config.auths_home` (or `~/.auths/keys.enc`).
423/// Sets the password from `config.keychain.passphrase` when present.
424fn 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/// Creates a PKCS#11-backed [`SecureSigner`](crate::signing::SecureSigner) from the
443/// environment config, if the backend is set to `"pkcs11"` or `"hsm"`.
444///
445/// Returns `None` if the keychain backend is not PKCS#11.
446///
447/// Args:
448/// * `config`: Environment configuration.
449///
450/// Usage:
451/// ```ignore
452/// if let Some(signer) = get_pkcs11_signer(&env)? {
453///     signer.sign_with_alias(&alias, &provider, message)?;
454/// }
455/// ```
456#[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        // INVARIANT: test-only literal with valid did:keri: prefix
624        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}