Authenticated encryption (XChacha20 + Poly1305)
Encryption makes your messages unreadable to eavesdroppers. Authentication ascertain the origin and integrity of the messages you read.
Both are important. Without encryption, you give away all your secrets, and without authentication, you can fall prey to forgeries (messages that look legitimate, but actually come from the attacker). A clever attacker may even leverage forgeries to steal your secrets.
Always authenticate your messages.
void crypto_aead_lock(uint8_t mac, uint8_t *ciphertext, const uint8_t key, const uint8_t nonce, const uint8_t *ad , size_t ad_size, const uint8_t *plaintext, size_t text_size);
Authenticated encryption with additional data.
The inputs are:
key: a 32-byte session key, shared between you and the recipient. It must be secret (unknown to the attacker) and random (unpredictable to the attacker). Of course, one does not simply transmit this key over the network. There are less suicidal ways to share session keys, such as meeting physically, or performing a Diffie Hellman key exchange (described below).
nonce: a 24-byte a number, used only once with any given session key. It doesn't have to be secret or random. But you must never reuse that number with the same key. If you do, the attacker will have access to the XOR of 2 different messages, and the ability to forge messages in your stead.
The easiest (and recommended) way to generate this nonce is to use your OS's random number generator (
/dev/urandomon UNIX systems). Don't worry about accidental collisions, the nonce is big enough to make them virtually impossible.
Don't use user space random number generators, they're error prone. You could accidentally reuse the generator's internal state, duplicate the random stream, and trigger a nonce reuse. Oops.
ad: the additional data. It is authenticated, but not encrypted. Sometimes useful when your protocol prevents you from encrypting bits of data, but you want to authenticate them anyway.
adis optional. It may be null if
plaintext: the secret you want to send. Of course, it must be unknown to the attacker. Keep in mind however that the length of the plaintext, unlike its content, is not secret. Make sure your protocol doesn't leak secret information with the length of messages. (It has happened before with variable-length voice encoding software.) Solutions to mitigate this include constant-length encodings and padding.
The outputs are:
mac: a 16-byte message authentication code (MAC), that only you could have produced. (Of course, this guarantee goes out the window the nanosecond the attacker somehow learns your session key, or sees 2 messages with the same nonce. Seriously, don't reuse that nonce.)
Transmit this MAC over the network so the recipient can authenticate your message.
ciphertext: the encrypted message (same length as the plaintext message). Transmit it over the network so the recipient can decrypt and read it.
ciphertextis allowed to have the same value as
plaintext. I so, encryption will happen in place.
int crypto_aead_unlock(uint8_t *plaintext, const uint8_t key, const uint8_t nonce, const uint8_t mac, const uint8_t *ad , size_t ad_size, const uint8_t *ciphertext, size_t text_size);
The flip side of the coin. The inputs are:
key: the session key. It's the same as the one used for authenticated encryption.
nonce: the nonce that was used to encrypt this particular message. No decryption is possible without it.
mac: the message authentication code produced by the sender. Integrity cannot be ensured without it.
ad: the additional data sent by the sender. Note:
adis optional. It may be null if
ciphertext: the encrypted text produced by the sender.
There are 2 outputs:
plaintext: The decrypted message (same length as the ciphertext).
plaintextis allowed to be the same as
ciphertext. If so, decryption will happen in place.
A return code: 0 if all went well, -1 if the message was corrupted (either accidentally or intentionally).
Tip: always check your return code.
Unlocking proceeds in two steps: first, we authenticate the additional
data and the ciphertext with the provided MAC. If any of those three
has been corrupted,
crypto_aead_unlock() returns -1 immediately,
without decrypting the message. If the message is genuine,
crypto_aead_unlock() decrypts the ciphertext, then returns 0.
(Again, if someone gave away the session key or reused a nonce, detecting forgeries becomes impossible. Don't reuse the nonce.)
void crypto_lock(uint8_t *box, // text_size + 16 const uint8_t key, const uint8_t nonce, const uint8_t *plaintext, size_t text_size);
Simplified interface for authenticated encryption, without additional data.
Here, the output is a "box" containing both the message and its authentication code. The MAC takes the first 16 bytes of the box, the message occupies the rest. The box must be 16 bytes larger than the message.
int crypto_unlock(uint8_t *plaintext, // box_size - 16 const uint8_t key, const uint8_t nonce, const uint8_t *box, size_t box_size);
Simplified interface for authenticated decryption.
Again, the "box" contains both the message and its authentication code (MAC first). The plaintext is 16 bytes smaller than the box.
Diffie-Hellman key exchange (X25519 + HChacha20)
Key exchange works thus: Alice and Bob each have a key pair (a secret key and a public key). They know each other's public key, but they keep their own secret key… secret. Key exchange works like this:
shared_secret = get_shared_secret(Alice_public_key, Bob_secret_key) = get_shared_secret(Bob_public_key, Alice_secret_key)
If Eve learns Alice's secret key, she could compute the shared secret between Alice and anyone else (including Bob), allowing her to read and forge correspondence. Protect your secret key.
Furthermore, Alice and Bob must know each other's public keys beforehand. If they don't, and try to communicate those keys over an insecure channel, Eve might intercept their communications and provide false public keys. There are various ways to learn of each other's public keys (crypto parties, certificate authorities, web of trust…), each with its advantages and drawbacks.
int crypto_lock_key(uint8_t shared_key , const uint8_t your_secret_key , const uint8_t their_public_key);
Computes a shared key with your secret key and their public key,
suitable for the
crypto_*lock() functions above. It performs a
X25519 key exchange, then hashes the shared secret (with HChacha20) to
get a suitably random-looking shared key.
Keep in mind that if either of your long term secret keys leaks, it may compromise all past messages! If you want forward secrecy, you'll need to exchange temporary public keys, then compute your shared secret with them. (How that should be done, and the exact security guarantees are not clear to me at the moment.)
The return code serves as a security check: there are a couple evil public keys out there, that force the shared key to a known constant (the HCHacha20 of zero). This never happens with legitimate public keys, but if the ones you process aren't exactly trustworthy, watch out.
crypto_lock_key() returns -1 whenever it detects such an evil
public key. If all goes well, it returns zero.
void crypto_x25519_public_key(uint8_t public_key, const uint8_t secret_key);
Deterministically computes the public key from the specified secret key. Make sure the secret key is random. Again, use your OS's random number generator.
int crypto_x25519(uint8_t shared_secret , const uint8_t your_secret_key , const uint8_t their_public_key);
Tip: this is a low level function. Unless you really know what
you're doing, you should use
Computes a shared secret with your secret key and the other party's
public key. Warning: the shared secret is not cryptographically
random. Don't use it directly as a session key. You need to hash it
first. Any cryptographically secure hash will do.
crypto_lock_key() uses HChacha20 (it's not a general purpose hash,
but here it works just fine).
crypto_lock_key(), the return code asserts contributory
behaviour: if zero, all went well. If -1, the shared secret has been
forced to a string of zeros.
Implementation detail: note that the most significant bit of the public key is systematically ignored. It is not needed, because every public key should be smaller than 2^255-19, which fits in 255 bits. If another implementation of x25519 gives you a key that's not fully reduced and has its high bit set, the computation will fail. On the other hand, it also means you may use this bit for other purposes (parity flipping for ed25519 compatibility or whatever unfathomable goal you have in mind).
Public key signatures (edDSA with curve25519 & Blake2b)
Authenticated encryption with key exchange is not always enough. Sometimes, you want to broadcast a signature, in such a way that everybody can verify.
When you sign a message with your private key, anybody who knows your public key can verify that you signed the message. Obviously, any attacker that gets a hold of your private key can sign messages in your stead. Protect your private key.
Monocypher provides public key signatures with a variant of ed25519, which uses Blake2b as the hash instead of SHA-512. SHA-512 is provided as an option for compatibility with other systems.
Blake2b is the default because it is faster, more flexible, harder to misuse than SHA-512, and already required by Argon2i. Monocypher needs only one hash, and that shall be Blake2b.
The reason why there's a SHA-512 option at all is official test vectors. Can't test signatures reliably without them.
Note that using Blake2b instead of SHA-512 does not block your upgrade path to faster implementations: Floodyberry's Donna library provides blazing fast implementations that can work with custom hashes.
void crypto_sign_public_key(uint8_t public_key, const uint8_t secret_key);
Deterministically computes a public key from the specified secret key. Make sure the secret key is randomly selected. OS good. User space bad.
By the way, these are not the same as key exchange key pairs. Maintain separate sets of keys for key exchange and signing. There are clever ways to unify those keys, but those aren't covered by Monocypher.
void crypto_sign(uint8_t signature, const uint8_t secret_key, const uint8_t public_key, // optional, may be null const uint8_t *message, size_t message_size);
Signs a message with your secret key. The public key is optional, and will be recomputed if you don't provide it. It's twice as slow, though.
int crypto_check(const uint8_t signature, const uint8_t public_key, const uint8_t *message, size_t message_size);
Checks that a given signature is genuine. Returns 0 for legitimate messages, -1 for forgeries. Of course, if the attacker got a hold of the matching private key, all bets are off.
A word of warning: this function does not run in constant time. It doesn't have to in most threat models, because nothing is secret: everyone knows the public key, and the signature and message are rarely secret.
If you want to ascertain the origin of a secret message, you may want to use x25519 key exchange instead.
Cryptographic Hash (Blake2b)
Blake2b is a fast cryptographically secure hash, based on the ideas of Chacha20. It is faster than md5, yet just as secure as SHA-3.
The direct interface sports 2 functions:
void crypto_blake2b_general(uint8_t *digest, size_t digest_size, const uint8_t *key , size_t key_size, const uint8_t *in , size_t in_size); void crypto_blake2b(uint8_t digest, const uint8_t *in, size_t in_size);
The second one is a convenience function, which uses a 64 bytes hash and no key (this is a good default).
If you use the first function, you can specify the size of the digest (I'd advise against anything below 32-bytes), and use a secret key to make the hash unpredictable —useful for message authentication codes.
(Note: Blake2b is immune to length extension attacks, and as such does not require any specific precaution. It can authenticate messages with a naive approach. However, older hashes are not immune to such attacks, and do require those precautions.)
digest: The output digest. Must have at least
digest_size: the length of the hash. Must be between 1 and 64.
key_size: length of the key. Must be between 0 and 64.
key: some secret key. May be null if key_size is 0.
Any deviation from these invariants results in undefined behaviour. Make sure your inputs are correct.
Incremental interfaces are useful to handle streams of data or large files without using too much memory. This interface uses 3 steps:
- initialisation, where we set up a context with the various hashing parameters;
- update, where we hash the message chunk by chunk, and keep the intermediary result in the context;
- and finalisation, where we produce the final digest.
There are 2 init functions, one update function, and one final function:
void crypto_blake2b_general_init(crypto_blake2b_ctx *ctx, size_t digest_size, const uint8_t *key, size_t key_size); void crypto_blake2b_init(crypto_blake2b_ctx *ctx); void crypto_blake2b_update(crypto_blake2b_ctx *ctx, const uint8_t *in, size_t in_size); void crypto_blake2b_final(crypto_blake2b_ctx *ctx, uint8_t *digest);
The invariants of the parameters are the same as for
digest_size must be between 1 and 64,
key_size must be between 0 and 64. Any bigger and you get undefined
crypto_blake2b_init() is a convenience init function, that specifies
a 64 bytes hash and no key. This is a good default.
crypto_blake2b_update() computes your hash piece by piece.
crypto_blake2b_final() outputs the digest.
Here's how you can hash the concatenation of 3 chunks with the incremental interface:
uint8_t digest; crypto_blake2b_ctx ctx; crypto_blake2b_init (&ctx); crypto_blake2b_update(&ctx, chunk1, chunk1_size); crypto_blake2b_update(&ctx, chunk2, chunk2_size); crypto_blake2b_update(&ctx, chunk3, chunk3_size); crypto_blake2b_final (&ctx, digest);
Password key derivation (Argon2i)
Storing passwords in plaintext is suicide. Storing hashed and salted passwords is better, but still very dangerous: passwords simply don't have enough entropy to prevent a dedicated attacker from guessing them by sheer brute force.
One way to prevent such attacks is to make sure hashing a password takes too much resources for a brute force search to be effective. Moreover, we'd like the attacker to spend as much resources for each attempt as we do, even if they have access to dedicated silicon.
Argon2i is a resource intensive password key derivation scheme optimised for the typical x86-like processor. It runs in constant time with respect to the contents of the password.
Typical applications are password checking (for online services), and key derivation (so you can encrypt stuff). You can use this for instance to protect your private keys.
The version currently provided by Monocypher has no threading support, so the degree of parallelism is currently limited to 1. It's good enough for most purposes anyway.
void crypto_argon2i(uint8_t *tag, uint32_t tag_size, void *work_area, uint32_t nb_blocks, uint32_t nb_iterations, const uint8_t *password, uint32_t password_size, const uint8_t *salt, uint32_t salt_size, const uint8_t *key, uint32_t key_size, const uint8_t *ad, uint32_t ad_size);
- The minimum tag size is 4 bytes
- The minimum number of blocks is 8. (blocks are 1024 bytes big.)
- the work area must be big enough to hold the requested number of
blocks, and suitably aligned for 64-bit integers. Tip: just use
- The minimum number of iterations is 1.
- The minimum salt size is 8 bytes.
- The key and additional data are optional. They can be null if their respective size is zero.
Any deviation from these invariants may result in undefined behaviour.
Recommended choice of parameters:
- If you need a key, use a 32 byte one.
- Do what you will with the additional data
- Use a 32 byte tag to derive a 256-bit key.
- Put 128 bits of entropy in the salt. 16 random bytes work well.
- Use at least 3 iterations. Argon2i is less safe with only one or two. Otherwise, more memory is better than more iterations.
crypto_memcmp() to compare Argon2i outputs. Argon2i is designed
to withstand offline attacks, but if you reveal your database through
timing leaks, the weakest passwords will be vulnerable.
The hardness of the computation can be chosen thus:
Decide how long the computation should take. Typically somewhere between half a second (convenient) and several seconds (paranoid).
Try to hash a password with 3 iterations and 100.000 blocks (a hundred megabytes). If it takes too long, reduce that number. If it doesn't take long enough, increase that number.
If the computation is too short even with all the memory you can spare, increase the number of iterations.
Constant time comparison
Packaging an easy to use, state of the art, timing immune crypto library took me over 2 months, full time. It will all be for naught if you start leaking information by using standard comparison functions.
In crypto, we often need to compare secrets together. A message authentication code for instance: while the MAC sent over the network along with a message is public, the true MAC is secret. If the attacker attempts a forgery, you don't want to tell him "your MAC is wrong, and it took me 384 microseconds to figure it out". If in the next attempt it takes you 462 microseconds instead, it gives away the fact that the attacker just got a few bytes right. Next thing you know, you've destroyed integrity.
You need special comparison functions, whose timing do not depend on the content of the buffers. They generally work with bit-wise or and xor.
Monocypher provides 2 functions:
int crypto_memcmp (const uint8_t *p1, const uint8_t *p2, size_t n); int crypto_zerocmp(const uint8_t *p , size_t n);
crypto_memcmp() returns 0 if it the two memory chunks are the same,
crypto_zerocmp() returns 0 if all bytes of the memory
chunk are zero, -1 otherwise. They both run in constant time. (More
precisely, their timing depends solely on the length of their
Warning: encryption alone is not sufficient for security. Using Chacha20 directly is therefore discouraged. Use authenticated encryption instead.
Monocypher provides an incremental interface for Chacha20, with an initialisation, and as many encryption steps as you want.
A not-so-cryptographic hash. May be used for some specific purposes, such as X25519 key derivation, or XChacha20 initialisation. If in doubt, do not use directly. Use Blake2b.
out is a cryptographically secure random number if
there's enough entropy in in the input
key. X25519 shared secrets
have enough entropy. The input
in fills the space reserved for the
nonce and counter. It doesn't have to be random.
void crypto_chacha20_H(uint8_t out, const uint8_t key, const uint8_t in );
void crypto_chacha20_init(crypto_chacha_ctx *ctx, const uint8_t key, const uint8_t nonce);
Initialises a chacha context. Again, don't use the same nonce and key twice. You'd expose the XOR of subsequent encrypted messages, and destroy confidentiality.
Warning: don't select the nonce at random Unlike the authenticated encryption we've seen at the top, this nonce is only 64 bits. This is too small for random nonces: you might reuse one by sheer dumb misfortune. Use a counter instead.
If there are multiple parties sending out messages, you can give them all an initial nonce of 0, 1 .. n-1 respectively, and have them increment their nonce by n. (Also make sure the counters never wrap around.)
void crypto_chacha20_Xinit(crypto_chacha_ctx *ctx, const uint8_t key, const uint8_t nonce);
Initialises a chacha context with a big nonce (192 bits). This nonce is big enough to be selected at random (use the OS; avoid user space generators). This is the init function Monocypher uses for authenticated encryption.
The bigger nonce is allowed by a clever use of HChacha20. The security guarantees are the same as regular initialisation. It is just a tiny bit slower —it doesn't matter in practice.
void crypto_chacha20_encrypt(crypto_chacha_ctx *ctx, uint8_t *cipher_text, const uint8_t *plain_text, size_t message_size);
Encrypts the plain_text by XORing it with a pseudo-random stream of numbers, seeded by the provided chacha20 context. Decryption is the same as encryption. Once the context is initialised, encryption can safely be chained thus:
crypto_encrypt_chacha20(ctx, plain_0, cipher_0, length_0); crypto_encrypt_chacha20(ctx, plain_1, cipher_1, length_1); crypto_encrypt_chacha20(ctx, plain_2, cipher_2, length_2);
plain_text and the output
cipher_text may point to the
same location, for in-place encryption.
plain_text is allowed to be null (0), in which case it
will be interpreted as an all zero input. The cipher_text will then
contain the raw chacha20 stream.
I must insist, encryption alone is not secure. Use authenticated encryption.
void crypto_chacha20_stream(crypto_chacha_ctx *ctx, uint8_t *cipher_text, size_t message_size);
Convenience function. Same as
chacha20_encrypt() with a null
plain_text. Useful as a user space random number generator. Did
I tell you that user space random number generators are error prone?
By the way, it's even worse in multithreaded programs. Really, use
your OS random number generator.
Still, this function can be used outside of a security context: deterministic procedural generation comes to mind.
One-time authentication (Poly1305)
Warning: Poly1305 is easy to mess up. Using it directly is just asking for trouble. Please don't. Use authenticated encryption instead.
Monocypher provides both a direct interface and an incremental interface for Poly1305.
void crypto_poly1305_auth(uint8_t mac, const uint8_t *m, size_t msg_size, const uint8_t key);
Produces a message authentication code for the given message and authentication key. Be careful. The requirements for this key are stringent:
- It must be secret: the attacker cannot be allowed to guess that key.
- It must be shared: the recipient must know this key. Without it, the integrity of the message cannot be verified.
- It must never be reused. That would utterly destroy security, and allow the attacker to recover the key then forge messages.
You cannot use the session key for this: it is secret and shared, but it is reused. If you use it, the attacker will recover it as soon as the second message is sent, and will break everything.
You cannot use a random number: if you don't send it over the network, it won't be shared, and the recipient wont be able to check anything. If you do, it won't be secret, and the attacker will be able to forge messages.
The only practical source for the authentication key is a chunk of the encryption stream used to encrypt the message. However, you must ensure you do not reuse that part of the stream to encrypt the message itself: the attacker could guess the stream by guessing the message, and forge away like a false-smith.
To get this right, you need a session key, a unique nonce, and a
stream cipher. Generate a stream with the session key and nonce.
Take the first 32 bits of that stream as your authentication key, then
use the rest of the stream to encrypt your message. Check out the
source code of
crypto\_aead\_lock() to see how it's done.
void crypto_poly1305_init(crypto_poly1305_ctx *ctx, const uint8_t key); void crypto_poly1305_update(crypto_poly1305_ctx *ctx, const uint8_t *m, size_t bytes); void crypto_poly1305_final(crypto_poly1305_ctx *ctx, uint8_t mac);
This is pretty straightforward. The init function initialises a context, and the update function authenticates the message chunk by chunk. Once the message is entirely processed, the final function gives you the message authentication code. For instance:
uint8_t mac; crypto_poly1305_cxt ctx; crypto_poly1305_init (&ctx, authentication_key); crypto_poly1305_update(&ctx, chunk1, chunk1_size); crypto_poly1305_update(&ctx, chunk2, chunk2_size); crypto_poly1305_update(&ctx, chunk3, chunk3_size); crypto_poly1305_final (&ctx, mac);