Cryptography & key management

3 min

Key types

Keys that are used:

  • master secret key (for indexes)
  • curve25519 public/private key pair (for incoming mail)

Stored keys

Keys that are stored in K2V under PK keys:

  • public: the public curve25519 key (plain text)
  • salt: the 32-byte salt S used to calculate digests that index keys below
  • if a password is used, password:<truncated(128bit) argon2 digest of password using salt S>:
    • a 32-byte salt Skey
    • followed a secret box
    • that is encrypted with a strong argon2 digest of the password (using the salt Skey) and a user secret (see below)
    • that contains the master secret key and the curve25519 private key

User secret

An additionnal secret that is added to the password when deriving the encryption key for the secret box. This additionnal secret should not be stored in K2V/S3, so that just knowing a user's password isn't enough to be able to decrypt their mailbox (supposing the attacker has a dump of their K2V/S3 bucket). This user secret should typically be stored in the LDAP database or just in the configuration file when using the static login provider.

Operations pseudo-code

We resume here the key cryptography logic for various operations on the mailbox

Creating a user account

This logic is run when the mailbox is created. Two modes are supported: password and certificate.

INITIALIZE(user_secret, password):
   if "salt" or "public" already exist, BAIL
   generate salt S (32 random bytes)
   generate public, private (curve25519 keypair)
   generate master (secretbox secret key)
   calculate digest = argon2_S(password)
   generate salt Skey (32 random bytes)
   calculate key = argon2_Skey(user_secret + password)
   serialize box_contents = (private, master)
   seal box blob = seal_key(box_contents)
   write S at "salt"
   write concat(Skey, blob) at "password:{hex(digest[..16])}"
   write public at "public"

INITIALIZE_WITHOUT_PASSWORD(private, master):
   if "salt" or "public" already exist, BAIL
   generate salt S (32 random bytes)
   write S at "salt"
   calculate public the public key associated with private
   write public at "public"

Opening the user's mailboxes (upon login)

OPEN(user_secret, password):
   load S = read("salt")
   calculate digest = argon2_S(password)
   load blob = read("password:{hex(digest[..16])}")
   set Skey = blob[..32]
   calculate key = argon2_Skey(user_secret + password)
   open secret box box_contents = open_key(blob[32..])
   retrieve master and private from box_contents
   retrieve public = read("public")

OPEN_WITHOUT_PASSWORD(private, master):
   load public = read("public")
   check that public is the correct public key associated with private

Account maintenance

ADD_PASSWORD(user_secret, existing_password, new_password):
   load S = read("salt")
   calculate digest = argon2_S(existing_password)
   load blob = read("existing_password:{hex(digest[..16])}")
   set Skey = blob[..32]
   calculate key = argon2_Skey(user_secret + existing_password)
   open secret box box_contents = open_key(blob[32..])
   retrieve master and private from box_contents
   calculate digest_new = argon2_S(new_password)
   generate salt Skeynew (32 random bytes)
   calculate key_new = argon2_Skeynew(user_secret + new_password)
   serialize box_contents_new = (private, master)
   seal box blob_new = seal_key_new(box_contents_new)
   write concat(Skeynew, blob_new) at "new_password:{hex(digest_new[..16])}"

REMOVE_PASSWORD(password):
   load S = read("salt")
   calculate digest = argon2_S(existing_password)
   check that "password:{hex(digest[..16])}" exists
   check that other passwords exist ?? (or not)
   delete "password:{hex(digest[..16])}"

Navigation