How Are Credentials Encrypted and Stored in Craft Agents?

Craft Agents stores sensitive credentials in an AES-256-GCM encrypted file at ~/.craft-agent/credentials.enc, deriving a 256-bit encryption key from a stable machine identifier using PBKDF2-SHA-256 with 100,000 iterations.

The craft-ai-agents/craft-agents-oss repository implements a secure credential management system that protects API keys, OAuth tokens, and service account credentials from unauthorized access. Instead of storing secrets in plain-text configuration files, Craft Agents encrypts all sensitive data using authenticated encryption standards and manages access through a high-level TypeScript API.

Encryption Architecture and Key Derivation

AES-256-GCM Implementation

The encryption layer uses AES-256-GCM (Galois/Counter Mode) to provide both confidentiality and authentication. The encrypted file follows a specific binary format implemented in packages/shared/src/credentials/backends/secure-storage.ts:

  • Header: A fixed 16-byte header containing the random salt and metadata
  • Payload: A concatenated structure of IV (12 bytes) | Authentication Tag (16 bytes) | Ciphertext

The credential file resides in the user’s home directory at ~/.craft-agent/credentials.enc. For workspace-specific credentials, the system stores files in sub-directories under this path.

Key Derivation with PBKDF2

The system derives the encryption key using PBKDF2-SHA-256 with the following parameters:

  • Base secret: A stable machine identifier retrieved from node-machine-id
  • Salt: Random salt stored in the file header
  • Iterations: 100,000 (PBKDF2_ITERATIONS)
  • Key length: 32 bytes (KEY_SIZE)

This derivation ensures that the encryption key is cryptographically separate from the raw machine identifier and resistant to brute-force attacks.

Decryption Flow and Legacy Support

When opening the store, the system reads the header to extract the salt, re-derives the key, and slices the IV, authentication tag, and ciphertext for the AES-GCM cipher. If decryption fails—typically after a hardware migration—the implementation falls back to a legacy key derivation path that used the raw machine ID without PBKDF2. This backward compatibility handles upgrades from older versions while surfacing a decryption_failed health issue to prompt credential re-entry.

Credential Data Model and Scoping

StoredCredential Interface

Credentials are represented by the generic StoredCredential interface defined in packages/shared/src/credentials/types.ts. This flexible structure accommodates:

  • OAuth refresh tokens and bearer tokens
  • API keys (Anthropic, OpenAI, etc.)
  • AWS IAM credentials
  • GCP service-account JSON

The type system classifies credentials by CredentialType, supporting scopes including global, llm-connection, workspace, source, and messaging.

Key Naming Convention

Each credential entry is stored under a unique key built from its type and scope using the delimiter ::. The conversion functions credentialIdToAccount and accountToCredentialId in packages/shared/src/credentials/types.ts handle the mapping between CredentialId objects and string keys.

Common key patterns include:

  • anthropic_api_key::global – A global API key accessible across all workspaces
  • source_oauth::workspace123::source456 – An OAuth token scoped to a specific source within a workspace
  • messaging_bearer::workspace123::telegram – A Telegram bot token scoped to a workspace

The CredentialManager API

The high-level CredentialManager class in packages/shared/src/credentials/manager.ts abstracts all interactions with the encrypted store. The manager automatically lazy-loads the encryption key on first use and caches it in memory for the process lifetime.

Retrieving credentials:

import { CredentialManager } from '@craft-agents/shared/credentials';
import { CredentialId } from '@craft-agents/shared/credentials/types';

const manager = new CredentialManager();

const sourceId: CredentialId = {
  type: 'source_oauth',
  workspaceId: 'ws-001',
  sourceId: 'source-xyz',
};

const cred = await manager.get(sourceId);
if (cred) {
  console.log('OAuth access token:', cred.value);
}

Storing credentials:

const anthKeyId: CredentialId = { type: 'anthropic_api_key' };
await manager.set(anthKeyId, { value: 'sk-abc123' });

Health monitoring:

const health = await manager.checkHealth();
if (!health.healthy) {
  health.issues.forEach(i => console.warn(i.message));
}

Storage Lifecycle and Migration

Initialization and Updates

The credential store follows a strict lifecycle:

  1. First run: If credentials.enc does not exist, the manager creates an empty JSON object, encrypts it with a freshly derived key, and writes the file
  2. Subsequent runs: The file is read, decrypted, and parsed into a plain-object map of credential entries
  3. Updates: Any call to set() re-encrypts the entire map and performs an atomic file overwrite to prevent corruption during writes

Machine Migration Handling

When the machine ID changes (e.g., after a hardware swap), decryption of the old file fails. The manager attempts the legacy key derivation, detects the mismatch, and surfaces a decryption_failed health issue. This signals the user to re-enter credentials rather than failing silently with a decryption error.

Working with Credentials in Code

The following example demonstrates the complete workflow for initializing the manager, storing a global API key, and retrieving a workspace-scoped token:

import { CredentialManager } from '@craft-agents/shared/credentials';
import { CredentialId } from '@craft-agents/shared/credentials/types';

// Initialize the manager (automatically loads encrypted file)
const manager = new CredentialManager();

// Store a global Anthropic API key
await manager.set(
  { type: 'anthropic_api_key' },
  { value: 'sk-ant-api03-...' }
);

// Retrieve a source-scoped OAuth token
const sourceCred: CredentialId = {
  type: 'source_oauth',
  workspaceId: 'workspace-123',
  sourceId: 'github-source-1'
};

const token = await manager.get(sourceCred);
console.log('Retrieved credential:', token?.value);

Summary

  • Encryption: Credentials are protected using AES-256-GCM authenticated encryption in packages/shared/src/credentials/backends/secure-storage.ts
  • Key derivation: A 256-bit key is derived via PBKDF2-SHA-256 (100,000 iterations) from a stable machine identifier and random salt
  • Storage location: Encrypted files live at ~/.craft-agent/credentials.enc with support for workspace-specific sub-directories
  • Data model: The StoredCredential interface supports multiple credential types scoped via :: delimited keys defined in packages/shared/src/credentials/types.ts
  • API: The CredentialManager class in packages/shared/src/credentials/manager.ts provides get(), set(), and checkHealth() methods with automatic key caching
  • Atomicity: All writes re-encrypt the entire credential map and atomically overwrite the file to prevent data corruption

Frequently Asked Questions

What encryption algorithm does Craft Agents use for credential storage?

Craft Agents uses AES-256-GCM (Galois/Counter Mode) authenticated encryption. The implementation in packages/shared/src/credentials/backends/secure-storage.ts combines a 256-bit key derived via PBKDF2-SHA-256 with a random 12-byte IV and 16-byte authentication tag to ensure both confidentiality and integrity of stored credentials.

What happens to my credentials if I migrate Craft Agents to a new machine?

When you move to a new machine, the stable machine identifier changes, causing decryption to fail. The system attempts a legacy key derivation for backward compatibility, but if that fails, the CredentialManager surfaces a decryption_failed health issue. You must re-enter your credentials, as the encrypted file is bound to the original hardware identifier.

How does Craft Agents prevent credential corruption during writes?

The CredentialManager ensures atomic updates by re-encrypting the entire credential map into memory and then performing an atomic file overwrite. This prevents partial writes or corruption if the process terminates during the save operation, ensuring the credentials.enc file always contains a valid, complete encrypted state.

What types of credentials can be stored in the encrypted file?

The StoredCredential interface supports various credential types including OAuth refresh tokens, bearer tokens, API keys (such as Anthropic or OpenAI), AWS IAM credentials, and GCP service-account JSON. Credentials are classified by CredentialType into scopes such as global, workspace-specific, source-specific, or messaging channels.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →