Skip to content

Encryption Design

Consider your data secured inside multiple independent layers of protection. These layers are not physical barriers but well-studied cryptographic constructions designed to resist both current and future attacks.

Your password is never stored. It is used only to derive a key that unlocks a small encrypted container holding the actual vault key. This vault key is generated randomly and is entirely independent of your password. All user data is encrypted with this random key.

As a result:

  • Changing your password does not require re-encrypting your data
  • Your data security does not depend directly on password strength alone
  • An attacker must defeat multiple independent cryptographic layers

At no point do we have access to your password or your data. A stolen vault file reveals only indistinguishable random data.

  • Password is never stored — only a derived key is used
  • Data is encrypted with a random master key — independent of the password
  • Password changes do not require re-encrypting data — only the wrapped master key is updated
  • Multiple cipher layers — reduces reliance on any single cipher
  • Integrity protection — ciphertext and metadata are authenticated
  • No recovery mechanism — no backdoors or server-side keys exist

Your Password

Argon2id + salt₁ (memory-hard)

key₁ — Password-derived key

CSPRNG

key_M — Master key (random)

salt₂

HKDF (salt₂, per-cipher info)

AES-256 key

Twofish-256 key

Serpent-256 key

HMAC key

Cipher cascade encryption

Encrypted master key

Stored: salts, params, IVs, enc(key_M)

The password is transformed using Argon2id:

key1=Argon2id(password,salt1,m,t,p)\mathrm{key}_1 = \mathrm{Argon2id}(\mathrm{password}, \mathrm{salt}_1, m, t, p)

Parameters:

  • mm — memory cost
  • tt — time cost
  • pp — parallelism

These parameters are stored alongside salt1\mathrm{salt}_1.

keyM0,1256\mathrm{key}_M \leftarrow {0,1}^{256}

The master key is generated uniformly at random (256-bit entropy) and is independent of the password.

First derive a pseudorandom key:

PRK=HKDFExtract(salt2,key1)\mathrm{PRK} = \mathrm{HKDF_Extract}(\mathrm{salt}_2, \mathrm{key}_1)

Then expand per cipher:

wrapKeyi=HKDFExpand(PRK,info="wrap-cipher-i",L=32)\mathrm{wrapKey}_i = \mathrm{HKDF_Expand}( \mathrm{PRK}, \mathrm{info} = \text{"wrap-cipher-}i\text{"}, L = 32 )

Authentication key:

kauth=HKDFExpand(PRK,info="auth",L=64)k_{\mathrm{auth}} = \mathrm{HKDF_Expand}( \mathrm{PRK}, \mathrm{info} = \text{"auth"}, L = 64 )

Domain separation is enforced via distinct info values.

Each cipher operates in CTR mode with a unique random IV.

The master key is encrypted through a cascade:

c1=EncAES-CTR(k1,IV1,keyM) c2=EncTwofish-CTR(k2,IV2,c1) c3=EncSerpent-CTR(k3,IV3,c2)\begin{aligned} c_1 &= \mathrm{Enc}*{\mathrm{AES\text{-}CTR}}(k_1, IV_1, \mathrm{key}*M) \ c_2 &= \mathrm{Enc}*{\mathrm{Twofish\text{-}CTR}}(k_2, IV_2, c_1) \ c_3 &= \mathrm{Enc}*{\mathrm{Serpent\text{-}CTR}}(k_3, IV_3, c_2) \end{aligned}

Authentication (Encrypt-then-MAC):

final=HMACSHA512(kauth,salt1mtpsalt2IV1IV2IV3c3)  IV1IV2IV3c3\mathrm{final} = \mathrm{HMAC}*{\mathrm{SHA512}}( k*{\mathrm{auth}}, \mathrm{salt}_1 | m | t | p | \mathrm{salt}_2 | IV_1 | IV_2 | IV_3 | c_3 ) \ |\ IV_1 | IV_2 | IV_3 | c_3

IVs are generated via CSPRNG, are unique per encryption, and stored alongside the ciphertext.

ValueDescription
salt1\mathrm{salt}_1Argon2 salt
(m,t,p)(m, t, p)Argon2 parameters
salt2\mathrm{salt}_2HKDF salt
IV1,IV2,IV3IV_1, IV_2, IV_3Per-layer IVs
enc(keyM)\mathrm{enc}(\mathrm{key}_M)Ciphertext + authentication tag

salt₁

salt₂

enc(key_M)

Your Password

Argon2id + salt₁

salts · params · IVs · enc(key_M)

key₁

HKDF (salt₂, per-cipher info)

AES-256 key

Twofish-256 key

Serpent-256 key

HMAC key

Verify HMAC → Cascade decryption

key_M

HKDF (data keys)

AES-256

Twofish-256

Serpent-256

Cipher cascade

Encrypted data

key1=Argon2id(password,salt1,m,t,p)\mathrm{key}_1 = \mathrm{Argon2id}(\mathrm{password}, \mathrm{salt}_1, m, t, p)
  1. Recompute key1\mathrm{key}_1
  2. Re-derive wrap keys and kauthk_{\mathrm{auth}}
  3. Verify HMAC before decryption
  4. Decrypt cascade (reverse order):
c2=DecSerpent-CTR(k3,IV3,c3) c1=DecTwofish-CTR(k2,IV2,c2) keyM=DecAES-CTR(k1,IV1,c1)\begin{aligned} c_2 &= \mathrm{Dec}*{\mathrm{Serpent\text{-}CTR}}(k_3, IV_3, c_3) \ c_1 &= \mathrm{Dec}*{\mathrm{Twofish\text{-}CTR}}(k_2, IV_2, c_2) \ \mathrm{key}*M &= \mathrm{Dec}*{\mathrm{AES\text{-}CTR}}(k_1, IV_1, c_1) \end{aligned}

Incorrect passwords or tampering result in authentication failure.

PRKD=HKDFExtract(salt2,keyM)\mathrm{PRK}_D = \mathrm{HKDF_Extract}(\mathrm{salt}_2, \mathrm{key}_M) dataKeyi=HKDFExpand(PRKD,info="data-cipher-i",L=32)\mathrm{dataKey}_i = \mathrm{HKDF_Expand}( \mathrm{PRK}_D, \mathrm{info} = \text{"data-cipher-}i\text{"}, L = 32 )

User data is encrypted using the same cascade structure:

  • CTR mode per cipher
  • fresh random IVs per object
  • Encrypt-then-MAC using HMAC-SHA512
  • authentication covers ciphertext and all required metadata
AlgorithmPurpose
Argon2idPassword key derivation
HKDF-SHA512Key expansion
AES-256-CTRCipher layer
Twofish-256-CTRCipher layer
Serpent-256-CTRCipher layer
XChaCha20-Poly1305Optional AEAD cipher
HMAC-SHA512Authentication
CSPRNGRandom generation
ThreatMitigation
Brute-force attacksArgon2id with high cost
Stolen vault fileFull encryption + authenticated decryption
Cipher failureMulti-cipher cascade
TamperingHMAC over ciphertext and metadata
Key reuseHKDF domain separation
IV collisionRandom IVs per operation
Memory exposureKey zeroization