Skip to main content

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:
  1. Master key — Stored in the OS keychain (macOS Keychain, Linux secret-service, Windows Credential Vault)
  2. Encryption engine — AES-256-GCM with random IVs and authentication tags
  3. Storage backends — JSON files (legacy) and PostgreSQL tables (current)

Encryption Format

All encrypted values use a self-describing format:
enc:v1:<iv-hex>:<authTag-hex>:<ciphertext-hex>
ComponentDescription
enc:v1Version prefix for format identification
iv-hex12-byte initialization vector (hex-encoded)
authTag-hexGCM authentication tag (hex-encoded)
ciphertext-hexEncrypted 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:
PlatformBackend
macOSKeychain Services (security CLI)
LinuxSecret Service API (GNOME Keyring, KDE Wallet)
WindowsCredential 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:
1

Generate random bytes

Generate 32 random bytes using crypto.randomBytes(32)
2

Store in keychain

Store in the OS keychain under the argentos service
3

Cache in memory

Cache in memory for the duration of the process

Key Retrieval

On subsequent operations:
  1. Check in-memory cache
  2. If not cached, read from OS keychain
  3. 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:
FieldDescription
variableEnvironment variable name (e.g., OPENAI_API_KEY)
encrypted_valueAES-256-GCM encrypted value
serviceService identifier (e.g., openai, anthropic)
categoryGrouping category (e.g., llm, channel, tool)
allowed_rolesRoles that can access this key
allowed_agentsAgent IDs that can access this key
allowed_teamsTeam IDs that can access this key
deny_allIf 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:
  1. Reads the JSON files
  2. Decrypts any already-encrypted values (using the same master key)
  3. Re-encrypts for PG storage
  4. Upserts into the PostgreSQL tables
  5. Reports migration counts
Existing PG entries are updated (not duplicated) via ON CONFLICT ... DO UPDATE.

Key Rotation

To rotate the master key:
1

Export all secrets

They are decrypted in memory during export
2

Delete the old master key from the OS keychain

3

Re-encrypt all secrets

A new master key is auto-generated
4

Update all storage backends

Currently, key rotation is a manual process. The system does not support automatic key rotation.

Security Properties

PropertyStatus
Encryption at restAES-256-GCM for all persistent secrets
Key managementOS keychain (not in files or env vars)
AuthenticationGCM auth tag prevents tampering
IV reuse preventionRandom 12-byte IV per encryption
Backward compatibilityPlaintext values accepted, encrypted on next write
Access controlPer-key role/agent/team restrictions (PG only)
Audit trailcreated_at / updated_at timestamps

Key Files

FileDescription
src/infra/secret-crypto.tsAES-256-GCM encrypt/decrypt (63 LOC)
src/infra/pg-secret-store.tsPostgreSQL secret store (365 LOC)
src/infra/keychain.tsOS keychain master key management
src/infra/service-keys.tsJSON-file service keys (legacy)