# Credential Encryption Implementation in Craft Agents: A Deep Dive into SecureStorageBackend

> Learn about Craft Agents credential encryption using AES-256-GCM and PBKDF2. Discover how your secrets are securely stored in lukilabs/craft-agents-oss.

- Repository: [Craft Docs/craft-agents-oss](https://github.com/lukilabs/craft-agents-oss)
- Tags: deep-dive
- Published: 2026-04-18

---

**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`](https://github.com/lukilabs/craft-agents-oss/blob/main/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 `IOPlatformUUID` from I/O Registry
- **Windows**: Retrieves `MachineGuid` from registry
- **Linux**: Uses `/var/lib/dbus/machine-id` or `/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:

1. **Hash the machine ID** using SHA-256
2. **Append version tag** (`craft-agent-v2`) to prevent downgrade attacks
3. **Run PBKDF2-SHA256** with 100,000 iterations using the per-file salt
4. **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`](https://github.com/lukilabs/craft-agents-oss/blob/main/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:
1. The manager updates the in-memory JSON store
2. Calls `SecureStorageBackend.write()` with the serialized data
3. The backend derives the encryption key from the stable machine ID
4. Generates a new IV and encrypts using AES-256-GCM
5. Atomically writes to `~/.craft-agent/credentials.enc` with `0o600` permissions

### Retrieving Credentials

When `get()` is called:
1. If the cache is cold, the backend reads the encrypted file
2. Extracts the salt from the header and derives the current key
3. Attempts AES-256-GCM decryption
4. If decryption fails, attempts legacy key derivation (see below)
5. 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:

```typescript
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.enc` with strict `0o600` permissions.
- **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**: `CredentialManager` in [`packages/shared/src/credentials/manager.ts`](https://github.com/lukilabs/craft-agents-oss/blob/main/packages/shared/src/credentials/manager.ts) provides 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`](https://github.com/lukilabs/craft-agents-oss/blob/main/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.