termique
Blog
Deep dive4 min readJune 18, 2026

End-to-end encrypted SSH credential storage: how it actually works

A technical walkthrough of PBKDF2 key derivation, AES-GCM encryption, and DEK wrapping, and why server-side encryption is not the same thing.

“End-to-end encrypted” gets applied to everything from messaging apps to cloud storage to SSH managers. It’s often used to mean “encrypted at rest,” which is weaker than it sounds. Here’s a precise definition and a technical walkthrough of what real E2EE looks like for SSH credentials.

What end-to-end encryption means, precisely

End-to-end encryption for stored credentials means: the encryption key is derived on the client, never transmitted to the server, and the server stores only ciphertext. Even with full access to the server’s database and filesystem, an attacker cannot recover plaintext credentials without the user’s master password.

This is different from server-side encryption, where the server encrypts data at rest using a key it manages. Server-side encryption protects against an attacker who steals the raw database files – but not against the service provider itself, a compromised key management system, or a legal order requiring decryption.

The three-layer structure

Layer 1: Key derivation from master password

The master password is never stored or transmitted. Instead, it’s used to derive a cryptographic key using PBKDF2 (Password-Based Key Derivation Function 2) with SHA-256, 100,000 iterations, and a random 16-byte salt stored server-side.

master_key = PBKDF2(
  password = user_master_password,
  salt     = user_salt,          // stored server-side, not secret
  iterations = 100_000,
  hash     = SHA-256,
  key_len  = 32 bytes
)

The salt is not secret – it’s stored in your user record. Its purpose is to prevent precomputation attacks (rainbow tables). The iteration count makes brute-forcing expensive: 100,000 iterations of PBKDF2-SHA256 takes around 100ms on modern hardware. An attacker trying 10 billion passwords per second on dedicated hardware still takes years to exhaust a reasonable password space.

Layer 2: Data Encryption Key (DEK)

Rather than encrypting each credential directly with the master-password-derived key, a separate random DEK (Data Encryption Key) is generated once per user. All credentials are encrypted with this DEK. The DEK itself is encrypted (“wrapped”) with the master key and stored server-side.

This indirection solves a practical problem: if a user changes their master password, only the wrapped DEK needs to be re-encrypted – not every credential in the vault. Without a DEK, changing your password would require decrypting and re-encrypting every stored credential.

dek = random_bytes(32)                    // generated once, random
wrapped_dek = AES_GCM_encrypt(
  key   = master_key,                     // derived above
  plain = dek
)                                         // wrapped_dek stored server-side

Layer 3: Credential encryption

Each SSH credential (password or private key) is encrypted individually using AES-256-GCM with a fresh random 12-byte nonce. GCM mode provides both confidentiality (the data is encrypted) and integrity (any tampering is detectable).

nonce = random_bytes(12)                  // fresh per credential
ciphertext = AES_GCM_encrypt(
  key   = dek,                            // unwrapped on unlock
  nonce = nonce,
  plain = ssh_password_or_private_key
)
// stored: nonce + ciphertext + auth_tag

What the server actually stores

  • user_salt – random, not secret, needed for key derivation
  • wrapped_dek – DEK encrypted with master key; useless without master password
  • verification_blob – used to check master password correctness without storing the password
  • encrypted_credential – nonce + ciphertext + auth tag; useless without DEK

The server has no master key, no unwrapped DEK, and no plaintext credentials. A complete database breach exposes only data that cannot be decrypted without the user’s master password.

Where computation happens

In Termique, all cryptographic operations run in a protected native process separate from the renderer. Key material lives in managed state and never enters the JavaScript layer. The renderer invokes encrypt/decrypt via a local IPC call; it receives only the result, never the key.

This architecture means there’s no way to extract the key by inspecting JavaScript memory, intercepting renderer IPC calls, or using devtools. The key exists only inside the Rust process.

Private keys specifically

SSH private keys get an additional layer: after decryption from the vault, they are stored in the OS keychain (macOS Keychain, Windows Credential Manager, libsecret on Linux) rather than in memory for the session duration. The SSH agent signs authentication challenges using the keychain-stored key without exposing the key bytes to the app itself.

What this doesn’t protect against

E2EE of stored credentials does not protect against a compromised machine. If your device has malware with keylogger or process memory access, your master password and decrypted credentials can be captured at the moment of use. It also doesn’t protect against a weak master password that can be brute-forced offline from a stolen wrapped DEK and salt.

The threat model that E2EE addresses: a breach of the cloud service, a rogue employee, or a legal compulsion order against the service provider. Against those threats, a properly implemented E2EE scheme is as strong as the user’s master password.

Related articles

Try Termique free.

SSH manager with end-to-end encrypted credentials, AI assistant, and cross-device sync.

Download free

More articles

01
GuideThe complete guide to SSH security in 2026
July 10, 2026
02
GuideBest SSH client for developers in 2026
July 6, 2026
03
FeatureTermique v0.3.0 – referrals, monitoring alerts, and remote sign-out
July 5, 2026
04
GuideHow to deploy Laravel with FrankenPHP on a VPS
July 2, 2026
All articles ⟶