Credential Encryption Implementation in Craft Agents: A Deep Dive into SecureStorageBackend
Craft Agents protects user secrets using AES-256-GCM encryption with machine-bound keys derived via PBKDF2, storing everything in a single file at ~/.craft-agent/credentials.enc.
The lukilabs/craft-agents-oss repository implements a zero-trust credential encryption system that secures API keys, OAuth tokens, and cloud credentials without exposing plaintext to disk. This article examines the implementation details of credential encryption in the SecureStorageBackend class, the cryptographic workflows, and the migration strategies that maintain backward compatibility.
How Craft Agents Implements Credential Encryption
The encryption layer resides in packages/shared/src/credentials/backends/secure-storage.ts, specifically within the SecureStorageBackend class. This backend provides the sole interface for persisting credentials to disk, ensuring that no plaintext secrets leak into logs or file systems.
The system uses a stable machine identifier to bind encryption keys to specific hardware, preventing simple copy attacks where an attacker moves the encrypted file to another machine. If hardware IDs are unavailable, the system falls back to a composite of username:homedir.
SecureStorageBackend Architecture and File Format
Encrypted File Location and Header Structure
The backend writes to a single file at ~/.craft-agent/credentials.enc. The file format consists of a 64-byte header followed by the encrypted payload:
- Magic string (8 bytes): Identifies the file format version
- Reserved flags (8 bytes): Future compatibility fields
- PBKDF2 salt (16 bytes): Random per-file salt for key derivation
- Reserved padding (32 bytes): Alignment and future extensions
The payload uses AES-256-GCM with a 12-byte initialization vector (IV) and 16-byte authentication tag. The file permissions are strictly set to 0o600 (owner read/write only).
Machine-Specific Key Derivation
Before encryption occurs, SecureStorageBackend derives a stable machine ID using hardware-specific identifiers:
- macOS: Reads
IOPlatformUUIDfrom I/O Registry - Windows: Retrieves
MachineGuidfrom registry - Linux: Uses
/var/lib/dbus/machine-idor/etc/machine-id
If hardware access fails, the system falls back to username:homedir to ensure continuity.
PBKDF2 Key Stretching and AES-256-GCM Encryption
The SecureStorageBackend implements the following key derivation workflow in the deriveKey method:
- Hash the machine ID using SHA-256
- Append version tag (
craft-agent-v2) to prevent downgrade attacks - Run PBKDF2-SHA256 with 100,000 iterations using the per-file salt
- Output a 32-byte AES-256 key
For encryption, the system:
- Generates a cryptographically random 12-byte IV
- Encrypts the JSON-encoded credential store using AES-256-GCM
- Appends the 16-byte authentication tag
- Writes the header + IV + ciphertext + tag to disk
Credential Encryption Workflow
The CredentialManager class (packages/shared/src/credentials/manager.ts) orchestrates all credential operations. It initializes SecureStorageBackend during construction and maintains an in-memory cache of the decrypted store to minimize disk I/O and decryption overhead.
Storing Credentials
When set() is called:
- The manager updates the in-memory JSON store
- Calls
SecureStorageBackend.write()with the serialized data - The backend derives the encryption key from the stable machine ID
- Generates a new IV and encrypts using AES-256-GCM
- Atomically writes to
~/.craft-agent/credentials.encwith0o600permissions
Retrieving Credentials
When get() is called:
- If the cache is cold, the backend reads the encrypted file
- Extracts the salt from the header and derives the current key
- Attempts AES-256-GCM decryption
- If decryption fails, attempts legacy key derivation (see below)
- Parses the JSON and returns the requested credential
Legacy Key Migration and Backward Compatibility
The SecureStorageBackend includes migration logic to support credential files created by earlier versions of Craft Agents. The legacy system derived keys from hostname + username + homedir rather than hardware UUIDs.
During decryption, if the current key fails, the backend automatically calls getLegacyEncryptionKey() to derive the v1 key and attempts decryption again. If successful, the system immediately re-encrypts the store using the current machine-bound key, transparently upgrading the file format.
If both current and legacy keys fail, the system treats the file as corrupted and deletes it to prevent lockout scenarios, requiring the user to re-authenticate.
Practical Usage Examples
The following TypeScript examples demonstrate how to interact with the encrypted credential store through the CredentialManager facade:
import { getCredentialManager } from '@/shared/src/credentials/manager';
// Initialise the manager (lazy by default, but can be forced eager)
await getCredentialManager().initialize();
// Store an Anthropic API key (encrypts automatically)
await getCredentialManager().set(
{ type: 'anthropic_api_key' },
{ value: 'sk-xxxx-your-key-here' }
);
// Retrieve a stored OAuth token for a workspace MCP
const cred = await getCredentialManager().get({
type: 'workspace_oauth',
workspaceId: 'ws-1234',
});
if (cred) {
console.log('Access token:', cred.value);
}
// Delete a source-specific bearer token
await getCredentialManager().delete({
type: 'source_bearer',
workspaceId: 'ws-1234',
sourceId: 'github',
});
// List all stored credential IDs (decrypts once, caches in memory)
const ids = await getCredentialManager().list();
console.log('All credential IDs:', ids);
All operations eventually invoke SecureStorageBackend, which handles the AES-256-GCM encryption, PBKDF2 key derivation, and atomic file writes to ~/.craft-agent/credentials.enc.
Summary
- Storage Location: All encrypted credentials reside in
~/.craft-agent/credentials.encwith strict0o600permissions. - Encryption Standard: AES-256-GCM with 12-byte IV and 16-byte authentication tag, implemented in
SecureStorageBackend. - Key Derivation: PBKDF2-SHA256 with 100,000 iterations using a machine-specific stable ID (hardware UUIDs or fallback to
username:homedir). - File Format: 64-byte header (magic, flags, salt) followed by the encrypted JSON payload.
- Migration Support: Automatic legacy key detection and transparent re-encryption for v1 credential files derived from hostname rather than hardware ID.
- API Surface:
CredentialManagerinpackages/shared/src/credentials/manager.tsprovides the high-level interface for all credential operations.
Frequently Asked Questions
How does Craft Agents prevent credential theft if the encrypted file is copied to another machine?
The SecureStorageBackend derives encryption keys from stable machine identifiers such as hardware UUIDs (macOS IOPlatformUUID, Windows MachineGuid, Linux /var/lib/dbus/machine-id). If the credentials.enc file is moved to a different computer, the target machine will generate a different key during PBKDF2 derivation, causing AES-256-GCM decryption to fail. Even if the attacker knows the user's password, they cannot decrypt the file without the original hardware-derived salt and key components.
What happens if Craft Agents cannot access hardware UUIDs on my system?
If the operating system blocks hardware access or the relevant files are missing, SecureStorageBackend falls back to a composite stable ID generated from username:homedir. While this is less secure than hardware binding (since copying both the home directory and credential file would allow decryption), it still ensures that casual file copying to a different user account or machine will fail. The fallback logic is implemented in the stable ID function within secure-storage.ts.
How does the credential encryption handle upgrades from older Craft Agents versions?
The backend maintains backward compatibility through getLegacyEncryptionKey(), which replicates the v1 key derivation logic using hostname + username + homedir. When SecureStorageBackend loads a credential file, it first attempts decryption with the current hardware-based key. If that fails, it automatically tries the legacy key. Upon successful decryption with a legacy key, the system immediately re-encrypts the store using the modern key and writes it back to disk, completing a transparent migration without user intervention.
Is the credential store encrypted in memory, or only on disk?
The CredentialManager maintains an in-memory cache of the decrypted JSON credential store for the lifetime of the process to minimize repetitive PBKDF2 operations and file I/O. While the data is encrypted at rest using AES-256-GCM with the secure key derivation described above, once loaded into the CredentialManager instance, the credentials exist as plaintext objects in Node.js memory. This design prioritizes performance and usability while maintaining strong encryption boundaries at the storage layer.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →