# Tabby's Built-In Encrypted Secrets Container: Architecture and Implementation

> Explore Tabby's encrypted secrets container architecture. Learn how user credentials are AES-256-CBC encrypted and stored securely with PBKDF2-SHA-512 key derivation for robust protection.

- Repository: [Eugene/tabby](https://github.com/Eugeny/tabby)
- Tags: architecture
- Published: 2026-03-03

---

**Tabby's encrypted secrets container uses a two-layer architecture where user credentials are stored as an AES-256-CBC encrypted blob with PBKDF2-SHA-512 key derivation (100,000 iterations), wrapped in a `StoredVault` interface for disk persistence and decrypted into a `Vault` interface containing typed `VaultSecret` entries at runtime.**

Tabby (formerly Terminus) is an open-source terminal emulator developed in the Eugeny/tabby repository that provides secure, always-encrypted storage for sensitive credentials like SSH passwords and private-key passphrases. The vault system protects user data using industry-standard cryptography while maintaining a flexible internal structure that supports multiple secret types through extensible key-value pairs. Understanding this architecture reveals how the application balances zero-knowledge security with cross-platform usability in its Electron-based environment.

## Two-Layer Vault Architecture

The secrets container operates through distinct persisted and runtime representations that separate encryption logistics from secret management logic.

### The Persisted Layer: StoredVault

When Tabby saves your secrets to disk, it creates a `StoredVault` object defined in [`tabby-core/src/services/vault.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/vault.service.ts) (lines 20-25). This JSON structure contains the encrypted payload alongside the cryptographic parameters required for decryption:

```typescript
export interface StoredVault {
    version: number          // format version – currently 1
    contents: string         // base64-encoded ciphertext of the whole vault
    keySalt: string          // hex-encoded PBKDF2 salt
    iv: string               // hex-encoded AES-CBC initialization vector
}

```

The `contents` field holds the entire encrypted vault as a base64 string, while `keySalt` and `iv` store the random salts and initialization vectors required to reproduce the encryption key and decrypt the data. The `version` field ensures forward compatibility as the format evolves.

### The Runtime Layer: Vault

After the user provides their master passphrase, the `decryptVault` helper (lines 82-94) transforms the `StoredVault` into a plaintext `Vault` object (lines 40-43):

```typescript
export interface Vault {
    config: any                     // placeholder for future config options
    secrets: VaultSecret[]          // array of stored secrets
}

```

This in-memory representation holds a `config` object reserved for future vault-wide settings and a `secrets` array containing individual credential entries. The separation between `StoredVault` (encrypted) and `Vault` (plaintext) ensures that unencrypted data never touches the disk.

### Individual Secret Structure: VaultSecret

Each entry in the `Vault.secrets` array follows the `VaultSecret` interface (lines 27-31):

```typescript
export interface VaultSecret {
    type: string                    // e.g. 'file', 'sshPassword', …
    key: VaultSecretKey             // opaque identifier object
    value: string                   // secret value or base64-encoded binary
}

```

The `type` field categorizes the secret (such as `'sshPassword'` or `'file'`), while `key` accepts an extensible `VaultSecretKey` object that different services populate with identifying properties like `host`, `user`, or `id`. The `value` field stores the actual secret text or base64-encoded binary data for file contents.

## Cryptographic Implementation Details

The encryption layer uses standardized algorithms implemented in the private `encryptVault` and `decryptVault` functions (lines 65-94) to ensure data confidentiality.

### Key Derivation and Encryption

When saving the vault, Tabby executes the following cryptographic workflow:

1. **Random Generation**: Creates a random PBKDF2 salt (`PBKDF_SALT_LENGTH`) and AES initialization vector (`CRYPT_IV_LENGTH`) using cryptographically secure random bytes.
2. **Key Derivation**: Derives a 256-bit encryption key from the master passphrase using **PBKDF2-SHA-512** with **100,000 iterations** (`PBKDF_ITERATIONS`).
3. **Symmetric Encryption**: Encrypts the JSON-stringified `Vault` object using **AES-256-CBC** (`CRYPT_ALG`), producing the ciphertext stored in `StoredVault.contents`.
4. **Persistence**: Encodes the salt, IV, and ciphertext as hex and base64 strings respectively, storing them in the `StoredVault` structure written to Tabby's configuration file.

This process runs inside Angular's `NgZone` with `wrapPromise` to maintain UI responsiveness during the computationally expensive PBKDF2 operations.

## Versioning and Migration Strategy

The vault format includes explicit versioning to support future structural changes without breaking existing user data.

### Current Version Constraints

The only supported format version is **1**. When `decryptVault` processes a stored blob, it validates the `version` field and throws an error if encountering an unsupported version (lines 82-85). This prevents data corruption from incompatible format interpretations.

### Legacy Data Normalization

The `migrateVaultContent` helper (lines 48-52) handles backward compatibility by normalizing legacy vault structures. Currently, this ensures the `secrets` field exists as an array even when loading older configurations that might have used different collection types.

## Practical Usage Examples

The `VaultService` exposes CRUD operations that manipulate the vault structure while maintaining encryption boundaries.

### Storing a File as an Encrypted Secret

The `VaultFileProvider` class demonstrates adding binary content to the vault by encoding files as base64 strings:

```typescript
// Generate a random identifier for the file
const id = (await wrapPromise(this.zone,
    promisify(crypto.randomBytes)(32))).toString('hex');

// Add the file content as a vault secret
await this.vault.addSecret({
    type: VAULT_SECRET_TYPE_FILE,
    key: {
        id,
        description: `${description} (${transfer.getName()})`,
    },
    value: Buffer.from(await transfer.readAll()).toString('base64'),
});

// Returns a vault:// URL referencing the stored secret
return `${this.prefix}${id}`;

```

*Implementation reference:* [`tabby-core/src/services/vault.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/vault.service.ts) (lines 8-18, 30-38).

### Retrieving a Stored Secret

To access previously stored credentials, services query by type and key properties:

```typescript
const secret = await this.vault.getSecret(
    VAULT_SECRET_TYPE_FILE,
    { id: key.substring(this.prefix.length) }
);

if (!secret) throw new Error('Not found');

// Decode the base64 content back to binary
return Buffer.from(secret.value, 'base64');

```

*Implementation reference:* [`tabby-core/src/services/vault.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/vault.service.ts) (lines 24-28).

### Direct Vault Encryption

For external scripts or migrations, you can encrypt a custom vault structure directly:

```typescript
import { encryptVault } from './tabby-core/src/services/vault.service';

const plainVault = {
    config: {},
    secrets: [
        { 
            type: 'sshPassword',
            key: { host: 'example.com', user: 'alice' },
            value: 's3cr3t' 
        }
    ]
};

const stored: StoredVault = await encryptVault(plainVault, 'my-strong-passphrase');
// `stored` can now be serialized to Tabby's configuration file

```

*Core logic:* `encryptVault` at lines 65-79 in [`tabby-core/src/services/vault.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/vault.service.ts).

## Summary

- **Two-layer architecture**: Tabby uses `StoredVault` for encrypted disk persistence and `Vault` for in-memory plaintext operations, ensuring secrets are only decrypted when needed.
- **Strong cryptography**: The container employs AES-256-CBC encryption with keys derived via PBKDF2-SHA-512 using 100,000 iterations and random salts/IVs for each save operation.
- **Flexible secret typing**: The `VaultSecret` interface supports extensible key structures and type categorization, enabling storage of passwords, keys, and file contents within the same encrypted container.
- **Versioned format**: The vault structure includes a version field (currently 1) with migration helpers to support future format evolution without data loss.
- **Centralized service**: All encryption, decryption, and secret management operations are encapsulated in `VaultService` within [`tabby-core/src/services/vault.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/vault.service.ts).

## Frequently Asked Questions

### What encryption algorithm does Tabby use for its secrets container?

Tabby encrypts the vault contents using **AES-256-CBC** symmetric encryption. The 256-bit encryption keys are derived from the user's master passphrase using **PBKDF2-SHA-512** with 100,000 iterations, as implemented in the `encryptVault` function in [`tabby-core/src/services/vault.service.ts`](https://github.com/Eugeny/tabby/blob/main/tabby-core/src/services/vault.service.ts).

### How does Tabby structure individual secrets within the encrypted container?

Individual secrets follow the `VaultSecret` interface containing three fields: a `type` string identifying the secret category (e.g., `'sshPassword'`), an opaque `key` object that services populate with identifying properties like `host` or `id`, and a `value` string holding the secret text or base64-encoded binary data.

### What happens when I add a new secret to Tabby's vault?

When you add a secret via `VaultService.addSecret()`, the service appends the new `VaultSecret` to the in-memory `Vault.secrets` array, then immediately re-encrypts the entire vault using `encryptVault()` with freshly generated random salts and IVs. The resulting `StoredVault` JSON object is then persisted to Tabby's configuration file, ensuring the new secret is never written to disk unencrypted.

### Is the Tabby vault format versioned for future upgrades?

Yes, the `StoredVault` interface includes a `version` field currently set to **1**. The `decryptVault` function validates this version during loading and throws an error if it encounters an unsupported format. The `migrateVaultContent` helper (lines 48-52) provides normalization logic for legacy data structures, ensuring smooth transitions if the format evolves in future releases.