Skip to content

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.

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 blob
KeyFull NamePurpose
DEKData Encryption KeyRandom 256-bit key that encrypts the actual sync data. Generated once during initial sync setup.
MEKMaster Encryption KeyDerived from your sync password via Argon2id. Used to wrap (encrypt) the DEK.
KEKKey Encryption KeyDerived from your recovery key via Argon2id. Used to wrap the DEK separately for account recovery.

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.

Both the MEK and KEK are derived using Argon2id, a memory-hard key derivation function that resists GPU and ASIC brute-force attacks.

ParameterValueNotes
Memory65,536 KB (64 MB)Forces attackers to allocate 64 MB per guess attempt
Iterations3Number of passes over the memory
Parallelism4Lanes of computation
Hash length32 bytes (256 bits)Output key size
Salt32 random bytesGenerated 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.

The DEK encrypts the sync data using AES-256-GCM (Galois/Counter Mode), which provides both confidentiality and integrity:

  1. Serialize — All sync data is serialized to JSON.
  2. Compress — The JSON is zlib-compressed to reduce blob size.
  3. Encrypt — AES-256-GCM encrypts the compressed data with a random 12-byte IV.
  4. Package — The output is: salt(32) + IV(12) + tag(16) + ciphertext.
  1. Parse — Extract salt, IV, GCM tag, and ciphertext from the package.
  2. Derive — Re-derive the MEK from password + salt using the stored Argon2id parameters.
  3. Unwrap DEK — Decrypt the wrapped DEK using the MEK.
  4. Decrypt — AES-256-GCM decrypts and verifies the ciphertext using the DEK.
  5. Decompress — zlib-decompress the plaintext.
  6. 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.

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 wrappedDekPassword and wrappedDekRecovery alongside the encrypted blob.

To authenticate push/pull operations without sending the key, a verification hash is computed:

  1. HKDF with SHA-256 is applied to the MEK with the domain string zestssh-sync-verification.
  2. The output is SHA-256 hashed.
  3. 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.

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.

DataServer has?
Encrypted blobYes — opaque ciphertext
SaltYes — needed to re-derive keys on pull
Argon2id parametersYes — needed for correct derivation
Verification hashYes — for authentication
Wrapped DEK (password)Yes — but encrypted, useless without MEK
Wrapped DEK (recovery)Yes — but encrypted, useless without KEK
Your passwordNever
The MEKNever
The DEKNever
Plaintext dataNever

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.

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.