Overview
ArgentOS stores API keys, service credentials, and authentication tokens using AES-256-GCM encryption. Secrets are encrypted before they hit disk or database — the system never stores plaintext credentials in any persistent location.
The encryption architecture has three layers:
- Master key — Stored in the OS keychain (macOS Keychain, Linux secret-service, Windows Credential Vault)
- Encryption engine — AES-256-GCM with random IVs and authentication tags
- Storage backends — JSON files (legacy) and PostgreSQL tables (current)
All encrypted values use a self-describing format:
enc:v1:<iv-hex>:<authTag-hex>:<ciphertext-hex>
| Component | Description |
|---|
enc:v1 | Version prefix for format identification |
iv-hex | 12-byte initialization vector (hex-encoded) |
authTag-hex | GCM authentication tag (hex-encoded) |
ciphertext-hex | Encrypted payload (hex-encoded) |
Example:
enc:v1:a1b2c3d4e5f6a7b8c9d0e1f2:1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d:8f7e6d5c4b3a...
The version prefix (enc:v1) enables future format upgrades without breaking existing encrypted values.
AES-256-GCM
The system uses AES-256-GCM (Galois/Counter Mode), which provides both confidentiality and authenticity:
- 256-bit key — Derived from the master key stored in the OS keychain
- 12-byte random IV — Generated fresh for every encryption operation using
crypto.randomBytes()
- Authentication tag — GCM produces a tag that detects any tampering with the ciphertext
- No padding needed — GCM is a stream cipher mode, so no PKCS7 padding is required
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
const ALGORITHM = "aes-256-gcm";
const IV_BYTES = 12;
function encryptSecret(plaintext: string): string {
const key = getMasterKey();
const iv = randomBytes(IV_BYTES);
const cipher = createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(plaintext, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag().toString("hex");
return `enc:v1:${iv.toString("hex")}:${authTag}:${encrypted}`;
}
OS Keychain Integration
The 256-bit master key is stored in the operating system’s native credential store:
| Platform | Backend |
|---|
| macOS | Keychain Services (security CLI) |
| Linux | Secret Service API (GNOME Keyring, KDE Wallet) |
| Windows | Credential Vault |
The master key is generated once on first use and stored under the service name argentos. It never appears in configuration files, environment variables, or logs.
Key Generation
On first encryption operation, if no master key exists in the keychain:
Generate random bytes
Generate 32 random bytes using crypto.randomBytes(32)
Store in keychain
Store in the OS keychain under the argentos service
Cache in memory
Cache in memory for the duration of the process
Key Retrieval
On subsequent operations:
- Check in-memory cache
- If not cached, read from OS keychain
- Cache for process lifetime
Backward Compatibility
The decrypt function accepts both encrypted and plaintext values:
function decryptSecret(value: string): string {
if (!value.startsWith("enc:v1:")) {
return value; // Plaintext — backward compatible
}
// ... decrypt ...
}
This enables gradual migration. Existing plaintext values in config files and databases continue to work. They are re-encrypted with AES-256-GCM when next written.
PostgreSQL Secret Store
When running with PostgreSQL backend (dual-write or postgres mode), secrets are stored in two PG tables:
Service Keys Table
Stores API keys and service credentials:
CREATE TABLE service_keys (
id TEXT PRIMARY KEY,
variable TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
encrypted_value TEXT NOT NULL,
service TEXT,
category TEXT,
enabled BOOLEAN NOT NULL DEFAULT true,
source TEXT,
allowed_roles TEXT[] DEFAULT '{}',
allowed_agents TEXT[] DEFAULT '{}',
allowed_teams TEXT[] DEFAULT '{}',
deny_all BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
Key fields:
| Field | Description |
|---|
variable | Environment variable name (e.g., OPENAI_API_KEY) |
encrypted_value | AES-256-GCM encrypted value |
service | Service identifier (e.g., openai, anthropic) |
category | Grouping category (e.g., llm, channel, tool) |
allowed_roles | Roles that can access this key |
allowed_agents | Agent IDs that can access this key |
allowed_teams | Team IDs that can access this key |
deny_all | If true, key is inaccessible to all agents |
Auth Credentials Table
Stores authentication profiles (OAuth tokens, API keys with metadata):
CREATE TABLE auth_credentials (
id TEXT PRIMARY KEY,
profile_id TEXT UNIQUE NOT NULL,
provider TEXT NOT NULL,
credential_type TEXT NOT NULL, -- 'api_key', 'oauth', 'token'
encrypted_payload TEXT NOT NULL,
email TEXT,
enabled BOOLEAN NOT NULL DEFAULT true,
last_used_at TIMESTAMPTZ,
cooldown_until TIMESTAMPTZ,
error_count INTEGER DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
The encrypted_payload field contains a JSON object encrypted with AES-256-GCM. This payload includes the actual credential data (API keys, OAuth tokens, refresh tokens) along with provider-specific metadata.
Access Control
Service keys support fine-grained access control:
allowed_roles — Only agents with matching roles can use the key
allowed_agents — Only specific agent IDs can use the key
allowed_teams — Only agents in specific teams can use the key
deny_all — Emergency kill switch to revoke all access
When all access control arrays are empty, the key is accessible to all agents (open access).
CRUD Operations
Service Keys
// List all keys (values decrypted in memory)
const keys = await pgListServiceKeys(sql);
// Resolve a key by variable name
const value = await pgResolveServiceKey(sql, "OPENAI_API_KEY");
// Upsert a key (value encrypted before storage)
await pgUpsertServiceKey(sql, {
variable: "OPENAI_API_KEY",
value: "sk-...",
name: "OpenAI Production",
service: "openai",
category: "llm"
});
// Delete a key
await pgDeleteServiceKey(sql, "OPENAI_API_KEY");
Auth Credentials
// List all credentials (payloads decrypted in memory)
const creds = await pgListAuthCredentials(sql);
// Get a credential by profile ID
const cred = await pgGetAuthCredential(sql, "anthropic:main");
// Upsert a credential
await pgUpsertAuthCredential(sql, {
profileId: "anthropic:main",
provider: "anthropic",
credentialType: "api_key",
payload: { key: "sk-ant-..." },
email: "[email protected]"
});
Migration from JSON
For installations migrating from JSON-file storage to PostgreSQL, Phoenix provides a one-shot migration function:
const result = await migrateSecretsToPg(sql, {
serviceKeysPath: "~/.argentos/service-keys.json",
authProfilesPath: "~/.argentos/agents/main/agent/auth-profiles.json"
});
// result: {
// serviceKeys: { migrated: 15, skipped: 0 },
// authCredentials: { migrated: 3, skipped: 0 }
// }
The migration:
- Reads the JSON files
- Decrypts any already-encrypted values (using the same master key)
- Re-encrypts for PG storage
- Upserts into the PostgreSQL tables
- Reports migration counts
Existing PG entries are updated (not duplicated) via ON CONFLICT ... DO UPDATE.
Key Rotation
To rotate the master key:
Export all secrets
They are decrypted in memory during export
Delete the old master key from the OS keychain
Re-encrypt all secrets
A new master key is auto-generated
Update all storage backends
Currently, key rotation is a manual process. The system does not support automatic key rotation.
Security Properties
| Property | Status |
|---|
| Encryption at rest | AES-256-GCM for all persistent secrets |
| Key management | OS keychain (not in files or env vars) |
| Authentication | GCM auth tag prevents tampering |
| IV reuse prevention | Random 12-byte IV per encryption |
| Backward compatibility | Plaintext values accepted, encrypted on next write |
| Access control | Per-key role/agent/team restrictions (PG only) |
| Audit trail | created_at / updated_at timestamps |
Key Files
| File | Description |
|---|
src/infra/secret-crypto.ts | AES-256-GCM encrypt/decrypt (63 LOC) |
src/infra/pg-secret-store.ts | PostgreSQL secret store (365 LOC) |
src/infra/keychain.ts | OS keychain master key management |
src/infra/service-keys.ts | JSON-file service keys (legacy) |