-
-
Notifications
You must be signed in to change notification settings - Fork 0
Config and Secrets
src/config.rs:
pub fn config_dir() -> PathBuf // ~/.config/admintoolkit/ (platform equivalent via `dirs::config_dir`)
pub fn config_file(name: &str) -> PathBuf // config_dir().join(name)
pub fn expand_path(path: &str) -> String // expands leading "~" and $VAR / ${VAR}Every tool keeps its own JSON file inside that one shared directory —
ssh_users.json, clickhouse.json, godaddy.json, mysql.json, etc. —
so tools stay independent on disk while sharing one place a user (or an
uninstall) can find everything. A new tool should follow the same pattern:
one serde_json-backed struct, config_file("<tool>.json"), plain
std::fs::read_to_string / std::fs::write wrapped in load()/save()
functions in that tool's own config.rs — see mysql_mgr::config::{load, save} for the minimal version.
expand_path matters for anything that's a filesystem path a user typed
into a form (SSH key paths, cert files) — always run user-entered paths
through it before using them, so ~/.ssh/id_ed25519 and $HOME/... both
work as typed.
There's no cross-platform equivalent of an OS keychain without pulling in a
native dependency (libsecret/dbus on Linux), so instead: AES-256-GCM under a
random 256-bit key generated on first use and stored at
~/.config/admintoolkit/.godaddy.key with 0600 permissions (owner-only).
The filename predates this covering more than GoDaddy — kept as-is so
already-encrypted secrets from earlier installs keep decrypting; don't
"fix" the filename.
secret::encrypt(plaintext: &str) -> io::Result<String> // random nonce + AES-GCM, base64-encoded
secret::decrypt(encoded: &str) -> io::Result<String>
secret::encrypt_optional(plaintext: &str) -> io::Result<String> // "" in -> "" out, no ciphertext for nothing
secret::decrypt_optional(encoded: &str) -> io::Result<String>Use the _optional variants for any field that's legitimately allowed to
be empty (e.g. an SSH profile that uses key-only auth, no password) — this
avoids emitting a zero-length ciphertext where "no value" would do.
This protects secrets from casual disk browsing/backups that don't also grab the key file — it is not a defense against another process running as the same OS user, since the key lives right next to the data. Keep that threat model in mind when documenting a new tool's secret handling; don't oversell it.
Every tool that stores a password/token follows the same shape (see
mysql_mgr::config::{Connection, ConnectionWithSecrets} for the concrete
example):
- The struct that gets serialized to JSON stores
..._encrypted: Stringfields — ciphertext only, never plaintext. - A separate
..WithSecretsstruct exists only in memory, built on demand by decrypting, and is what actually gets handed to the connect/exec code. It's never serialized. - On update, an empty password field in the form means "keep the existing
one" (don't overwrite with blank) —
upsert_connectioninmysql_mgr::configonly re-encrypts and replaces..._encryptedwhen the incoming value is non-empty.
Follow this shape for any new tool that stores a credential — don't put a
plaintext password field on the struct that gets serde_json::to_string'd,
even temporarily.