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/// Validated alias for a stored key.
50///
51/// Invariants: non-empty and contains no null bytes.
52///
53/// Usage:
54/// ```ignore
55/// let alias = KeyAlias::new("my-signing-key")?;
56/// keychain.store_key(&alias, &did, &encrypted)?;
57/// ```
58#[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    /// Creates a validated `KeyAlias`.
67    ///
68    /// Rejects empty strings and strings containing null bytes.
69    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    /// Wraps a string without validation (for trusted internal paths).
85    pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
86        Self(s.into())
87    }
88
89    /// Returns the alias as a string slice.
90    pub fn as_str(&self) -> &str {
91        &self.0
92    }
93
94    /// Consumes self and returns the inner `String`.
95    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
149/// Platform-agnostic interface for storing and loading private keys securely.
150///
151/// All implementors must be Send + Sync for thread-safe access.
152pub trait KeyStorage: Send + Sync {
153    /// Stores encrypted key data associated with an alias AND an identity DID.
154    fn store_key(
155        &self,
156        alias: &KeyAlias,
157        identity_did: &IdentityDID,
158        encrypted_key_data: &[u8],
159    ) -> Result<(), AgentError>;
160
161    /// Loads the encrypted key data AND the associated identity DID for a given alias.
162    fn load_key(&self, alias: &KeyAlias) -> Result<(IdentityDID, Vec<u8>), AgentError>;
163
164    /// Deletes a key by its alias.
165    fn delete_key(&self, alias: &KeyAlias) -> Result<(), AgentError>;
166
167    /// Lists all aliases stored by this backend for the specific service.
168    fn list_aliases(&self) -> Result<Vec<KeyAlias>, AgentError>;
169
170    /// Lists aliases associated ONLY with the given identity DID.
171    fn list_aliases_for_identity(
172        &self,
173        identity_did: &IdentityDID,
174    ) -> Result<Vec<KeyAlias>, AgentError>;
175
176    /// Retrieves the identity DID associated with a given alias.
177    fn get_identity_for_alias(&self, alias: &KeyAlias) -> Result<IdentityDID, AgentError>;
178
179    /// Returns the name of the storage backend.
180    fn backend_name(&self) -> &'static str;
181}
182
183/// Decrypt a stored key and return its Ed25519 public key bytes.
184///
185/// Loads the encrypted key for `alias`, calls `passphrase_provider` to obtain
186/// the decryption passphrase, decrypts the PKCS8 blob, and returns the raw
187/// 32-byte public key.
188///
189/// Args:
190/// * `keychain`: The key storage backend holding the encrypted key.
191/// * `alias`: Keychain alias of the stored key.
192/// * `passphrase_provider`: Provider to obtain the decryption passphrase.
193///
194/// Usage:
195/// ```ignore
196/// let pk = extract_public_key_bytes(keychain, "my-key", &provider)?;
197/// let device_did = DeviceDID::from_ed25519(pk.as_slice().try_into()?);
198/// ```
199pub 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
215/// Return a boxed `KeyStorage` implementation driven by the supplied `EnvironmentConfig`.
216///
217/// Uses `config.keychain.backend` to select the backend and `config.keychain.file_path`
218/// / `config.keychain.passphrase` for the encrypted-file backend. Falls back to the
219/// platform default when no override is specified.
220///
221/// Args:
222/// * `config` - The environment configuration carrying keychain settings and home path.
223///
224/// Usage:
225/// ```ignore
226/// let env = EnvironmentConfig::from_env();
227/// let keychain = get_platform_keychain_with_config(&env)?;
228/// ```
229pub 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
238/// Return a boxed KeyStorage implementation for the current platform.
239///
240/// Reads keychain configuration from environment variables via
241/// `EnvironmentConfig::from_env()`. Prefer `get_platform_keychain_with_config`
242/// for new code to keep env-var reads at the process boundary.
243///
244/// # Environment Variable Override
245///
246/// Set `AUTHS_KEYCHAIN_BACKEND` to override the platform default:
247/// - `"file"` - Use encrypted file storage at `~/.auths/keys.enc`
248/// - `"memory"` - Use in-memory storage (for testing only)
249///
250/// Invalid values will log a warning and use the platform default.
251///
252/// # Errors
253/// Returns `AgentError` if the platform keychain fails to initialize.
254pub fn get_platform_keychain() -> Result<Box<dyn KeyStorage + Send + Sync>, AgentError> {
255    get_platform_keychain_with_config(&EnvironmentConfig::from_env())
256}
257
258/// Get the platform-default keychain backend.
259#[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        // Try Secret Service first, fall back to encrypted file storage
276        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        // No Secret Service feature, check for file fallback
298        #[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    // Fallback for unsupported platforms or missing features
318    #[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
327/// Get a keychain backend by name (for environment variable override).
328fn 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
352/// Construct an `EncryptedFileStorage` from the provided config.
353///
354/// Uses `config.keychain.file_path` when set; otherwise resolves the default
355/// path from `config.auths_home` (or `~/.auths/keys.enc`).
356/// Sets the password from `config.keychain.passphrase` when present.
357fn 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}