Zero-Knowledge Encryption
ZestSSH Cloud Sync uses a zero-knowledge encryption architecture. Your sync password never leaves your device, and the server only stores encrypted blobs. Even if the server is compromised, your SSH credentials, private keys, and connection data remain protected.
Architecture Overview
Section titled “Architecture Overview”The encryption uses a three-layer key hierarchy with DEK indirection:
Password ---Argon2id---> MEK ---AES-GCM---> wrappedDEK_password |Recovery ---Argon2id---> KEK ---AES-GCM---> wrappedDEK_recovery |random DEK ----------------------AES-GCM---> encrypted blobKey Roles
Section titled “Key Roles”| Key | Full Name | Purpose |
|---|---|---|
| DEK | Data Encryption Key | Random 256-bit key that encrypts the actual sync data. Generated once during initial sync setup. |
| MEK | Master Encryption Key | Derived from your sync password via Argon2id. Used to wrap (encrypt) the DEK. |
| KEK | Key Encryption Key | Derived from your recovery key via Argon2id. Used to wrap the DEK separately for account recovery. |
Why DEK Indirection?
Section titled “Why DEK Indirection?”The DEK is a random key that never changes. It is wrapped separately by both the password-derived MEK and the recovery-derived KEK. This design means:
- Password change only re-wraps the DEK with a new MEK — no data re-encryption needed.
- Recovery unwraps the DEK via the recovery KEK, then the DEK decrypts the data blob.
- The DEK itself is never transmitted in plaintext.
Key Derivation: Argon2id
Section titled “Key Derivation: Argon2id”Both the MEK and KEK are derived using Argon2id, a memory-hard key derivation function that resists GPU and ASIC brute-force attacks.
Parameters
Section titled “Parameters”| Parameter | Value | Notes |
|---|---|---|
| Memory | 65,536 KB (64 MB) | Forces attackers to allocate 64 MB per guess attempt |
| Iterations | 3 | Number of passes over the memory |
| Parallelism | 4 | Lanes of computation |
| Hash length | 32 bytes (256 bits) | Output key size |
| Salt | 32 random bytes | Generated per-user via Random.secure() |
These parameters are versioned (argon2Version = 1) and stored alongside the encrypted blob on the server. Future versions of ZestSSH can bump parameters without breaking existing users’ ability to derive their key — the server returns the parameter version used when the blob was created.
Data Encryption: AES-256-GCM
Section titled “Data Encryption: AES-256-GCM”The DEK encrypts the sync data using AES-256-GCM (Galois/Counter Mode), which provides both confidentiality and integrity:
Encryption Flow
Section titled “Encryption Flow”- Serialize — All sync data is serialized to JSON.
- Compress — The JSON is zlib-compressed to reduce blob size.
- Encrypt — AES-256-GCM encrypts the compressed data with a random 12-byte IV.
- Package — The output is:
salt(32) + IV(12) + tag(16) + ciphertext.
Decryption Flow
Section titled “Decryption Flow”- Parse — Extract salt, IV, GCM tag, and ciphertext from the package.
- Derive — Re-derive the MEK from password + salt using the stored Argon2id parameters.
- Unwrap DEK — Decrypt the wrapped DEK using the MEK.
- Decrypt — AES-256-GCM decrypts and verifies the ciphertext using the DEK.
- Decompress — zlib-decompress the plaintext.
- Deserialize — Parse the JSON into application data.
If the GCM authentication tag does not match (wrong key or tampered data), decryption fails with a SyncDecryptionException.
DEK Wrapping
Section titled “DEK Wrapping”The DEK is wrapped (encrypted) using AES-256-GCM with the wrapping key (MEK or KEK):
- Wrapped format:
nonce(12) + tag(16) + ciphertext(32) = 60 bytes - The server stores both
wrappedDekPasswordandwrappedDekRecoveryalongside the encrypted blob.
Verification Hash
Section titled “Verification Hash”To authenticate push/pull operations without sending the key, a verification hash is computed:
- HKDF with SHA-256 is applied to the MEK with the domain string
zestssh-sync-verification. - The output is SHA-256 hashed.
- This verification hash is sent to the server.
The domain-specific HKDF step ensures the verification hash is cryptographically independent from the MEK, so even if an attacker intercepts the verification hash, they cannot derive the encryption key.
Recovery verification uses a separate domain string (zestssh-recovery-verification) to maintain independence.
Recovery Key
Section titled “Recovery Key”During sync setup, ZestSSH generates a recovery key. This key is:
- Derived through the same Argon2id process with a separate salt.
- Used to wrap the DEK independently of your password.
- The only way to recover access if you forget your sync password.
Store your recovery key safely. ZestSSH cannot recover your data without either your sync password or recovery key.
What the Server Sees
Section titled “What the Server Sees”| Data | Server has? |
|---|---|
| Encrypted blob | Yes — opaque ciphertext |
| Salt | Yes — needed to re-derive keys on pull |
| Argon2id parameters | Yes — needed for correct derivation |
| Verification hash | Yes — for authentication |
| Wrapped DEK (password) | Yes — but encrypted, useless without MEK |
| Wrapped DEK (recovery) | Yes — but encrypted, useless without KEK |
| Your password | Never |
| The MEK | Never |
| The DEK | Never |
| Plaintext data | Never |
Conflict Resolution
Section titled “Conflict Resolution”The server tracks a monotonically increasing version number. Pushes include an expected_version field. If the server version has advanced (another device pushed), a SyncConflictException is thrown with both versions. The client must pull the latest data, merge, and retry.
Certificate Pinning
Section titled “Certificate Pinning”All API calls to the sync server use a pinned HTTP client (PinnedHttpClient) for TLS certificate verification, preventing man-in-the-middle attacks even if a device’s CA store is compromised.