The trust foundation for the remix-agent ecosystem — credential encryption, decryption, and zeroization.
remix-credentials is the only code that ever touches a user's raw credentials (site passwords, API keys, tokens). Both the open-source remix-agent-runtime and the closed-source remix-agent-platform depend on this single crate for all credential operations.
remix-credentials (this crate, MIT, open source)
▲ ▲
│ │
│ depends on │ depends on
│ │
remix-agent-runtime remix-agent-platform
(MIT, open source) (proprietary, closed source)
This library is intentionally small, focused, and isolated so that security researchers can audit the entire codebase in an afternoon. The trust story is simple: "Here's one repo. It handles all credential crypto. Read every line."
Add the dependency to your Cargo.toml:
[dependencies]
remix-credentials = { git = "https://github.com/hkd987/remix-credentials" }use remix_credentials::{Credential, CredentialType, encrypt, decrypt};
// Create a credential
let cred = Credential::new(
"github_login".to_string(),
CredentialType::UsernamePassword,
vec![
("username".to_string(), "octocat".to_string()),
("password".to_string(), "s3cret".to_string()),
],
None,
Default::default(),
).unwrap();
// Encrypt with a 256-bit key
let key = [0x42u8; 32];
let encrypted = encrypt(&cred, &key).unwrap();
// Decrypt — returns a guard that zeroizes on drop
let guard = decrypt(&encrypted, &key).unwrap();
let decrypted = guard.access().unwrap();
assert_eq!(decrypted.name(), "github_login");
assert_eq!(decrypted.field("username").unwrap().expose(), "octocat");
// guard drops here → credential memory is zeroizeduse std::path::Path;
use remix_credentials::{
Credential, CredentialType, CredentialSet,
save_to_file, load_from_file,
};
let mut set = CredentialSet::new();
set.add(Credential::new(
"github".to_string(),
CredentialType::UsernamePassword,
vec![
("username".to_string(), "octocat".to_string()),
("password".to_string(), "s3cret".to_string()),
],
Some("*.github.com".to_string()),
Default::default(),
).unwrap());
// Encrypt and save (derives key from passphrase via Argon2id)
save_to_file(&set, "my-strong-passphrase", Path::new("creds.json")).unwrap();
// Later: load and decrypt
let loaded = load_from_file("my-strong-passphrase", Path::new("creds.json")).unwrap();
assert_eq!(loaded.get("github").unwrap().field("username").unwrap().expose(), "octocat");use remix_credentials::{load_from_env, discover_from_env};
// Explicit loading — reads GITHUB_USERNAME and GITHUB_PASSWORD, then clears them
let cred = load_from_env("github", &["username", "password"]).unwrap();
// Auto-discovery — finds all REMIX_CRED_* env vars, groups by name
// e.g. REMIX_CRED_GITHUB_USERNAME=octocat, REMIX_CRED_GITHUB_PASSWORD=s3cret
let set = discover_from_env().unwrap();use remix_credentials::{derive_key, derive_key_with_salt, encrypt, decrypt};
// Derive a fresh key (generates random salt)
let derived = derive_key("my-passphrase").unwrap();
// Encrypt with the derived key
let key = derived.key();
// ... encrypt(credential, key) ...
// Later: re-derive with the same salt for decryption
let salt = derived.salt();
let rederived = derive_key_with_salt("my-passphrase", salt).unwrap();
assert_eq!(derived.key(), rederived.key());The runtime uses remix-credentials to load and decrypt credentials locally before an agent run:
- At startup, credentials are loaded from environment variables (
REMIX_CRED_*) or decrypted from an encrypted file using a user-provided passphrase. - During a run, decrypted credentials are held in memory inside types that zeroize on drop. A
DecryptedCredentialGuardprovides read-only access with an optional TTL. - When the agent needs to log in to a website, the runtime exposes the plaintext credential to the LLM so it can type it into the form. This is the fundamental tradeoff — the LLM sees plaintext during active use.
- After use, all credential memory is zeroized. No plaintext survives in process memory after the credential types go out of scope.
The managed platform uses the same crate inside its vault service to encrypt credentials at rest in its database and decrypt them inside agent containers at runtime. Same crypto code path regardless of where the agent runs — no proprietary credential handling anywhere in the stack.
| Threat | Mitigation |
|---|---|
| Credentials leaked via logs | Debug and Display redact all sensitive fields |
| Credentials persisted in plaintext | AES-256-GCM encrypted file format |
| Credentials lingering in memory | Zeroize + ZeroizeOnDrop on all sensitive types |
| Brute-force attacks on credential files | Argon2id key derivation (64 MB memory, 3 iterations) |
| Tampered encrypted data | AES-GCM authentication tag detects modification |
| Accidental cloning of secrets | No Clone on any decrypted credential type |
| Env var exposure | Variables cleared immediately after reading |
| Threat | Why |
|---|---|
| Memory forensics on a compromised host | Root access can read process memory; zeroization reduces the window but cannot eliminate the risk |
| LLM sees plaintext credentials | By design — the LLM must see the password to type it into a form |
| Key management and storage | The caller is responsible for protecting encryption keys |
| Side-channel attacks | Not a hardened cryptographic enclave |
| Credential theft via LLM provider | If the provider logs prompts, credentials could be exposed |
This transparency is intentional. Users should know exactly what they're getting and what they're not.
| Type | Description |
|---|---|
Credential |
A single credential with validated fields, redacted Debug/Display |
CredentialSet |
An ordered collection of credentials with name-based lookup |
CredentialType |
Enum: UsernamePassword, ApiKey, Token, Cookie, Custom |
SecretString |
A string that zeroizes on drop and redacts in Debug/Display |
SecretMap |
A key-value map of secrets with zeroization support |
EncryptedCredential |
Encrypted blob with ciphertext, nonce, and version tag |
DecryptedCredentialGuard |
Time-limited, read-only access to a decrypted credential |
DerivedKey |
A 256-bit key + salt derived from a passphrase via Argon2id |
KdfParams |
Argon2id tuning parameters (memory, iterations, parallelism) |
CredentialFile |
On-disk JSON format for encrypted credential storage |
CredentialError |
Error enum covering all failure modes |
| Function | Description |
|---|---|
encrypt(credential, key) |
Encrypt a single credential with AES-256-GCM |
encrypt_set(set, key) |
Encrypt an entire credential set as one blob |
decrypt(encrypted, key) |
Decrypt into a DecryptedCredentialGuard |
decrypt_set(encrypted, key) |
Decrypt into a CredentialSet |
derive_key(passphrase) |
Derive a 256-bit key with random salt |
derive_key_with_salt(passphrase, salt) |
Re-derive a key with a known salt |
derive_key_with_params(passphrase, salt, params) |
Derive with custom Argon2id parameters |
load_from_env(name, fields) |
Load a credential from env vars (clears them after) |
discover_from_env() |
Auto-discover all REMIX_CRED_* env vars |
save_to_file(set, passphrase, path) |
Encrypt and write a credential set to a file |
load_from_file(passphrase, path) |
Load and decrypt a credential set from a file |
Encrypted credential files are JSON with base64-encoded fields:
{
"version": 1,
"algorithm": "aes-256-gcm",
"kdf": "argon2id",
"kdf_params": {
"memory_kb": 65536,
"iterations": 3,
"parallelism": 4
},
"salt": "<base64>",
"nonce": "<base64>",
"ciphertext": "<base64>"
}The file is versioned so the format can evolve without breaking existing encrypted data.
- Do one thing. Encrypt, decrypt, and zeroize credentials. No storage backends, no networking, no key management infrastructure.
- No secrets in, no secrets out. The crate never writes credentials to disk unencrypted, never logs them, never sends them anywhere.
- Standard crypto only. AES-256-GCM for encryption, Argon2id for key derivation. No custom cryptography.
- Zeroize everything. All sensitive memory is overwritten on drop. No credential material survives after use.
- Minimal dependencies. Only well-audited crates from the RustCrypto project.
#![forbid(unsafe_code)]enforced.
| Crate | Purpose |
|---|---|
aes-gcm |
AES-256-GCM authenticated encryption |
argon2 |
Argon2id key derivation |
zeroize |
Secure memory wiping |
rand |
Cryptographically secure random number generation |
serde + serde_json |
Serialization for credential structures and file format |
base64 |
Encoding for file format and transport |
cargo build # Build the crate
cargo test # Run all 141 tests
cargo clippy -- -D warnings # Lint
cargo doc --no-deps # Generate documentationMIT