1
$\begingroup$

Imagine this scenario:

  • On a particular PC is a service that provides cryptographic functions -- in particular AES-CBC and ECC (ECIES/ECDSA).
  • The service provides access to a single key stored in an HSM -- the key itself is never visible to any software on the PC (including the service itself).
  • Copying the service software to another PC will not provide access to the same key, since the HSM doesn't follow.
  • However any app running on the same PC can request that the service perform arbitrary cryptography using the same key (there is no significant authentication, other than being on the same PC). (There might be a password to initially "unlock" the service after reboot, but for the purposes of this question assume it's already unlocked.)

Problems with the above aside, if an app wants to be able to "securely" encrypt data (such that another app on the same PC with access to the same cryptography service cannot decrypt it), am I correct that a reasonable way to do so would be something like this?

  • Encrypting:
    • Generate random nonce.
    • Hash/encrypt nonce with some hard-coded fixed internal (semi-)secret (not using service).
    • Use encrypted nonce as IV for data encryption via service.
    • Write unencrypted nonce to file along with encrypted data.
  • Decrypting:
    • Read unencrypted nonce and ciphertext from file.
    • Recover encrypted nonce via hash/encrypt using the same internal secret.
    • Decrypt ciphertext using encrypted nonce as IV.

The theory being that this should be as secure as the secret itself is (and more secure against attackers without access to the service).

And which would be better, salt+hashing the nonce or encrypting it, or some other function?

Another possible variation would be to use the original nonce as IV but (reversibly) encrypt it before writing to the file. Though I've assumed that's either no better or slightly worse than the above.

$\endgroup$

1 Answer 1

3
$\begingroup$

No. CBC decryption has a neat feature that allows recovery from errors. enter image description here

Not having the IV is essentially an error and only corrupts the first block. So if an attacker decrypts using an all zero IV, they don't get the first block (since they don't know the secret IV to XOR it with) but every following block comes out just fine.

Note:you also need authentication

You should be authenticating the ciphertext. Your existing proposal talks about encrypting the data but an attacker could modify the ciphertext at rest which your application won't detect. CBC encryption for example lets you flip bits in the plaintext (with some caveats). Can your application handle mangled plaintext safely?

https://en.wikipedia.org/wiki/Authenticated_encryption

One easy way to compute an authentication tag is:

  • Tag=Hash(ciphertext+tag_key)
    • tag_key is derived using the application key and an HSM operation
    • EG:tag_key=hash(application_key+HSM_encrypt(nonce)+bytes("tag_key"))
  • truncate to whatever length you want

Note that the tag key goes AFTER the ciphertext to avoid length extension attacks. using an HMAC is an option but not really nessesary.

Before decrypting recompute the hash and check equality but do it constant time. If you don't have a constant time compare lying around, do randomised_compare(a,b)=(Hash(compare_secret+A)==Hash(compare_secret+B)). Compare secret can be derived from your application key or a random value. It just needs to be a secret.

This makes it impossible to do a timing attack to find the correct MAC tag for a mangled ciphertext.

Alternatives

Use the HSM only as part of key derivation

If you switch around the operations and derive the secret key using the HSM and do the cryptography in software it works fine. Use the HSM to derive the per-message key.

I'd suggest using crypto_secretbox_easy from the libsodium library.

My suggested solution is as follows:

Setup:

  • the application chooses two long term secrets secret_A and secret_B

To generate the per-message key:

  • choose a 128 bit nonce randomly. (you can choose an initial random value and count from there if you like)
  • hsm_challenge=sha256(nonce+secret_A)[:16]
  • hsm_response=HSM_encrypt_AES_CBC(hsm_challenge, IV=0)
  • message_key=sha256(hsm_response + secret_B)
  • ciphertext=crypto_secretbox_easy(message,nonce=0,key=message_key)
  • return nonce+ciphertext

To decrypt:

  • split the message into nonce and ciphertext
  • derive the message_key the same way as was done during encryption
  • message=crypto_secretbox_open_easy(ciphertext,nonce=0,key=message_key)`
    • check for verification failure (function returns -1) return message

The key derivation prevents an attacker from knowing what to ask the HSM if they have a given message unless they know the value of secret_A. So they can't capture a bunch of encrypted messages ask the HSM for some encryptions and then complete the decryption after later stealing the application keys.

Use the HSM encryption with a tweak

If you do need a construction that will do what you're looking for but uses the HSM for bulk cryptography, you can apply the Even-Mansour mode of operation to the block cipher inside the HSM CBC implementation, treating the keyed AES block cipher as a publicly known pseudorandom permutation.

1 key Evan-Mansour

If the attacker can't do chosen a chosen plaintext attack on the resulting block cipher ... say because you're using an unpredictable IV along with CBC encryption, a single static key for all messages should be fine but does degrade security if lots of messages are sent. Security is (2^128)/(number of blocks encrypted) so you'd lose log2(num_blocks) bits of security. Deriving a new key per message is very cheap so no reason not to.

Setup:

  • Generate an authentication key K_tag
    • EG:`K_tag=SHA256(application_key+bytes("K_tag"))

To encrypt:

  • Generate a 16 byte message key K_msg
    • Derive A from the application key and nonce
    • EG:K_msg=SHA256(application_key+nonce+bytes("K_msg"))[:16]
  • XOR the first plaintext block with K_msg
  • CBC encrypt the result with IV=K_msg
    • this is equivalent to XORing the first block with K_msg and using IV=0
  • XOR all ciphertext blocks with K_msg
  • calculate the authentication tag
    • EG:tag=SHA256(IV+ciphertext+K_tag)
  • output ciphertext+truncate(tag,TAG_BYTE_LENGTH)
    • the authentication tag can be 64 bits or something, you don't need the whole hash

To decrypt, do the same thing in reverse order, check the tag is valid before anything else though.

Make sure all plaintexts are padded to integer block lengths so the HSM doesn't do ciphertext stealing that will mess this up.

$\endgroup$
5
  • $\begingroup$ Thanks for the response. I was actually thinking about generating a random message key/IV for the majority of the encryption and using the shared key only to encrypt that key/IV, such that the shared key will only actually be used for one or two block messages, not longer. Does that improve things? $\endgroup$
    – Miral
    May 28, 2023 at 22:59
  • $\begingroup$ I oversimplified things a bit in the question. I actually have one "home" HSM and two "satellite" HSMs. The two satellites have different keys. The home has both of the satellite keys. I want to encrypt on the home such that either of the two satellites can decrypt. Hence why thinking of doing bulk encryption in software with a message key and then encrypting the message key with each of the satellite HSM keys. This was a separate aspect to the part actually posed in the question re: also having some secret sauce such that a different app with access to the satellite HSM can't decrypt. $\endgroup$
    – Miral
    May 28, 2023 at 23:29
  • $\begingroup$ Bulk encryption in software is the way to go. I've updated the section "Use the HSM only as part of key derivation" with a good suggestion for how to do all this. libsodium is easy to integrate, secure, and foolproof. Messing around with CBC is a bad idea. The key insight here is that instead of encrypting a randomly chosen key so the other side can recover it, you can instead derive the message key using a response from the HSM. This simplifies all the code and the message format. $\endgroup$ May 28, 2023 at 23:57
  • $\begingroup$ I don't think your suggested method will work, given two different HSM key targets. I don't want two different message keys for the bulk encryption. Your pre-edit suggestion sounded more workable. $\endgroup$
    – Miral
    May 29, 2023 at 0:06
  • $\begingroup$ Missed the "such that either of the two satellites can decrypt" part. No issues, have both HSMs derive their own per message key. The base station uses the first one and sends along an offset (key1 xor key2) as well. The first station ignores it, the second xors it's derived key with the offset to get the actual message key. You can obviously send along more than one such offset. $\endgroup$ May 29, 2023 at 6:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.