LoupVaillant / Monocypher

An easy to use, easy to deploy crypto library
https://monocypher.org
Other
580 stars 80 forks source link

Crypto_box availability #199

Closed nrclark closed 3 years ago

nrclark commented 3 years ago

Do you have any plans to include crypto_box() support? I'm working on a gpg-style tool for encrypting firmware releases, and I want to use asymmetric encryption.

It's easy enough for me to use libsodium on a build server, but much harder to bake it into a bootloader or a bare-metal system. I've been using tweetnacl, but I'd love to ditch it because of its slow performance and its weird codebase.

LoupVaillant commented 3 years ago

Hi,

You can do a crypto_box_equivalent() in Monocypher by calling crypto_key_exchange() then crypto_lock().

The reason I do not provide crypto_box() directly is because using the two functions above in succession is easy and not error prone (and I want to keep the API as small and orthogonal as reasonable.)

A suggestion about encrypting firmware updates: if the sender uses a temporary (ephemeral) key to encrypt the firmware, and a long term (static) EdDSA key to sign the ciphertext, security will be good, but the target system will have to verify the signature, which is (i) not the fastest option and (ii) requires EdDSA, which represent a non-negligible volume of code. (It is the fastest option on the sender's side, though).

Nearly identical security can be achieved with key exchange only: instead of just exchanging between the server's ephemeral key and the recipient's long term key, you can also exchange between the server's long term key and the recipient long term keys, then hash the two key exchanges together to form the symmetric encryption key (which you can use with crypto_lock() as usual). It will be a bit faster than signature verification, and will require significantly less code on the recipient side, in case that matters.

I'm currently working on a turnkey solution (it's basically a Noise clone), but it's not done yet.

fscoto commented 3 years ago

Adding to @LoupVaillant's comment: Monocypher cannot possibly provide crypto_box() in a compatible form because it is being compatible with NaCl and libsodium: crypto_box() uses (X)Salsa20, which Monocypher does not include. As far as I know, there are no plans to ever include it, either, as its function is provided by the faster (X)Chacha20.

The cross-compatibility story between NaCl, TweetNaCl, libsodium and Monocypher is a bit complex, but you can generally assume that if two functions don't have the same name, they're not going to be compatible, with the crypto_sign() family being incompatible across all but TweetNaCl<->libsodium.

nrclark commented 3 years ago

@fscoto @LoupVaillant

Thanks, this is great info. I'll give it some thought, and I'll give the crypto_key_exchange() and crypto_lock() combo a test.

nrclark commented 3 years ago

While I've got the ear of a couple of crypto gurus, I'd love to get your thoughts on the crypto approach I'd been planning. If you spot any security weaknesses/design oversights, it'll help me learn.

The approach I'd been working on was:

  1. Build-server keeps one set of private-keys for crypto_box() and crypto_sign(), and a different crypto_box() public key. These are bundled into one file, which is owned by the build-server.

  2. Device has public-keys to match the build-server's private ones, and its own crypto_box() private key (to match the server-side pubkey). These are also bundled together into one file, which is installed on all devices.

  3. Build-server processes a file in blocks, and uses the following algorithm:

    1. Output stream starts off with a randomly-generated nonce, stored in plaintext.
    2. Input stream is processed in blocks of 128KiB (last block can be smaller).
    3. Each block is encrypted with crypto_box(), incrementing the nonce by 1 each time.
    4. The MAC for each block's cyphertext is signed using crypto_sign(), and the signature is placed right before the cyphertext. The cyphertext's MAC is assumed to be the first 16 (aka crypto_box_MACBYTES) bytes of the output from crypto_box().
  4. Encrypted file is provided to the target device via insecure channels, possibly offline/asynchronous (HTTP download, USB stick, etc).

  5. When the target decrypts the file, it uses the following algorithm:

    1. Nonce is received and stored in memory.
    2. Block is received.
    3. Block's MAC signature is verified using the target's half of the crypto_sign() keypair.
    4. If the signature matches, block is decrypted with crypto_box_open() using the target's crypto_box private-key and the build-server's crypto_box public key. Stored nonce is incremented.
    5. Decryption tool waits for the next block of data.
LoupVaillant commented 3 years ago

Okay, that's not too bad. I do have a couple red flags to look out for, but nothing that isn't easily fixed.

  1. Your description makes it sound like the server has one set of keys for all devices. Which would mean all devices share the same private key. I guess that wasn't the intention, but if it was: try to avoid that. Give each device their own key, so that if one is compromised, the others are still safe. (It's okay however to have only one set of private keys for the server.) It does mean the server has to perform one encryption per device, instead of once for everyone, but as you'll see there are ways around that.

  2. You're only using static keys. It makes things simpler, but it reduces forward secrecy: all past messages will be revealed if the server or the device lose their keys. Not the most important point, considering that forward secrecy is fundamentally limited in non-interactive channels. We can let that slide.

  3. You are signing only the MAC. That may be safe, but you should know that unlike HMAC, attackers can have non-trivial control over the final value of the MAC in some circumstances (that does not make them unsafe, mind you, just less widely applicable). I'd rather sign the whole ciphertext, even if it makes everything slower.


On top of this, I have a couple performance hints:

  1. You don't need the signature at all. crypto_box() is key exchange followed by authenticated encryption. If the recipient sucesfully decrypts a block, they know for a fact it came from someone who knew the server's private key. Adding a signature on top of that provides no benefit whatsoever. _Note that this voids my point (3) above: we don't care that you sign only the mac, because we don't care that you sign anything.)

  2. Using crypto_box() on every single block is a significant performance hit. There's no need for such overkill. What you want instead is separate it in two stages: first perform the key exchange with something like Monocypher's crypto_key_exchange(), then encrypt every block with that same key, using the random-then-incremented nonce you described. You can use Libsodium's crypto_secretbox() for this, or Monocypher's crypto_lock().


My suggestion for your use case:

Note my use of an ephemeral key to achieve some modicum of forward secrecy (if the server's key is lost past messages are still safe). It's a bit slower, but this only happens once at the beginning of the file, instead of once per block. Also, note how nonce management is simplified: thanks to the random shared secret, nonce reuse is no longer a problem, we can start at zero every time. We don't event have to transmit the nonce —but we do have to transmit the public half of the ephemeral key.

Now if you don't want to encrypt the file several times, there is a way:

Hope that helps, Loup.

nrclark commented 3 years ago

Just wanted to chime back in and say thanks for the write-up! It was informative and well-reasoned.

LoupVaillant commented 3 years ago

No problem, you're welcome. :slightly_smiling_face: