Skip to main content

authy/
api.rs

1//! High-level programmatic API for the Authy vault.
2//!
3//! [`AuthyClient`] provides a simple facade over the vault, handling
4//! load → operate → save → audit in every method call.
5
6use crate::audit;
7use crate::auth;
8use crate::error::{AuthyError, Result};
9use crate::vault::{self, Vault, VaultKey};
10use crate::vault::secret::SecretEntry;
11
12/// High-level client for programmatic vault access.
13///
14/// Each operation loads the vault, performs the mutation, saves it back,
15/// and appends an audit entry — mirroring the CLI handler pattern.
16pub struct AuthyClient {
17    key: VaultKey,
18    /// HMAC key derived from the master material, used for audit chain.
19    audit_key: Vec<u8>,
20    /// Human-readable actor label for audit entries.
21    actor: String,
22}
23
24impl AuthyClient {
25    /// Authenticate with a passphrase.
26    pub fn with_passphrase(passphrase: &str) -> Result<Self> {
27        let key = VaultKey::Passphrase(passphrase.to_string());
28        let material = audit::key_material(&key);
29        let audit_key = audit::derive_audit_key(&material);
30        Ok(Self {
31            key,
32            audit_key,
33            actor: "api(passphrase)".to_string(),
34        })
35    }
36
37    /// Authenticate with an age keyfile on disk.
38    pub fn with_keyfile(keyfile_path: &str) -> Result<Self> {
39        let (identity, pubkey) = auth::read_keyfile(keyfile_path)?;
40        let key = VaultKey::Keyfile { identity, pubkey };
41        let material = audit::key_material(&key);
42        let audit_key = audit::derive_audit_key(&material);
43        Ok(Self {
44            key,
45            audit_key,
46            actor: "api(keyfile)".to_string(),
47        })
48    }
49
50    /// Authenticate from environment variables (`AUTHY_KEYFILE` or `AUTHY_PASSPHRASE`).
51    ///
52    /// This does **not** fall through to interactive prompts — it only reads env vars.
53    pub fn from_env() -> Result<Self> {
54        if let Ok(keyfile_path) = std::env::var("AUTHY_KEYFILE") {
55            return Self::with_keyfile(&keyfile_path);
56        }
57        if let Ok(passphrase) = std::env::var("AUTHY_PASSPHRASE") {
58            return Self::with_passphrase(&passphrase);
59        }
60        Err(AuthyError::AuthFailed(
61            "No credentials found. Set AUTHY_KEYFILE or AUTHY_PASSPHRASE.".into(),
62        ))
63    }
64
65    /// Override the actor label used in audit entries.
66    pub fn with_actor(mut self, actor: impl Into<String>) -> Self {
67        self.actor = actor.into();
68        self
69    }
70
71    /// Check whether the vault has been initialized.
72    pub fn is_initialized() -> bool {
73        vault::is_initialized()
74    }
75
76    /// Retrieve a secret by name. Returns `None` if not found.
77    pub fn get(&self, name: &str) -> Result<Option<String>> {
78        let v = vault::load_vault(&self.key)?;
79
80        let result = v.secrets.get(name).map(|e| e.value.clone());
81        let outcome = if result.is_some() { "success" } else { "not_found" };
82
83        self.audit("get", Some(name), outcome, None);
84        Ok(result)
85    }
86
87    /// Retrieve a secret by name, returning an error if it does not exist.
88    pub fn get_or_err(&self, name: &str) -> Result<String> {
89        self.get(name)?
90            .ok_or_else(|| AuthyError::SecretNotFound(name.to_string()))
91    }
92
93    /// Store a secret. If `force` is false and the secret already exists,
94    /// returns [`AuthyError::SecretAlreadyExists`].
95    pub fn store(&self, name: &str, value: &str, force: bool) -> Result<()> {
96        let mut v = vault::load_vault(&self.key)?;
97
98        if !force && v.secrets.contains_key(name) {
99            self.audit("store", Some(name), "denied", Some("already exists"));
100            return Err(AuthyError::SecretAlreadyExists(name.to_string()));
101        }
102
103        let is_update = v.secrets.contains_key(name);
104        v.secrets
105            .insert(name.to_string(), SecretEntry::new(value.to_string()));
106        v.touch();
107        vault::save_vault(&v, &self.key)?;
108
109        let op = if is_update { "update" } else { "store" };
110        self.audit(op, Some(name), "success", None);
111        Ok(())
112    }
113
114    /// Remove a secret. Returns `true` if the secret existed.
115    pub fn remove(&self, name: &str) -> Result<bool> {
116        let mut v = vault::load_vault(&self.key)?;
117
118        let existed = v.secrets.remove(name).is_some();
119        if existed {
120            v.touch();
121            vault::save_vault(&v, &self.key)?;
122            self.audit("remove", Some(name), "success", None);
123        } else {
124            self.audit("remove", Some(name), "not_found", None);
125        }
126
127        Ok(existed)
128    }
129
130    /// Rotate a secret to a new value. Returns the new version number.
131    /// The secret must already exist.
132    pub fn rotate(&self, name: &str, new_value: &str) -> Result<u32> {
133        let mut v = vault::load_vault(&self.key)?;
134
135        let entry = v
136            .secrets
137            .get_mut(name)
138            .ok_or_else(|| AuthyError::SecretNotFound(name.to_string()))?;
139
140        entry.value = new_value.to_string();
141        entry.metadata.bump_version();
142        let version = entry.metadata.version;
143
144        v.touch();
145        vault::save_vault(&v, &self.key)?;
146
147        self.audit(
148            "rotate",
149            Some(name),
150            "success",
151            Some(&format!("v{version}")),
152        );
153        Ok(version)
154    }
155
156    /// List secret names, optionally filtered by a policy scope.
157    pub fn list(&self, scope: Option<&str>) -> Result<Vec<String>> {
158        let v = vault::load_vault(&self.key)?;
159
160        let names: Vec<String> = if let Some(scope_name) = scope {
161            let policy = v
162                .policies
163                .get(scope_name)
164                .ok_or_else(|| AuthyError::PolicyNotFound(scope_name.to_string()))?;
165            let all_names: Vec<&str> = v.secrets.keys().map(String::as_str).collect();
166            policy
167                .filter_secrets(&all_names)?
168                .into_iter()
169                .map(String::from)
170                .collect()
171        } else {
172            v.secrets.keys().cloned().collect()
173        };
174
175        self.audit("list", None, "success", None);
176        Ok(names)
177    }
178
179    /// Initialize a new vault. The vault must not already exist.
180    pub fn init_vault(&self) -> Result<()> {
181        if vault::is_initialized() {
182            return Err(AuthyError::VaultAlreadyExists(
183                vault::vault_path().display().to_string(),
184            ));
185        }
186        let v = Vault::new();
187        vault::save_vault(&v, &self.key)?;
188
189        // Write default config
190        let config = crate::config::Config::default();
191        config.save(&vault::config_path())?;
192
193        self.audit("init", None, "success", None);
194        Ok(())
195    }
196
197    /// Read all audit entries from the log.
198    pub fn audit_entries(&self) -> Result<Vec<audit::AuditEntry>> {
199        audit::read_entries(&vault::audit_path())
200    }
201
202    /// Verify the integrity of the audit chain.
203    /// Returns `(entry_count, valid)`.
204    pub fn verify_audit_chain(&self) -> Result<(usize, bool)> {
205        audit::verify_chain(&vault::audit_path(), &self.audit_key)
206    }
207
208    // ── internal helpers ─────────────────────────────────────────
209
210    fn audit(&self, operation: &str, secret: Option<&str>, outcome: &str, detail: Option<&str>) {
211        let _ = audit::log_event(
212            &vault::audit_path(),
213            operation,
214            secret,
215            &self.actor,
216            outcome,
217            detail,
218            &self.audit_key,
219        );
220    }
221}