const SERVICE_NAME: &str = "pigeon";
pub fn set_secret(alias: &str, secret: &str) -> Result<(), String> {
let entry = keyring::Entry::new(SERVICE_NAME, alias)
.map_err(|err| format!("failed to open keychain entry for '{alias}': {err}"))?;
entry
.set_password(secret)
.map_err(|err| format!("failed to store secret for '{alias}': {err}"))
}
fn env_var_name(alias: &str) -> String {
let mut name = String::from("PIGEON_SECRET_");
for ch in alias.chars() {
let upper = ch.to_ascii_uppercase();
name.push(if upper.is_ascii_alphanumeric() || upper == '_' {
upper
} else {
'_'
});
}
name
}
pub fn get_secret(alias: &str) -> Result<String, String> {
if let Ok(secret) = std::env::var(env_var_name(alias)) {
return Ok(secret);
}
let entry = keyring::Entry::new(SERVICE_NAME, alias)
.map_err(|err| format!("failed to open keychain entry for '{alias}': {err}"))?;
entry
.get_password()
.map_err(|err| format!("failed to read secret for '{alias}': {err}"))
}
pub fn delete_secret(alias: &str) -> Result<(), String> {
let entry = keyring::Entry::new(SERVICE_NAME, alias)
.map_err(|err| format!("failed to open keychain entry for '{alias}': {err}"))?;
match entry.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(err) => Err(format!("failed to delete secret for '{alias}': {err}")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn env_var_name_uppercases_and_sanitizes_the_alias() {
assert_eq!(env_var_name("gmail-work"), "PIGEON_SECRET_GMAIL_WORK");
assert_eq!(env_var_name("My.Bucket"), "PIGEON_SECRET_MY_BUCKET");
}
#[test]
fn get_secret_prefers_the_env_var_over_the_os_keyring() {
let alias = "adr-0087-test-alias";
let var = env_var_name(alias);
unsafe {
std::env::set_var(&var, "injected-secret");
}
let result = get_secret(alias);
unsafe {
std::env::remove_var(&var);
}
assert_eq!(result.unwrap(), "injected-secret");
}
}