What Encryption Method Is Used for Credential Storage in Craft Agents? AES-256-GCM Explained

Craft Agents encrypts all user credentials using AES-256-GCM (Advanced Encryption Standard with 256-bit keys in Galois/Counter Mode), storing them in a machine-bound file at ~/.craft-agent/credentials.enc with PBKDF2 key derivation and authenticated encryption tags.

The craft-ai-agents/craft-agents-oss repository implements a secure storage backend that ensures OAuth tokens, API keys, and bearer tokens never persist in plaintext. According to the source code in packages/shared/src/credentials/backends/secure-storage.ts, the system combines hardware-specific identifiers with strong key derivation to protect credentials at rest.

AES-256-GCM Encryption Architecture

Craft Agents employs AES-256-GCM to provide both confidentiality and integrity verification. This authenticated encryption mode generates a 16-byte authentication tag during encryption that is verified during decryption, preventing tampering and ensuring data authenticity.

Encrypted File Format and Header Structure

The credential store file uses a structured binary format consisting of a 64-byte header followed by the encrypted payload. As documented in the source header, the layout includes:

  • A magic string ("CRAFT01\0") identifying the file format
  • Reserved flags for future extensions
  • A 32-byte PBKDF2 salt stored in plaintext (bytes 8-39)
  • Padding to align the header to 64 bytes

Following the header, the encrypted payload contains a random 12-byte initialization vector (IV), the AES-256-GCM ciphertext of the JSON-encoded credential store, and the 16-byte Galois Message Authentication Code (GMAC) tag.

Key Derivation via PBKDF2

The 256-bit encryption key is never stored on disk; instead, it is deterministically derived for each host machine. In SecureStorageBackend.getEncryptionKey() (lines 22-28), the implementation:

  1. Collects a stable machine identifier (hardware UUID on macOS, MachineGuid on Windows, or /var/lib/dbus/machine-id on Linux)
  2. Hashes this identifier with a version string
  3. Runs PBKDF2 with 100,000 iterations, SHA-256, and the 32-byte salt from the file header to produce the AES key

This machine-binding prevents credential files from being decrypted on unauthorized hardware, mitigating copy-paste attacks.

Implementation in SecureStorageBackend

The SecureStorageBackend class in packages/shared/src/credentials/backends/secure-storage.ts handles all cryptographic operations using Node.js's native crypto module.

Encryption and Decryption Flow

For encryption, the system generates a cryptographically random 12-byte IV and initializes a cipher via createCipheriv('aes-256-gcm', key, iv). After encrypting the JSON credential payload, it retrieves the authentication tag using cipher.getAuthTag() and appends both the ciphertext and tag to the file.

For decryption, SecureStorageBackend.tryDecrypt() (lines 64-71) creates a decipher instance via createDecipheriv('aes-256-gcm', key, iv), supplies the stored authentication tag via decipher.setAuthTag(), and recovers the plaintext. If the authentication tag verification fails—indicating tampering or key mismatch—the decryption throws an error before any data is returned.

Migration and Fallback Handling

When loading credentials, loadStoreSync() (lines 41-55) implements a secure migration path:

  1. Attempts decryption using the current machine-specific key derivation
  2. Falls back to a legacy hostname-based key (v1) if the first attempt fails, supporting seamless upgrades
  3. If both attempts fail, treats the file as corrupted and safely deletes it to prevent stale credential exposure

Practical Usage Example

The following TypeScript example demonstrates storing a Slack OAuth token using the secure storage backend:

import { SecureStorageBackend } from './packages/shared/src/credentials/backends/secure-storage';
import { CredentialId, StoredCredential } from './packages/shared/src/credentials/types';

async function saveExample() {
  const backend = new SecureStorageBackend();
  const id: CredentialId = { 
    type: 'source_oauth', 
    workspaceId: 'ws-123', 
    sourceId: 'slack' 
  };
  const cred: StoredCredential = {
    value: 'xoxb-1234567890-abcdef',    // Slack bot token
    clientId: 'my-client-id',
    clientSecret: 'my-client-secret',
    source: 'native',
  };
  await backend.set(id, cred);
}

When backend.set() executes, it automatically serializes the credential store to JSON, encrypts the payload using AES-256-GCM with a fresh IV, and atomically writes the result to ~/.craft-agent/credentials.enc.

Summary

  • AES-256-GCM provides authenticated encryption for all credentials, ensuring both confidentiality and integrity via a 16-byte authentication tag.
  • PBKDF2 key derivation uses 100,000 iterations and machine-specific hardware IDs to generate encryption keys that cannot be replicated on other devices.
  • Structured file format includes a 64-byte header with magic bytes, flags, and a 32-byte salt, followed by the encrypted JSON payload.
  • SecureStorageBackend handles encryption/decryption in packages/shared/src/credentials/backends/secure-storage.ts, with fallback logic for legacy key migration and corruption detection.

Frequently Asked Questions

Why does Craft Agents use AES-256-GCM instead of other encryption modes?

AES-256-GCM provides authenticated encryption with associated data (AEAD), which simultaneously guarantees data confidentiality and detects any unauthorized modifications. Unlike CBC or ECB modes, GCM generates an authentication tag during encryption that is verified during decryption, preventing padding oracle attacks and ensuring credential integrity.

How is the encryption key protected if it is not stored on disk?

The encryption key is deterministically derived from stable hardware identifiers unique to each machine (such as the hardware UUID on macOS or MachineGuid on Windows). Using PBKDF2 with 100,000 iterations and a random salt, the key is recomputed on-demand when the agent starts. This means an attacker cannot decrypt the credential file by copying it to another machine, as the derived key will differ.

What happens if the credentials.enc file becomes corrupted?

According to the implementation in loadStoreSync(), the system first attempts decryption with the current machine key, then falls back to a legacy hostname-based key for migration purposes. If both attempts fail, the backend safely deletes the corrupted file and starts with an empty credential store, preventing the application from crashing or exposing stale data.

Can I transfer my encrypted credentials to another machine?

No, the encryption is intentionally machine-bound. Because the AES-256-GCM key incorporates hardware-specific identifiers, decrypting the credentials.enc file on a different device will fail authentication. To migrate credentials, you must re-authenticate or export/import the credentials through the application's explicit migration tools rather than copying the raw encrypted file.

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 →