No. CBC decryption has a neat feature that allows recovery from errors.
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.
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.