secure-env 0.2.2

Encrypted environment variables manager for your shell
Documentation
use age::secrecy::SecretString;
use anyhow::{Context, Result};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;

use crate::crypto;

pub type Sets = BTreeMap<String, BTreeMap<String, String>>;

/// A valid shell environment variable name: starts with a letter or `_`, then
/// only letters, digits, or `_`.
pub fn is_valid_env_name(name: &str) -> bool {
    let mut chars = name.chars();
    match chars.next() {
        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
        _ => return false,
    }
    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}

pub fn read_plain(path: &Path, passphrase: SecretString) -> Result<Option<Sets>> {
    if !path.exists() {
        return Ok(None);
    }
    let ciphertext =
        fs::read(path).with_context(|| format!("failed to read {}", path.display()))?;
    let plaintext = crypto::decrypt(&ciphertext, passphrase)?;
    let sets = from_toml(&plaintext)?;
    Ok(Some(sets))
}

pub fn from_toml(bytes: &[u8]) -> Result<Sets> {
    let text = std::str::from_utf8(bytes).context("decrypted data is not valid UTF-8")?;
    toml::from_str(text).context("decrypted data is not valid TOML")
}

pub fn to_toml(sets: &Sets) -> String {
    toml::to_string_pretty(sets).expect("sets serialize to TOML")
}

/// Atomically write `bytes` to `path` via a temp file and rename.
pub fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> {
    let dir = path.parent().unwrap_or_else(|| Path::new("."));
    fs::create_dir_all(dir).with_context(|| format!("failed to create {}", dir.display()))?;
    let file_name = path
        .file_name()
        .map(|n| n.to_string_lossy().into_owned())
        .unwrap_or_else(|| "secure-env.enc".to_string());
    let tmp = dir.join(format!(".{file_name}.tmp.{}", std::process::id()));
    fs::write(&tmp, bytes).with_context(|| format!("failed to write {}", tmp.display()))?;
    fs::rename(&tmp, path)
        .with_context(|| format!("failed to move {} into place", tmp.display()))?;
    Ok(())
}