#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SecureStore {
Os,
File,
}
pub const OVERRIDE_ENV: &str = "VTI_SECURE_STORE";
pub fn override_from_env() -> Result<Option<SecureStore>, String> {
let raw = match std::env::var(OVERRIDE_ENV) {
Ok(v) => v,
Err(std::env::VarError::NotPresent) => return Ok(None),
Err(std::env::VarError::NotUnicode(_)) => {
return Err(format!("{OVERRIDE_ENV} is not valid UTF-8"));
}
};
parse_override(&raw)
}
fn parse_override(raw: &str) -> Result<Option<SecureStore>, String> {
match raw.trim().to_ascii_lowercase().as_str() {
"" => Ok(None),
"os" | "keyring" => Ok(Some(SecureStore::Os)),
"file" => Ok(Some(SecureStore::File)),
other => Err(format!(
"{OVERRIDE_ENV}={other:?} is not a store. Use `os` for the platform \
credential store, or `file` for a plaintext file at 0600."
)),
}
}
fn remedy() -> &'static str {
#[cfg(target_os = "macos")]
{
"Unlock the login keychain, or run this from a session that has one \
(an SSH session without `security unlock-keychain` does not)."
}
#[cfg(target_os = "linux")]
{
"Start a Secret Service provider — gnome-keyring-daemon, KWallet, or \
KeePassXC — and make sure DBus is reachable ($DBUS_SESSION_BUS_ADDRESS). \
Headless hosts usually have neither."
}
#[cfg(target_os = "windows")]
{
"Check that the Credential Manager service is running and that this \
account has a loaded user profile (a service account run with \
`LoadUserProfile=false` does not)."
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
"This platform has no supported credential store."
}
}
pub fn unavailable_message(tool: &str, err: &dyn std::fmt::Display) -> String {
format!(
"error: the OS credential store is unavailable, so {tool} cannot read or \
write your session.\n\
\n\
cause: {err}\n\
\n\
Your credentials are not lost — they are in a store this process cannot \
reach. {tool} is stopping rather than continuing as though you had never \
logged in, which is what it used to do.\n\
\n\
{}\n\
\n\
On a host that genuinely has no credential store, opt in to file storage \
deliberately:\n\
\n\
{OVERRIDE_ENV}=file {tool} ...\n\
\n\
That writes secrets as plaintext JSON at mode 0600. Only use it where the \
filesystem itself is the trust boundary — an encrypted volume, or a \
locked-down container.",
remedy()
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unknown_override_is_an_error_not_a_default() {
assert!(matches!(parse_override("os"), Ok(Some(SecureStore::Os))));
assert!(matches!(
parse_override("File"),
Ok(Some(SecureStore::File))
));
assert!(matches!(
parse_override(" file "),
Ok(Some(SecureStore::File))
));
assert!(matches!(parse_override(""), Ok(None)));
assert!(parse_override("plaintext").is_err());
assert!(parse_override("fil").is_err());
assert!(parse_override("true").is_err());
}
#[test]
fn the_message_names_the_consequence_and_the_opt_out() {
let msg = unavailable_message("pnm", &"no default store");
assert!(msg.contains("cannot read or write your session"));
assert!(msg.contains("no default store"));
assert!(msg.contains("VTI_SECURE_STORE=file pnm"));
assert!(msg.contains("0600"));
}
}