Skip to content

hkd987/remix-credentials

Repository files navigation

remix-credentials

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."

Quick Start

Add the dependency to your Cargo.toml:

[dependencies]
remix-credentials = { git = "https://github.com/hkd987/remix-credentials" }

Encrypt and decrypt a credential

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 zeroized

Encrypt a set of credentials to a file

use 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");

Load credentials from environment variables

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();

Derive a key from a passphrase

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());

How It Fits Into remix-agent

remix-agent-runtime (open source)

The runtime uses remix-credentials to load and decrypt credentials locally before an agent run:

  1. At startup, credentials are loaded from environment variables (REMIX_CRED_*) or decrypted from an encrypted file using a user-provided passphrase.
  2. During a run, decrypted credentials are held in memory inside types that zeroize on drop. A DecryptedCredentialGuard provides read-only access with an optional TTL.
  3. 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.
  4. After use, all credential memory is zeroized. No plaintext survives in process memory after the credential types go out of scope.

remix-agent-platform (proprietary)

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.

Security Model

What this crate protects against

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

What this crate does NOT protect against

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.

API Overview

Core Types

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

Functions

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

Credential File Format

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.

Design Principles

  • 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.

Dependencies

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

Building and Testing

cargo build              # Build the crate
cargo test               # Run all 141 tests
cargo clippy -- -D warnings  # Lint
cargo doc --no-deps      # Generate documentation

License

MIT

About

No description, website, or topics provided.

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages