Skip to main content

authy/auth/
mod.rs

1pub mod context;
2
3use std::env;
4use std::fs;
5use std::io::IsTerminal;
6
7use crate::error::{AuthyError, Result};
8use crate::session;
9use crate::vault::{self, VaultKey};
10use context::AuthContext;
11
12const AUTHY_PASSPHRASE_ENV: &str = "AUTHY_PASSPHRASE";
13const AUTHY_KEYFILE_ENV: &str = "AUTHY_KEYFILE";
14const AUTHY_TOKEN_ENV: &str = "AUTHY_TOKEN";
15const AUTHY_NON_INTERACTIVE_ENV: &str = "AUTHY_NON_INTERACTIVE";
16
17/// Check if we are in non-interactive mode.
18/// Returns true if stdin is not a TTY or AUTHY_NON_INTERACTIVE=1 is set.
19pub fn is_non_interactive() -> bool {
20    if env::var(AUTHY_NON_INTERACTIVE_ENV)
21        .map(|v| v == "1")
22        .unwrap_or(false)
23    {
24        return true;
25    }
26    !std::io::stdin().is_terminal()
27}
28
29/// Resolve authentication. Tries in order:
30/// 1. AUTHY_TOKEN env var (session token, requires AUTHY_KEYFILE for vault decryption)
31/// 2. AUTHY_KEYFILE env var (master keyfile)
32/// 3. AUTHY_PASSPHRASE env var (master passphrase)
33/// 4. Interactive passphrase prompt (only if TTY is available)
34pub fn resolve_auth(require_write: bool) -> Result<(VaultKey, AuthContext)> {
35    // Check for token-based auth first
36    if let Ok(token) = env::var(AUTHY_TOKEN_ENV) {
37        if require_write {
38            return Err(AuthyError::TokenReadOnly);
39        }
40
41        // Token auth requires a keyfile to decrypt the vault
42        let keyfile_path = env::var(AUTHY_KEYFILE_ENV)
43            .map_err(|_| AuthyError::AuthFailed(
44                "AUTHY_TOKEN requires AUTHY_KEYFILE to be set".into(),
45            ))?;
46
47        let (identity, pubkey) = read_keyfile(&keyfile_path)?;
48        let vault_key = VaultKey::Keyfile {
49            identity: identity.clone(),
50            pubkey,
51        };
52
53        // Load the vault to validate the token
54        let vault = vault::load_vault(&vault_key)?;
55        let hmac_key = vault::crypto::derive_key(identity.as_bytes(), b"session-hmac", 32);
56        let session_record = session::validate_token(&token, &vault.sessions, &hmac_key)?;
57
58        let auth_ctx = AuthContext::from_token(
59            session_record.id.clone(),
60            session_record.scope.clone(),
61            session_record.run_only,
62        );
63
64        return Ok((vault_key, auth_ctx));
65    }
66
67    // Check for keyfile auth
68    if let Ok(keyfile_path) = env::var(AUTHY_KEYFILE_ENV) {
69        let (identity, pubkey) = read_keyfile(&keyfile_path)?;
70        let vault_key = VaultKey::Keyfile { identity, pubkey };
71        let auth_ctx = AuthContext::master_keyfile();
72        return Ok((vault_key, auth_ctx));
73    }
74
75    // Check for passphrase env var
76    if let Ok(passphrase) = env::var(AUTHY_PASSPHRASE_ENV) {
77        let vault_key = VaultKey::Passphrase(passphrase);
78        let auth_ctx = AuthContext::master_passphrase();
79        return Ok((vault_key, auth_ctx));
80    }
81
82    // Non-interactive mode: fail immediately without prompting
83    if is_non_interactive() {
84        return Err(AuthyError::AuthFailed(
85            "No credentials provided. Set AUTHY_KEYFILE, AUTHY_PASSPHRASE, or AUTHY_TOKEN environment variable.".into(),
86        ));
87    }
88
89    interactive_passphrase_prompt()
90}
91
92#[cfg(feature = "cli")]
93fn interactive_passphrase_prompt() -> Result<(VaultKey, AuthContext)> {
94    let passphrase = dialoguer::Password::new()
95        .with_prompt("Enter vault passphrase")
96        .interact()
97        .map_err(|e| AuthyError::AuthFailed(format!("Failed to read passphrase: {e}")))?;
98    Ok((VaultKey::Passphrase(passphrase), AuthContext::master_passphrase()))
99}
100
101#[cfg(not(feature = "cli"))]
102fn interactive_passphrase_prompt() -> Result<(VaultKey, AuthContext)> {
103    Err(AuthyError::AuthFailed(
104        "No credentials provided. Set AUTHY_KEYFILE or AUTHY_PASSPHRASE.".into(),
105    ))
106}
107
108/// Resolve auth specifically for init (no vault exists yet, just get the key).
109pub fn resolve_auth_for_init(
110    passphrase: Option<String>,
111    generate_keyfile: Option<String>,
112) -> Result<VaultKey> {
113    if let Some(keyfile_path) = generate_keyfile {
114        let (secret_key, public_key) = vault::crypto::generate_keypair();
115        // Write the keyfile (secret key)
116        fs::write(&keyfile_path, &secret_key)?;
117        // Restrict permissions
118        #[cfg(unix)]
119        {
120            use std::os::unix::fs::PermissionsExt;
121            fs::set_permissions(&keyfile_path, fs::Permissions::from_mode(0o600))?;
122        }
123        // Write the public key alongside
124        let pubkey_path = format!("{}.pub", keyfile_path);
125        fs::write(&pubkey_path, &public_key)?;
126
127        eprintln!("Generated keyfile: {}", keyfile_path);
128        eprintln!("Public key: {}", pubkey_path);
129
130        return Ok(VaultKey::Keyfile {
131            identity: secret_key,
132            pubkey: public_key,
133        });
134    }
135
136    if let Some(pass) = passphrase {
137        return Ok(VaultKey::Passphrase(pass));
138    }
139
140    // Check env
141    if let Ok(pass) = env::var(AUTHY_PASSPHRASE_ENV) {
142        return Ok(VaultKey::Passphrase(pass));
143    }
144
145    interactive_init_passphrase()
146}
147
148#[cfg(feature = "cli")]
149fn interactive_init_passphrase() -> Result<VaultKey> {
150    let pass = dialoguer::Password::new()
151        .with_prompt("Create vault passphrase")
152        .with_confirmation("Confirm passphrase", "Passphrases don't match")
153        .interact()
154        .map_err(|e| AuthyError::AuthFailed(format!("Failed to read passphrase: {e}")))?;
155    Ok(VaultKey::Passphrase(pass))
156}
157
158#[cfg(not(feature = "cli"))]
159fn interactive_init_passphrase() -> Result<VaultKey> {
160    Err(AuthyError::AuthFailed(
161        "No credentials provided. Pass a passphrase or set AUTHY_PASSPHRASE.".into(),
162    ))
163}
164
165/// Read an age keyfile from disk. Returns (identity_string, public_key_string).
166pub fn read_keyfile(path: &str) -> Result<(String, String)> {
167    let content = fs::read_to_string(path)
168        .map_err(|e| AuthyError::InvalidKeyfile(format!("Cannot read {}: {}", path, e)))?;
169
170    let identity: age::x25519::Identity = content
171        .trim()
172        .parse()
173        .map_err(|e: &str| AuthyError::InvalidKeyfile(e.to_string()))?;
174
175    let pubkey = identity.to_public().to_string();
176    Ok((content.trim().to_string(), pubkey))
177}