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 saltS
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
- a 32-byte salt
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])}"