Skip to content

Credential Storage

Wayland Core stores credentials through a CredentialsStore trait backed by one of three implementations. The backend is selected in config.toml; the default is plaintext with 0o600 permissions enforced on write. For most interactive installs the keyring backend is the better choice. The encrypted-file backend is for headless or server deployments where the OS keychain is unavailable.

All three backends use flat string keys with dotted prefixes for namespacing, for example providers.anthropic.api_key or bedrock.secret_access_key.


Set the backend in ~/.wayland-core/config.toml (or your project-local .wayland-core.toml):

[storage.credentials]
backend = "keyring" # or "plaintext" or "encrypted_file"
service_name = "wayland-core" # optional; scopes keyring entries (default: "wayland-core")

For the encrypted-file backend, two file paths must also be specified:

[storage.credentials]
backend = { encrypted_file = { cipher_path = "~/.wayland/credentials.enc", key_params_path = "~/.wayland/credentials.kdf.json" } }

Set WAYLAND_VAULT=plaintext before startup to skip any auto-migration prompt and keep the plaintext backend regardless of what the config file says.


PlaintextCredentialsStore stores a [secrets] table in a TOML file. On every write it enforces 0o600 permissions on Unix and attempts a deny-all ACL on Windows via atomic_write - a sibling tempfile is written and then renamed so a crash or interruption cannot leave a half-written file. Reads warn via tracing if the file is world-readable but still load, to avoid stranding users on a freshly created file that the kernel briefly held at the umask default.

Default path: wherever wayland-core --config-path reports on your OS (usually ~/.config/wayland-core/config.toml on Linux, ~/Library/Application Support/wayland-core/config.toml on macOS).

This backend is appropriate for development machines where the OS keychain is inconvenient. It offers no encryption at rest; anyone with read access to the file can read the secrets.


KeyringCredentialsStore uses the keyring crate to store secrets in the platform’s native credential store:

  • macOS: Keychain
  • Windows: Credential Manager
  • Linux: Secret Service (e.g. GNOME Keyring, KWallet)

Each key is stored as a (service, user) pair where service is the configured service_name (default: "wayland-core") and user is the key string. Lookup is O(1).

To switch to the keyring backend:

[storage.credentials]
backend = "keyring"

Then set your API key once:

Terminal window
wayland-core setup # interactive onboarding stores keys in the selected backend

Or set it directly via the environment - the engine’s credential lookup order is environment variable first, then the credential store, then the config file. Keys read from the environment are never written to the store.


EncryptedFileCredentialsStore is for server or CI deployments where no OS keychain is available. It uses:

  • KDF: Argon2id (v0x13), parameters m=65536 KiB (64 MiB), t=3, p=1, output 32 bytes
  • AEAD: XChaCha20-Poly1305, 24-byte nonce, 16-byte Poly1305 MAC tag
  • Blob layout: nonce(24) || ciphertext || tag(16) in the cipher file

The KDF parameters are confirmed in crates/wcore-config/src/credentials.rs:

const DEFAULT_M_COST_KIB: u32 = 64 * 1024; // 64 MiB
const DEFAULT_T_COST: u32 = 3;
const DEFAULT_P_COST: u32 = 1;
pub const NONCE_LEN: usize = 24;
pub const TAG_LEN: usize = 16;
pub const KEY_LEN: usize = 32;

The backend writes two files:

FileContentsSecret?
cipher_path (e.g. ~/.wayland/credentials.enc)Raw bytes: nonce(24) || ciphertext || tag(16)Yes - protect with 0o600
key_params_path (e.g. ~/.wayland/credentials.kdf.json)JSON: { salt_b64, m_cost, t_cost, p_cost, version }No - contains only public KDF tuning params

The salt is 16 random bytes generated from rand::thread_rng() (seeded from the OS) on vault creation and stored in key_params_path. Storing the KDF parameters separately means you can re-derive the key from your passphrase at any time without knowing the original tuning values. The AEAD nonce is re-randomized on every put() write.

Both files are written with atomic_write (tempfile + rename) and 0o600 permissions are enforced immediately after.

The engine resolves the vault passphrase in this order:

  1. WAYLAND_VAULT_PASSPHRASE_FD: a Unix file descriptor number (e.g. 3). The engine validates the fd is open and readable before consuming it. This path does not appear in /proc/<pid>/environ.
  2. WAYLAND_VAULT_PASSPHRASE: env var (legacy). Emits a tracing::warn! about the /proc/<pid>/environ visibility risk on Linux.
  3. Interactive prompt: rpassword::prompt_password("vault passphrase: ") on a TTY.

The passphrase is held in a zeroize::Zeroizing<String> in memory. Argon2id derivation runs once per process; subsequent get()/put() calls reuse the cached derived key under a parking_lot::Mutex. Cross-process write concurrency is not modeled - if you run multiple writers against the same vault files, serialize at the application layer.

On first access the engine:

  1. Reads the passphrase via the resolution order above.
  2. Loads key_params_path if it exists; otherwise uses KdfParams::default() (fresh random salt).
  3. If cipher_path already exists, decrypts it immediately to verify the passphrase is correct. A wrong passphrase fails here rather than silently rotating the vault key on the next write.

Pass the passphrase via a file descriptor to avoid environment variable exposure:

Terminal window
# Open a pipe, write the passphrase to fd 3, run wayland-core
exec 3< <(echo -n "my-vault-passphrase")
WAYLAND_VAULT_PASSPHRASE_FD=3 wayland-core "run my task"
exec 3<&-

For CI/CD, inject the passphrase via a secret manager into the fd at process start rather than an environment variable.


Keys follow a dotted convention. Some examples used by the engine:

KeyWhat it holds
providers.anthropic.api_keyAnthropic API key
providers.openai.api_keyOpenAI API key
bedrock.access_key_idAWS access key ID for Bedrock
bedrock.secret_access_keyAWS secret access key for Bedrock
providers.<name>.api_keyAny custom provider’s key

The wayland-core setup interactive onboarding stores keys under these namespaced paths. Channel adapter keys (Slack, Discord, etc.) use channels.<name>.<field> prefixes managed by their respective crates.


ScenarioRecommended backend
Developer workstation, interactive usekeyring
Developer workstation, no keychain daemonplaintext (ensure 0o600; consider full-disk encryption)
Server or CI, no GUI/keychainencrypted_file with passphrase via fd
Testing or short-lived containerplaintext or environment variables only