Tabby's Built-In Encrypted Secrets Container: Architecture and Implementation
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 (lines 20-25). This JSON structure contains the encrypted payload alongside the cryptographic parameters required for decryption:
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):
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):
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:
- Random Generation: Creates a random PBKDF2 salt (
PBKDF_SALT_LENGTH) and AES initialization vector (CRYPT_IV_LENGTH) using cryptographically secure random bytes. - Key Derivation: Derives a 256-bit encryption key from the master passphrase using PBKDF2-SHA-512 with 100,000 iterations (
PBKDF_ITERATIONS). - Symmetric Encryption: Encrypts the JSON-stringified
Vaultobject using AES-256-CBC (CRYPT_ALG), producing the ciphertext stored inStoredVault.contents. - Persistence: Encodes the salt, IV, and ciphertext as hex and base64 strings respectively, storing them in the
StoredVaultstructure 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:
// 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 (lines 8-18, 30-38).
Retrieving a Stored Secret
To access previously stored credentials, services query by type and key properties:
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 (lines 24-28).
Direct Vault Encryption
For external scripts or migrations, you can encrypt a custom vault structure directly:
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.
Summary
- Two-layer architecture: Tabby uses
StoredVaultfor encrypted disk persistence andVaultfor 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
VaultSecretinterface 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
VaultServicewithintabby-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.
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.
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 →