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/// A role token read from persistent storage is not a recognized role. Returned (never defaulted) so
84/// a corrupt or unknown stored role fails closed instead of silently becoming the most-privileged key.
85#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
86#[error("stored key role {0:?} is not a recognized role")]
87pub struct UnknownKeyRole(String);
88
89impl KeyRole {
90    /// Parse a role token read back from a keychain/credential store. An unrecognized token is
91    /// refused ([`UnknownKeyRole`]) rather than defaulted, so a corrupt store entry cannot load a key
92    /// as the most-privileged `Primary` role. Every backend decodes a stored role here.
93    ///
94    /// Args:
95    /// * `token`: the role string read from the store (e.g. `"primary"`, `"delegated_agent"`).
96    ///
97    /// Usage:
98    /// ```ignore
99    /// let role = KeyRole::from_persisted(stored_token)?;
100    /// ```
101    pub fn from_persisted(token: &str) -> Result<KeyRole, UnknownKeyRole> {
102        token.parse().map_err(|_| UnknownKeyRole(token.to_string()))
103    }
104}
105
106/// Validated alias for a stored key.
107///
108/// Invariants: non-empty and contains no null bytes.
109///
110/// Usage:
111/// ```ignore
112/// let alias = KeyAlias::new("my-signing-key")?;
113/// keychain.store_key(&alias, &did, &encrypted)?;
114/// ```
115#[derive(
116    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
117)]
118#[serde(transparent)]
119#[repr(transparent)]
120pub struct KeyAlias(String);
121
122impl KeyAlias {
123    /// Creates a validated `KeyAlias`.
124    ///
125    /// Rejects empty strings and strings containing null bytes.
126    pub fn new<S: Into<String>>(s: S) -> Result<Self, AgentError> {
127        let s = s.into();
128        if s.is_empty() {
129            return Err(AgentError::InvalidInput(
130                "key alias must not be empty".into(),
131            ));
132        }
133        if s.contains('\0') {
134            return Err(AgentError::InvalidInput(
135                "key alias must not contain null bytes".into(),
136            ));
137        }
138        Ok(Self(s))
139    }
140
141    /// Wraps a string without validation (for trusted internal paths).
142    pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
143        Self(s.into())
144    }
145
146    /// Returns the alias as a string slice.
147    pub fn as_str(&self) -> &str {
148        &self.0
149    }
150
151    /// Consumes self and returns the inner `String`.
152    pub fn into_inner(self) -> String {
153        self.0
154    }
155}
156
157impl fmt::Display for KeyAlias {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        f.write_str(&self.0)
160    }
161}
162
163impl Deref for KeyAlias {
164    type Target = str;
165    fn deref(&self) -> &Self::Target {
166        &self.0
167    }
168}
169
170impl AsRef<str> for KeyAlias {
171    fn as_ref(&self) -> &str {
172        &self.0
173    }
174}
175
176impl Borrow<str> for KeyAlias {
177    fn borrow(&self) -> &str {
178        &self.0
179    }
180}
181
182impl From<KeyAlias> for String {
183    fn from(alias: KeyAlias) -> String {
184        alias.0
185    }
186}
187
188impl PartialEq<str> for KeyAlias {
189    fn eq(&self, other: &str) -> bool {
190        self.0 == other
191    }
192}
193
194impl PartialEq<&str> for KeyAlias {
195    fn eq(&self, other: &&str) -> bool {
196        self.0 == *other
197    }
198}
199
200impl PartialEq<String> for KeyAlias {
201    fn eq(&self, other: &String) -> bool {
202        self.0 == *other
203    }
204}
205
206/// Migrate a legacy single-key alias `{alias}` to the multi-key form
207/// `{alias}--0`.
208///
209/// Idempotent: safe to call whether or not the migration has already
210/// happened. Reads the legacy slot, writes to the new slot, deletes the
211/// legacy slot. Returns the new alias form.
212///
213/// Behavior matrix:
214/// - Both legacy and `--0` present: no-op, returns `{alias}--0` (assumes
215///   `--0` is authoritative since it's the new convention).
216/// - Only legacy present: copies to `--0`, deletes legacy, returns `--0`.
217/// - Only `--0` present: no-op, returns `--0`.
218/// - Neither present: returns `--0` anyway; the caller's next `load_key`
219///   will surface the absence.
220pub fn migrate_legacy_alias(
221    keychain: &(dyn KeyStorage + Send + Sync),
222    old_alias: &KeyAlias,
223) -> Result<KeyAlias, AgentError> {
224    let new_alias = KeyAlias::new_unchecked(format!("{}--0", old_alias));
225
226    if keychain.load_key(&new_alias).is_ok() {
227        // Already migrated (or a fresh multi-key identity was created without
228        // ever touching the legacy alias). In either case, new form wins — but
229        // still reconcile any next-key commitments left under the legacy prefix.
230        migrate_next_key_commitments(keychain, old_alias)?;
231        return Ok(new_alias);
232    }
233
234    let (did, role, data) = match keychain.load_key(old_alias) {
235        Ok(v) => v,
236        Err(_) => {
237            // Legacy absent too; nothing to migrate. Caller hits absence on
238            // their next load_key call.
239            return Ok(new_alias);
240        }
241    };
242
243    keychain.store_key(&new_alias, &did, role, &data)?;
244    // Best-effort legacy cleanup; failure here isn't fatal.
245    let _ = keychain.delete_key(old_alias);
246
247    // The current key moved to slot 0; its pre-committed next key(s) must move with
248    // it so rotation can still derive `{current}--next-N`. Leaving them under the
249    // legacy prefix orphans the commitment and bricks all future rotation.
250    migrate_next_key_commitments(keychain, old_alias)?;
251    Ok(new_alias)
252}
253
254/// Move every `{old_alias}--next-{N}` commitment to `{old_alias}--0--next-{N}` so the
255/// next-key linkage survives the slot-0 migration of the current key. Idempotent:
256/// commitments already in the slot-0 form are left untouched.
257fn migrate_next_key_commitments(
258    keychain: &(dyn KeyStorage + Send + Sync),
259    old_alias: &KeyAlias,
260) -> Result<(), AgentError> {
261    let legacy_prefix = format!("{}--next-", old_alias);
262    let slot0_prefix = format!("{}--0--next-", old_alias);
263    for alias in keychain.list_aliases()? {
264        let Some(suffix) = alias.as_str().strip_prefix(legacy_prefix.as_str()) else {
265            continue;
266        };
267        let target = KeyAlias::new_unchecked(format!("{slot0_prefix}{suffix}"));
268        if keychain.load_key(&target).is_ok() {
269            continue;
270        }
271        let (did, role, data) = match keychain.load_key(&alias) {
272            Ok(v) => v,
273            Err(_) => continue,
274        };
275        keychain.store_key(&target, &did, role, &data)?;
276        let _ = keychain.delete_key(&alias);
277    }
278    Ok(())
279}
280
281/// Platform-agnostic interface for storing and loading private keys securely.
282///
283/// All implementors must be Send + Sync for thread-safe access.
284pub trait KeyStorage: Send + Sync {
285    /// Stores encrypted key data associated with an alias AND an identity DID.
286    fn store_key(
287        &self,
288        alias: &KeyAlias,
289        identity_did: &IdentityDID,
290        role: KeyRole,
291        encrypted_key_data: &[u8],
292    ) -> Result<(), AgentError>;
293
294    /// Loads the encrypted key data AND the associated identity DID for a given alias.
295    fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError>;
296
297    /// Deletes a key by its alias.
298    fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError>;
299
300    /// Lists all aliases stored by this backend for the specific service.
301    fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError>;
302
303    /// Lists aliases associated ONLY with the given identity DID.
304    fn list_aliases_for_identity(
305        &self,
306        identity_did: &IdentityDID,
307    ) -> Result<Vec<KeyAlias>, AgentError>;
308
309    /// List aliases for an identity filtered by role.
310    fn list_aliases_for_identity_with_role(
311        &self,
312        identity_did: &IdentityDID,
313        role: KeyRole,
314    ) -> Result<Vec<KeyAlias>, AgentError> {
315        let all = self.list_aliases_for_identity(identity_did)?;
316        let mut filtered = Vec::new();
317        for alias in all {
318            if let Ok((_, r, _)) = self.load_key(&alias)
319                && r == role
320            {
321                filtered.push(alias);
322            }
323        }
324        Ok(filtered)
325    }
326
327    /// Retrieves the identity DID associated with a given alias.
328    fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError>;
329
330    /// Returns the name of the storage backend.
331    fn backend_name(&self) -> &'static str;
332
333    /// Returns true if this backend manages keys in hardware (SE, HSM).
334    ///
335    /// Hardware backends generate keys internally, don't need passphrases,
336    /// and store opaque handles instead of encrypted PKCS8.
337    fn is_hardware_backend(&self) -> bool {
338        false
339    }
340
341    /// Re-associates an existing stored key with a different identity DID
342    /// without touching the key material.
343    ///
344    /// Needed by KERI inception: the key must exist (so its public key can be
345    /// read) before the identity prefix it belongs to can be derived. The
346    /// default implementation round-trips the stored material through
347    /// `store_key`, which is correct for software backends. Hardware backends
348    /// MUST override this — their `store_key` generates a fresh key, which
349    /// would orphan the original.
350    ///
351    /// Args:
352    /// * `alias`: Alias of the already-stored key.
353    /// * `identity_did`: The identity to associate the key with.
354    ///
355    /// Usage:
356    /// ```ignore
357    /// keychain.rebind_identity(&alias, &controller_did)?;
358    /// ```
359    fn rebind_identity(
360        &self,
361        alias: &KeyAlias,
362        identity_did: &IdentityDID,
363    ) -> Result<(), AgentError> {
364        if self.is_hardware_backend() {
365            return Err(AgentError::StorageError(format!(
366                "hardware backend '{}' must override rebind_identity",
367                self.backend_name()
368            )));
369        }
370        let (_, role, material) = self.load_key(alias)?;
371        self.store_key(alias, identity_did, role, &material)
372    }
373
374    /// Returns the raw public key bytes of a stored key without a passphrase.
375    ///
376    /// Hardware backends derive this from their opaque handle; software
377    /// backends would need a passphrase to decrypt the stored PKCS8, so the
378    /// default errors — use `extract_public_key_bytes` for those.
379    fn export_public_key(&self, alias: &KeyAlias) -> Result<Vec<u8>, AgentError> {
380        let _ = alias;
381        Err(AgentError::StorageError(format!(
382            "backend '{}' does not support passphrase-less public key export",
383            self.backend_name()
384        )))
385    }
386
387    /// Signs a message with a stored key without a passphrase.
388    ///
389    /// Hardware backends sign inside the hardware (may trigger a biometric
390    /// prompt); software backends need a passphrase, so the default errors —
391    /// use `SecureSigner::sign_with_alias` for those.
392    fn sign_raw(&self, alias: &KeyAlias, message: &[u8]) -> Result<Vec<u8>, AgentError> {
393        let _ = (alias, message);
394        Err(AgentError::StorageError(format!(
395            "backend '{}' does not support passphrase-less signing",
396            self.backend_name()
397        )))
398    }
399}
400
401/// Decrypt a stored key and return its public key bytes and curve type.
402///
403/// Loads the encrypted key for `alias`, calls `passphrase_provider` to obtain
404/// the decryption passphrase, decrypts the PKCS8 blob, and returns the raw
405/// public key along with its curve type.
406///
407/// Args:
408/// * `keychain`: The key storage backend holding the encrypted key.
409/// * `alias`: Keychain alias of the stored key.
410/// * `passphrase_provider`: Provider to obtain the decryption passphrase.
411///
412/// Usage:
413/// ```ignore
414/// let (pk, curve) = extract_public_key_bytes(keychain, "my-key", &provider)?;
415/// ```
416pub fn extract_public_key_bytes(
417    keychain: &dyn KeyStorage,
418    alias: &KeyAlias,
419    passphrase_provider: &dyn crate::signing::PassphraseProvider,
420) -> Result<(Vec<u8>, auths_crypto::CurveType), AgentError> {
421    if keychain.is_hardware_backend() {
422        // Hardware backends store opaque handles — get public key from hardware
423        let (_, _role, _handle) = keychain.load_key(alias)?;
424        #[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
425        {
426            let pubkey = super::secure_enclave::public_key_from_handle(&_handle)?;
427            return Ok((pubkey, auths_crypto::CurveType::P256));
428        }
429        #[cfg(not(all(target_os = "macos", feature = "keychain-secure-enclave")))]
430        {
431            return Err(AgentError::BackendUnavailable {
432                backend: keychain.backend_name(),
433                reason: "hardware backend not available on this platform".into(),
434            });
435        }
436    }
437
438    use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
439
440    let (_, _role, encrypted) = keychain.load_key(alias)?;
441    let passphrase = passphrase_provider
442        .get_passphrase(&format!("Enter passphrase for key '{alias}':"))
443        .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
444    let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
445    let (_, pubkey, curve) = load_seed_and_pubkey(&pkcs8)?;
446    Ok((pubkey.to_vec(), curve))
447}
448
449/// Sign a message using the key for `alias`, handling both software and hardware backends.
450///
451/// - Software: prompts for passphrase, decrypts PKCS8, signs with `TypedSeed`
452/// - Hardware (Secure Enclave): loads handle, signs via biometric (Touch ID)
453///
454/// Returns `(signature, public_key, curve)`. Callers never need to know the backend type.
455///
456/// Args:
457/// * `keychain`: The key storage backend.
458/// * `alias`: The key alias to sign with.
459/// * `passphrase_provider`: Provider for passphrase prompts (ignored on hardware backends).
460/// * `message`: The raw bytes to sign.
461pub fn sign_with_key(
462    keychain: &dyn KeyStorage,
463    alias: &KeyAlias,
464    passphrase_provider: &dyn crate::signing::PassphraseProvider,
465    message: &[u8],
466) -> Result<(Vec<u8>, Vec<u8>, auths_crypto::CurveType), AgentError> {
467    if keychain.is_hardware_backend() {
468        let (_, _role, _handle) = keychain.load_key(alias)?;
469        #[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
470        {
471            let sig = super::secure_enclave::sign_with_handle(&_handle, message)?;
472            let pubkey = super::secure_enclave::public_key_from_handle(&_handle)?;
473            return Ok((sig, pubkey, auths_crypto::CurveType::P256));
474        }
475        #[cfg(not(all(target_os = "macos", feature = "keychain-secure-enclave")))]
476        {
477            return Err(AgentError::BackendUnavailable {
478                backend: keychain.backend_name(),
479                reason: "hardware signing not available on this platform".into(),
480            });
481        }
482    }
483
484    use crate::crypto::signer::{decrypt_keypair, load_seed_and_pubkey};
485
486    let (_, _role, encrypted) = keychain.load_key(alias)?;
487    let passphrase = passphrase_provider
488        .get_passphrase(&format!("Enter passphrase for key '{}' to sign:", alias))
489        .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
490    let pkcs8 = decrypt_keypair(&encrypted, &passphrase)?;
491    let (seed, pubkey, curve) = load_seed_and_pubkey(&pkcs8)?;
492
493    let typed_seed = match curve {
494        auths_crypto::CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(*seed.as_bytes()),
495        auths_crypto::CurveType::P256 => auths_crypto::TypedSeed::P256(*seed.as_bytes()),
496    };
497
498    let sig = auths_crypto::typed_sign(&typed_seed, message)
499        .map_err(|e| AgentError::SigningFailed(e.to_string()))?;
500
501    Ok((sig, pubkey, curve))
502}
503
504/// Return a boxed `KeyStorage` implementation driven by the supplied `EnvironmentConfig`.
505///
506/// Uses `config.keychain.backend` to select the backend and `config.keychain.file_path`
507/// / `config.keychain.passphrase` for the encrypted-file backend. Falls back to the
508/// platform default when no override is specified.
509///
510/// Args:
511/// * `config` - The environment configuration carrying keychain settings and home path.
512///
513/// Usage:
514/// ```ignore
515/// let env = EnvironmentConfig::from_env();
516/// let keychain = get_platform_keychain_with_config(&env)?;
517/// ```
518pub fn get_platform_keychain_with_config(
519    config: &EnvironmentConfig,
520) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
521    if let Some(ref backend) = config.keychain.backend {
522        return get_backend_by_name(backend, config);
523    }
524    get_platform_default(config)
525}
526
527/// Return a boxed KeyStorage implementation for the current platform.
528///
529/// Reads keychain configuration from environment variables via
530/// `EnvironmentConfig::from_env()`. Prefer `get_platform_keychain_with_config`
531/// for new code to keep env-var reads at the process boundary.
532///
533/// # Environment Variable Override
534///
535/// Set `AUTHS_KEYCHAIN_BACKEND` to override the platform default:
536/// - `"file"` - Use encrypted file storage at `~/.auths/keys.enc`
537/// - `"memory"` - Use in-memory storage (for testing only)
538///
539/// Invalid values will log a warning and use the platform default.
540///
541/// # Errors
542/// Returns `AgentError` if the platform keychain fails to initialize.
543pub fn get_platform_keychain() -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
544    get_platform_keychain_with_config(&EnvironmentConfig::from_env())
545}
546
547/// Get the platform-default keychain backend.
548#[allow(unused_variables, unreachable_code)]
549fn get_platform_default(
550    config: &EnvironmentConfig,
551) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
552    #[cfg(target_os = "ios")]
553    {
554        return Ok(Box::new(IOSKeychain::new(SERVICE_NAME)));
555    }
556
557    #[cfg(target_os = "macos")]
558    {
559        // Try Secure Enclave first (fingerprint signing), fall back to macOS Keychain
560        #[cfg(feature = "keychain-secure-enclave")]
561        {
562            if super::secure_enclave::is_available()
563                && let Ok(home) = auths_home_with_config(config)
564            {
565                match super::secure_enclave::SecureEnclaveKeyStorage::new(&home) {
566                    Ok(storage) => {
567                        log::info!("Using Secure Enclave (Touch ID signing)");
568                        return Ok(Box::new(storage));
569                    }
570                    Err(e) => {
571                        log::warn!(
572                            "Secure Enclave available but init failed ({}), using macOS Keychain",
573                            e
574                        );
575                    }
576                }
577            }
578        }
579        return Ok(Box::new(MacOSKeychain::new(SERVICE_NAME)));
580    }
581
582    #[cfg(all(target_os = "linux", feature = "keychain-linux-secretservice"))]
583    {
584        // Try Secret Service first, fall back to encrypted file storage
585        match LinuxSecretServiceStorage::new(SERVICE_NAME) {
586            Ok(storage) => return Ok(Box::new(storage)),
587            Err(e) => {
588                warn!("Secret Service unavailable ({}), trying file fallback", e);
589                #[cfg(feature = "keychain-file-fallback")]
590                {
591                    return new_encrypted_file_storage(config).map(|s| {
592                        let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
593                        b
594                    });
595                }
596                #[cfg(not(feature = "keychain-file-fallback"))]
597                {
598                    return Err(e);
599                }
600            }
601        }
602    }
603
604    #[cfg(all(target_os = "linux", not(feature = "keychain-linux-secretservice")))]
605    {
606        // No Secret Service feature, check for file fallback
607        #[cfg(feature = "keychain-file-fallback")]
608        {
609            return new_encrypted_file_storage(config).map(|s| {
610                let b: Box<dyn KeyStorage + Send + Sync> = Box::new(s);
611                b
612            });
613        }
614    }
615
616    #[cfg(all(target_os = "windows", feature = "keychain-windows"))]
617    {
618        return Ok(Box::new(WindowsCredentialStorage::new(SERVICE_NAME)?));
619    }
620
621    #[cfg(target_os = "android")]
622    {
623        return Ok(Box::new(AndroidKeystoreStorage::new(SERVICE_NAME)?));
624    }
625
626    // Fallback for unsupported platforms or missing features
627    #[allow(unused_variables)]
628    let _ = config;
629    #[allow(unreachable_code)]
630    {
631        warn!("Using in-memory keychain (not recommended for production)");
632        Ok(Box::new(MemoryKeychainHandle))
633    }
634}
635
636/// Get a keychain backend by name (for environment variable override).
637fn get_backend_by_name(
638    name: &str,
639    config: &EnvironmentConfig,
640) -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
641    match name.to_lowercase().as_str() {
642        "memory" => {
643            info!("Using in-memory keychain (AUTHS_KEYCHAIN_BACKEND=memory)");
644            Ok(Box::new(MemoryKeychainHandle))
645        }
646        "file" => {
647            info!("Using encrypted file storage (AUTHS_KEYCHAIN_BACKEND=file)");
648            let storage = new_encrypted_file_storage(config)?;
649            Ok(Box::new(storage))
650        }
651        #[cfg(feature = "keychain-pkcs11")]
652        "hsm" | "pkcs11" => {
653            info!("Using PKCS#11 HSM backend (AUTHS_KEYCHAIN_BACKEND={name})");
654            let pkcs11_config =
655                config
656                    .pkcs11
657                    .as_ref()
658                    .ok_or_else(|| AgentError::BackendInitFailed {
659                        backend: "pkcs11",
660                        error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
661                    })?;
662            let storage = super::pkcs11::Pkcs11KeyRef::new(pkcs11_config)?;
663            Ok(Box::new(storage))
664        }
665        #[cfg(all(target_os = "macos", feature = "keychain-secure-enclave"))]
666        "secure-enclave" => {
667            info!("Using Secure Enclave backend (AUTHS_KEYCHAIN_BACKEND=secure-enclave)");
668            let home =
669                auths_home_with_config(config).map_err(|e| AgentError::BackendInitFailed {
670                    backend: "secure-enclave",
671                    error: format!("failed to resolve auths home: {e}"),
672                })?;
673            let storage = super::secure_enclave::SecureEnclaveKeyStorage::new(&home)?;
674            Ok(Box::new(storage))
675        }
676        _ => {
677            warn!(
678                "Unknown keychain backend '{}', using platform default",
679                name
680            );
681            get_platform_default(config)
682        }
683    }
684}
685
686/// Construct an `EncryptedFileStorage` from the provided config.
687///
688/// Uses `config.keychain.file_path` when set; otherwise resolves the default
689/// path from `config.auths_home` (or `~/.auths/keys.enc`).
690/// Sets the password from `config.keychain.passphrase` when present.
691fn new_encrypted_file_storage(
692    config: &EnvironmentConfig,
693) -> Result<EncryptedFileStorage, AgentError> {
694    let storage = if let Some(ref path) = config.keychain.file_path {
695        EncryptedFileStorage::with_path(path.clone())?
696    } else {
697        let home =
698            auths_home_with_config(config).map_err(|e| AgentError::StorageError(e.to_string()))?;
699        EncryptedFileStorage::new(&home)?
700    };
701
702    if let Some(ref passphrase) = config.keychain.passphrase {
703        storage.set_password(Zeroizing::new(passphrase.clone()));
704    }
705
706    Ok(storage)
707}
708
709/// Creates a PKCS#11-backed [`SecureSigner`](crate::signing::SecureSigner) from the
710/// environment config, if the backend is set to `"pkcs11"` or `"hsm"`.
711///
712/// Returns `None` if the keychain backend is not PKCS#11.
713///
714/// Args:
715/// * `config`: Environment configuration.
716///
717/// Usage:
718/// ```ignore
719/// if let Some(signer) = get_pkcs11_signer(&env)? {
720///     signer.sign_with_alias(&alias, &provider, message)?;
721/// }
722/// ```
723#[cfg(feature = "keychain-pkcs11")]
724pub fn get_pkcs11_signer(
725    config: &EnvironmentConfig,
726) -> Result<Option<Box<dyn crate::signing::SecureSigner>>, AgentError> {
727    let is_pkcs11 = config
728        .keychain
729        .backend
730        .as_deref()
731        .map(|b| matches!(b.to_lowercase().as_str(), "hsm" | "pkcs11"))
732        .unwrap_or(false);
733
734    if !is_pkcs11 {
735        return Ok(None);
736    }
737
738    let pkcs11_config = config
739        .pkcs11
740        .as_ref()
741        .ok_or_else(|| AgentError::BackendInitFailed {
742            backend: "pkcs11",
743            error: "PKCS#11 configuration required (set AUTHS_PKCS11_LIBRARY)".into(),
744        })?;
745
746    let signer = super::pkcs11::Pkcs11Signer::new(pkcs11_config)?;
747    Ok(Some(Box::new(signer)))
748}
749
750impl KeyStorage for Arc<dyn KeyStorage + Send + Sync> {
751    fn store_key(
752        &self,
753        alias: &KeyAlias,
754        identity_did: &IdentityDID,
755        role: KeyRole,
756        encrypted_key_data: &[u8],
757    ) -> Result<(), AgentError> {
758        self.as_ref()
759            .store_key(alias, identity_did, role, encrypted_key_data)
760    }
761    fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
762        self.as_ref().load_key(alias)
763    }
764    fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
765        self.as_ref().delete_key(alias)
766    }
767    fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
768        self.as_ref().list_aliases()
769    }
770    fn list_aliases_for_identity(
771        &self,
772        identity_did: &IdentityDID,
773    ) -> Result<Vec<KeyAlias>, AgentError> {
774        self.as_ref().list_aliases_for_identity(identity_did)
775    }
776    fn list_aliases_for_identity_with_role(
777        &self,
778        identity_did: &IdentityDID,
779        role: KeyRole,
780    ) -> Result<Vec<KeyAlias>, AgentError> {
781        self.as_ref()
782            .list_aliases_for_identity_with_role(identity_did, role)
783    }
784    fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
785        self.as_ref().get_identity_for_alias(alias)
786    }
787    fn backend_name(&self) -> &'static str {
788        self.as_ref().backend_name()
789    }
790    fn is_hardware_backend(&self) -> bool {
791        self.as_ref().is_hardware_backend()
792    }
793}
794
795impl KeyStorage for Box<dyn KeyStorage + Send + Sync> {
796    fn store_key(
797        &self,
798        alias: &KeyAlias,
799        identity_did: &IdentityDID,
800        role: KeyRole,
801        encrypted_key_data: &[u8],
802    ) -> Result<(), AgentError> {
803        self.as_ref()
804            .store_key(alias, identity_did, role, encrypted_key_data)
805    }
806    fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, KeyRole, Vec<u8>), AgentError> {
807        self.as_ref().load_key(alias)
808    }
809    fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError> {
810        self.as_ref().delete_key(alias)
811    }
812    fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError> {
813        self.as_ref().list_aliases()
814    }
815    fn list_aliases_for_identity(
816        &self,
817        identity_did: &IdentityDID,
818    ) -> Result<Vec<KeyAlias>, AgentError> {
819        self.as_ref().list_aliases_for_identity(identity_did)
820    }
821    fn list_aliases_for_identity_with_role(
822        &self,
823        identity_did: &IdentityDID,
824        role: KeyRole,
825    ) -> Result<Vec<KeyAlias>, AgentError> {
826        self.as_ref()
827            .list_aliases_for_identity_with_role(identity_did, role)
828    }
829    fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError> {
830        self.as_ref().get_identity_for_alias(alias)
831    }
832    fn backend_name(&self) -> &'static str {
833        self.as_ref().backend_name()
834    }
835    fn is_hardware_backend(&self) -> bool {
836        self.as_ref().is_hardware_backend()
837    }
838}
839
840#[cfg(test)]
841mod tests {
842    use super::*;
843
844    #[test]
845    fn test_service_name_constant() {
846        assert_eq!(SERVICE_NAME, "dev.auths.agent");
847    }
848
849    #[test]
850    fn test_get_backend_by_name_memory() {
851        let env = EnvironmentConfig::default();
852        let backend = get_backend_by_name("memory", &env).unwrap();
853        assert_eq!(backend.backend_name(), "Memory");
854    }
855
856    #[test]
857    fn test_get_backend_by_name_case_insensitive() {
858        let env = EnvironmentConfig::default();
859        let backend = get_backend_by_name("MEMORY", &env).unwrap();
860        assert_eq!(backend.backend_name(), "Memory");
861    }
862
863    #[test]
864    fn test_key_role_serde_roundtrip() {
865        let roles = [
866            KeyRole::Primary,
867            KeyRole::NextRotation,
868            KeyRole::DelegatedAgent,
869        ];
870        for role in &roles {
871            let json = serde_json::to_string(role).unwrap();
872            let parsed: KeyRole = serde_json::from_str(&json).unwrap();
873            assert_eq!(*role, parsed);
874        }
875    }
876
877    #[test]
878    fn test_key_role_display_and_parse() {
879        assert_eq!(KeyRole::Primary.to_string(), "primary");
880        assert_eq!(KeyRole::NextRotation.to_string(), "next_rotation");
881        assert_eq!(KeyRole::DelegatedAgent.to_string(), "delegated_agent");
882        assert_eq!("primary".parse::<KeyRole>().unwrap(), KeyRole::Primary);
883        assert_eq!(
884            "delegated_agent".parse::<KeyRole>().unwrap(),
885            KeyRole::DelegatedAgent
886        );
887        assert!("unknown".parse::<KeyRole>().is_err());
888    }
889
890    #[test]
891    fn a_corrupt_persisted_role_is_refused_not_elevated_to_primary() {
892        // A role token that is not one of the known roles is rejected, not loaded as Primary, so a
893        // corrupt or tampered store entry cannot become the most-privileged key.
894        assert_eq!(KeyRole::from_persisted("primary"), Ok(KeyRole::Primary));
895        assert_eq!(
896            KeyRole::from_persisted("delegated_agent"),
897            Ok(KeyRole::DelegatedAgent)
898        );
899        assert!(KeyRole::from_persisted("bogus").is_err());
900        assert!(KeyRole::from_persisted("").is_err());
901        assert!(KeyRole::from_persisted("PRIMARY").is_err());
902    }
903
904    #[test]
905    fn migrate_legacy_alias_preserves_the_next_key_commitment() {
906        use super::super::memory::IsolatedKeychainHandle;
907
908        // A failed `id expand` must not brick rotation: when the legacy current key
909        // `main` migrates to slot `main--0`, its pre-committed next key `main--next-2`
910        // must move to `main--0--next-2` so rotation can still derive `{current}--next-N`.
911        // Otherwise the commitment is orphaned and the identity can never rotate.
912        let kc = IsolatedKeychainHandle::new();
913        let did = IdentityDID::parse("did:keri:Etest").unwrap();
914        kc.store_key(
915            &KeyAlias::new_unchecked("main"),
916            &did,
917            KeyRole::Primary,
918            b"current",
919        )
920        .unwrap();
921        kc.store_key(
922            &KeyAlias::new_unchecked("main--next-2"),
923            &did,
924            KeyRole::NextRotation,
925            b"next",
926        )
927        .unwrap();
928
929        let migrated = migrate_legacy_alias(&kc, &KeyAlias::new_unchecked("main")).unwrap();
930        assert_eq!(migrated.as_str(), "main--0");
931
932        assert!(
933            kc.load_key(&KeyAlias::new_unchecked("main--0")).is_ok(),
934            "current key must migrate to slot 0"
935        );
936        assert!(
937            kc.load_key(&KeyAlias::new_unchecked("main--0--next-2"))
938                .is_ok(),
939            "the next-key commitment must migrate with the current key, or rotation is bricked"
940        );
941        assert!(kc.load_key(&KeyAlias::new_unchecked("main")).is_err());
942        assert!(
943            kc.load_key(&KeyAlias::new_unchecked("main--next-2"))
944                .is_err()
945        );
946    }
947
948    #[test]
949    fn migrate_legacy_alias_recovers_an_already_orphaned_next_key() {
950        use super::super::memory::IsolatedKeychainHandle;
951
952        // The corrupted state a pre-fix failed expand left behind: the current key is
953        // already at slot 0, but its next-key commitment was orphaned under the legacy
954        // prefix. Re-running migration must reconcile it and restore rotatability.
955        let kc = IsolatedKeychainHandle::new();
956        let did = IdentityDID::parse("did:keri:Etest").unwrap();
957        kc.store_key(
958            &KeyAlias::new_unchecked("main--0"),
959            &did,
960            KeyRole::Primary,
961            b"current",
962        )
963        .unwrap();
964        kc.store_key(
965            &KeyAlias::new_unchecked("main--next-2"),
966            &did,
967            KeyRole::NextRotation,
968            b"next",
969        )
970        .unwrap();
971
972        migrate_legacy_alias(&kc, &KeyAlias::new_unchecked("main")).unwrap();
973
974        assert!(
975            kc.load_key(&KeyAlias::new_unchecked("main--0--next-2"))
976                .is_ok(),
977            "re-running migration must recover an orphaned next-key commitment"
978        );
979        assert!(
980            kc.load_key(&KeyAlias::new_unchecked("main--next-2"))
981                .is_err()
982        );
983    }
984
985    #[test]
986    fn test_list_aliases_with_role_filter() {
987        use super::super::memory::IsolatedKeychainHandle;
988
989        let keychain = IsolatedKeychainHandle::new();
990        let did = IdentityDID::parse("did:keri:Etest").unwrap();
991
992        keychain
993            .store_key(
994                &KeyAlias::new_unchecked("operator"),
995                &did,
996                KeyRole::Primary,
997                b"key1",
998            )
999            .unwrap();
1000        keychain
1001            .store_key(
1002                &KeyAlias::new_unchecked("operator--next-0"),
1003                &did,
1004                KeyRole::NextRotation,
1005                b"key2",
1006            )
1007            .unwrap();
1008        keychain
1009            .store_key(
1010                &KeyAlias::new_unchecked("deploy-agent"),
1011                &did,
1012                KeyRole::DelegatedAgent,
1013                b"key3",
1014            )
1015            .unwrap();
1016
1017        let primary = keychain
1018            .list_aliases_for_identity_with_role(&did, KeyRole::Primary)
1019            .unwrap();
1020        assert_eq!(primary.len(), 1);
1021        assert_eq!(primary[0].as_str(), "operator");
1022
1023        let agents = keychain
1024            .list_aliases_for_identity_with_role(&did, KeyRole::DelegatedAgent)
1025            .unwrap();
1026        assert_eq!(agents.len(), 1);
1027        assert_eq!(agents[0].as_str(), "deploy-agent");
1028    }
1029}