+++ title = "Cryptography & key management" weight = 20 +++ ## 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:`: - 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])}"`