Skip to main content
The AgentOS vault provides secure storage for API keys, tokens, and other sensitive credentials using AES-256-GCM encryption with PBKDF2 key derivation.

Overview

The vault encrypts all secrets at rest and automatically locks after a configurable period of inactivity. Decrypted secrets are auto-zeroed from memory after use.
The vault is locked by default. You must unlock it with a master password before storing or retrieving secrets.

Architecture

┌──────────────────────────────────────────────────────┐
│                   Master Password                     │
└────────────────────┬─────────────────────────────────┘

          ┌──────────────────────┐
          │  PBKDF2-SHA256       │
          │  600,000 iterations  │
          └──────────┬───────────┘

          ┌──────────────────────┐
          │  AES-256-GCM Key     │
          └──────────┬───────────┘

    ┌────────────────────────────────────┐
    │  Encrypted Credentials (at rest)   │
    │  ┌──────────────────────────────┐  │
    │  │ IV (12 bytes)                │  │
    │  │ Ciphertext                   │  │
    │  │ Auth Tag (16 bytes)          │  │
    │  └──────────────────────────────┘  │
    └────────────────────────────────────┘

Vault State

src/vault.ts
interface VaultState {
  unlocked: boolean;                           // Is vault currently unlocked?
  cryptoKey: CryptoKey | null;                 // Derived AES key (in-memory only)
  salt: string | null;                         // PBKDF2 salt (base64)
  autoLockTimer: ReturnType<typeof setTimeout> | null;
  autoLockMs: number;                          // Auto-lock after N ms (default: 30 min)
}

interface VaultEntry {
  key: string;           // Credential key (e.g., "ANTHROPIC_API_KEY")
  iv: string;            // Initialization vector (base64)
  ciphertext: string;    // Encrypted value (base64)
  tag: string;           // GCM authentication tag (base64)
  createdAt: number;     // Timestamp
  updatedAt: number;     // Timestamp
}

Initializing the Vault

import { trigger } from "iii-sdk";

// Initialize vault with master password
await trigger("vault::init", {
  password: "your-strong-master-password",
  autoLockMinutes: 30,  // Lock after 30 minutes of inactivity
});

// Returns: { unlocked: true, autoLockMinutes: 30 }

Key Derivation (PBKDF2)

src/vault.ts
async function deriveKey(
  password: string,
  salt: Uint8Array,
): Promise<CryptoKey> {
  const encoder = new TextEncoder();
  const keyMaterial = await crypto.subtle.importKey(
    "raw",
    encoder.encode(password),
    "PBKDF2",
    false,
    ["deriveKey"],
  );

  return crypto.subtle.deriveKey(
    {
      name: "PBKDF2",
      salt: salt,
      iterations: 600_000,  // OWASP recommendation (2023)
      hash: "SHA-256",
    },
    keyMaterial,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"],
  );
}
Use a strong, unique master password. This password cannot be recovered if lost.

Storing Secrets

import { trigger } from "iii-sdk";

// Store API key
await trigger("vault::set", {
  key: "ANTHROPIC_API_KEY",
  value: "sk-ant-xxxxxxxxxxxxxxxxxxxxx",
});

// Store OAuth token
await trigger("vault::set", {
  key: "GITHUB_TOKEN",
  value: "ghp_xxxxxxxxxxxxxxxxxxxxx",
});

// Returns: { stored: true, key: "GITHUB_TOKEN", updatedAt: 1709985234567 }

Encryption (AES-256-GCM)

src/vault.ts
async function encrypt(
  key: CryptoKey,
  plaintext: string,
): Promise<{ iv: string; ciphertext: string; tag: string }> {
  const encoder = new TextEncoder();
  const iv = crypto.getRandomValues(new Uint8Array(12));  // 96-bit nonce

  const encrypted = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv, tagLength: 128 },  // 128-bit auth tag
    key,
    encoder.encode(plaintext),
  );

  const buf = new Uint8Array(encrypted);
  const ciphertext = buf.slice(0, buf.length - 16);
  const tag = buf.slice(buf.length - 16);

  return {
    iv: Buffer.from(iv).toString("base64"),
    ciphertext: Buffer.from(ciphertext).toString("base64"),
    tag: Buffer.from(tag).toString("base64"),
  };
}

Retrieving Secrets

import { trigger } from "iii-sdk";

const entry = await trigger("vault::get", {
  key: "ANTHROPIC_API_KEY",
});

console.log(entry.value);       // "sk-ant-xxxxx"
console.log(entry.createdAt);   // 1709985234567
console.log(entry.updatedAt);   // 1709985234567

Decryption (AES-256-GCM)

src/vault.ts
async function decrypt(
  key: CryptoKey,
  entry: { iv: string; ciphertext: string; tag: string },
): Promise<string> {
  const iv = Buffer.from(entry.iv, "base64");
  const ciphertext = Buffer.from(entry.ciphertext, "base64");
  const tag = Buffer.from(entry.tag, "base64");

  // Combine ciphertext + tag for GCM
  const combined = new Uint8Array(ciphertext.length + tag.length);
  combined.set(ciphertext);
  combined.set(tag, ciphertext.length);

  const decrypted = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv, tagLength: 128 },
    key,
    combined,
  );

  return new TextDecoder().decode(decrypted);
}

Auto-Zeroing Secrets

Decrypted secrets are automatically zeroed from memory after 30 seconds:
src/vault.ts
import { wrapZeroized, autoDispose } from "./security-zeroize.js";

const plaintext = await decrypt(vault.cryptoKey!, entry);

// Wrap in auto-zeroing buffer
const zb = wrapZeroized(plaintext);
autoDispose(zb, 30_000);  // Zero after 30 seconds

return {
  key,
  value: zb.toString(),  // Return before auto-zero
  createdAt: entry.createdAt,
  updatedAt: entry.updatedAt,
};
Secrets are cleared from memory 30 seconds after retrieval. Re-fetch if needed.

Auto-Lock

The vault automatically locks after autoLockMinutes of inactivity:
src/vault.ts
function resetAutoLock() {
  if (vault.autoLockTimer) clearTimeout(vault.autoLockTimer);
  
  vault.autoLockTimer = setTimeout(() => {
    vault.unlocked = false;
    vault.cryptoKey = null;  // Clear key from memory
    
    triggerVoid("security::audit", {
      type: "vault_auto_locked",
      detail: { after: vault.autoLockMs },
    });
  }, vault.autoLockMs);
}

// Reset timer on every vault operation
registerFunction({ id: "vault::get" }, async (req) => {
  assertUnlocked();
  resetAutoLock();  // Reset timer
  // ... decrypt and return secret
});

Manual Lock

// Lock vault immediately
vault.unlocked = false;
vault.cryptoKey = null;

triggerVoid("security::audit", {
  type: "vault_locked",
  detail: { manual: true },
});

Listing Secrets

import { trigger } from "iii-sdk";

const result = await trigger("vault::list", {});

console.log(result.keys);
// [
//   { key: "ANTHROPIC_API_KEY", createdAt: 1709985234567, updatedAt: 1709985234567 },
//   { key: "GITHUB_TOKEN", createdAt: 1709985235123, updatedAt: 1709985235123 },
// ]

console.log(result.count);  // 2
vault::list returns keys only, never the decrypted values.

Deleting Secrets

await trigger("vault::delete", {
  key: "OLD_API_KEY",
});

// Returns: { deleted: true, key: "OLD_API_KEY" }

Rotating the Master Password

await trigger("vault::rotate", {
  currentPassword: "old-password",
  newPassword: "new-strong-password",
});

// Returns: { rotated: 15, success: true }

How Rotation Works

src/vault.ts
registerFunction({ id: "vault::rotate" }, async (req) => {
  const { currentPassword, newPassword } = req.body;
  assertUnlocked();

  // Derive old key
  const meta = await trigger("state::get", { scope: "vault", key: "__meta" });
  const oldSalt = Buffer.from(meta.salt, "base64");
  const oldKey = await deriveKey(currentPassword, oldSalt);

  // Fetch all encrypted entries
  const entries = await trigger("state::list", { scope: "vault" });
  const credentials = entries.filter((e) => e.key !== "__meta");

  // Backup before rotation
  for (const entry of credentials) {
    await trigger("state::set", {
      scope: "vault_backup",
      key: entry.key,
      value: entry.value,
    });
  }

  // Generate new salt and key
  const newSalt = crypto.getRandomValues(new Uint8Array(32));
  const newKey = await deriveKey(newPassword, newSalt);

  // Re-encrypt all credentials
  const updates = [];
  for (const entry of credentials) {
    const plaintext = await decrypt(oldKey, entry.value);
    const encrypted = await encrypt(newKey, plaintext);
    updates.push({
      key: entry.key,
      value: { ...entry.value, ...encrypted, updatedAt: Date.now() },
    });
  }

  // Atomically update all entries
  try {
    for (const { key, value } of updates) {
      await trigger("state::set", { scope: "vault", key, value });
    }
    await trigger("state::set", {
      scope: "vault",
      key: "__meta",
      value: {
        salt: Buffer.from(newSalt).toString("base64"),
        createdAt: meta.createdAt,
        rotatedAt: Date.now(),
      },
    });
  } catch (err) {
    // Rollback on failure
    // (restore from vault_backup scope)
    throw new Error(`Vault rotation failed, rolled back: ${err.message}`);
  }

  vault.cryptoKey = newKey;
  vault.salt = Buffer.from(newSalt).toString("base64");
  resetAutoLock();

  return { rotated: updates.length, success: true };
});

Backup and Restore

Backup

await trigger("vault::backup", {});

// Returns: { backedUp: 15, success: true }
Backup creates a snapshot in the vault_backup scope (still encrypted).

Restore

await trigger("vault::restore", {
  password: "master-password",  // Password for the backup
});

// Returns: { restored: 15, success: true }
Backups are encrypted with the same master password. If you lose the password, backups are unrecoverable.

Audit Events

All vault operations generate audit events:
// Vault unlocked
{
  "type": "vault_unlocked",
  "detail": { "autoLockMs": 1800000 },
  "timestamp": 1709985234567
}

// Secret accessed
{
  "type": "vault_get",
  "detail": { "key": "ANTHROPIC_API_KEY" },
  "timestamp": 1709985235123
}

// Secret stored
{
  "type": "vault_set",
  "detail": { "key": "GITHUB_TOKEN" },
  "timestamp": 1709985235890
}

// Vault rotated
{
  "type": "vault_rotated",
  "detail": { "credentialsRotated": 15 },
  "timestamp": 1709985236567
}

// Auto-locked
{
  "type": "vault_auto_locked",
  "detail": { "after": 1800000 },
  "timestamp": 1709987034567
}

CLI Commands

# Initialize vault
agentos vault init
# Prompts: Enter master password:

# Store a secret
agentos vault set ANTHROPIC_API_KEY sk-ant-xxxxx

# Retrieve a secret
agentos vault get ANTHROPIC_API_KEY

# List all stored keys (not values)
agentos vault list

# Delete a secret
agentos vault remove OLD_API_KEY

# Rotate master password
agentos vault rotate
# Prompts: Current password:
#          New password:

# Backup vault
agentos vault backup

# Restore from backup
agentos vault restore

Best Practices

1

Strong Master Password

Use a password manager to generate a strong, unique master password (20+ characters).
2

Rotate Regularly

Rotate the master password every 90 days using vault::rotate.
3

Backup Before Rotation

Always run vault::backup before vault::rotate in case of failure.
4

Monitor Access

Review vault_get events in audit log to detect unauthorized access.
5

Lock When Done

Manually lock the vault when finished with sensitive operations.

Security Considerations

What the Vault Protects

Secrets at rest — AES-256-GCM encryption
Secrets in transit — Never logged or transmitted unencrypted
Memory exposure — Auto-zeroing after 30 seconds
Brute force — 600K PBKDF2 iterations slow down attacks

What You Must Protect

⚠️ Master password — Store securely, never commit to git
⚠️ Unlocked vault — Anyone with access to unlocked vault can read secrets
⚠️ Backup files — Encrypted backups still require the master password
⚠️ Audit logs — May contain key names (not values) — protect access

Advanced: Custom Auto-Lock

// Lock after 5 minutes of inactivity
await trigger("vault::init", {
  password: "master-password",
  autoLockMinutes: 5,
});

// Disable auto-lock (not recommended)
await trigger("vault::init", {
  password: "master-password",
  autoLockMinutes: 0,  // Never auto-lock
});

Next Steps

Audit Chain

Track vault access events in the audit log

RBAC

Control which agents can access vault secrets

Build docs developers (and LLMs) love