Reputation: 243
As far as I understand, using a wrong key (with the correct size) to decrypt something with AES-CBC
should just output some garbage. CBC doesn't have any sort of MAC, so you really can only look at the results of the decryption and decide for yourself if that is the plaintext you want.
However, when decrypting with SubtleCrypto, a wrong key will cause an OperationError
, but a wrong ciphertext will not, and neither will a wrong IV. I would have expected all these three cases to have similar behaviours.
How is it possible for the implementation to know that the key was wrong and not any of the other inputs? Do keys have to have a specific structure, other than the size? In that case, the key space would be smaller than the advertised bit length of the key, no?
async function simpleCryptoTest() {
// all zeroes plaintext, key and IV
const iv = new ArrayBuffer(16)
const key = new ArrayBuffer(32)
const plaintext = new ArrayBuffer(64)
const algorithm = {name: 'AES-CBC'};
const correctCryptoKey = await crypto.subtle.importKey('raw', key, algorithm, false, ['encrypt', 'decrypt'])
const ciphertext = await crypto.subtle.encrypt({...algorithm, iv: iv}, correctCryptoKey, plaintext)
console.log("ciphertext", ciphertext)
const decryptedCorrect = crypto.subtle.decrypt({...algorithm, iv: iv}, correctCryptoKey, ciphertext)
const wrongCiphertext = new Uint8Array(ciphertext)
wrongCiphertext[0] = ~ciphertext[0] // flipping the first byte should be enough
const decryptedWrongCiphertext = crypto.subtle.decrypt({...algorithm, iv: iv}, correctCryptoKey, wrongCiphertext)
const wrongIv = new Uint8Array(iv)
wrongIv[0] = 1 // we know the correct IV is all zeroes
const decryptedWrongIv = crypto.subtle.decrypt({...algorithm, iv: wrongIv}, correctCryptoKey, ciphertext)
const wrongKey = new Uint8Array(key)
wrongKey[0] = ~key[0]
const decryptedWrongKey = crypto.subtle.importKey('raw', wrongKey, algorithm, false, ['decrypt']).then((wrongCryptoKey) => {
return crypto.subtle.decrypt({...algorithm, iv: iv}, wrongCryptoKey, ciphertext)
})
const results = await Promise.allSettled([decryptedCorrect, decryptedWrongCiphertext, decryptedWrongIv, decryptedWrongKey])
console.log("decrypted with the correct key", results[0])
console.log("decrypted with corrupted ciphertext", results[1])
console.log("decrypted with corrupted IV", results[2])
console.log('decrypted with the wrong key', results[3])
}
simpleCryptoTest()
/*
decrypted with the correct key → {status: "fulfilled", value: ArrayBuffer(64)}
decrypted with corrupted ciphertext → {status: "fulfilled", value: ArrayBuffer(64)}
decrypted with corrupted IV → {status: "fulfilled", value: ArrayBuffer(64)}
decrypted with the wrong key → {status: "rejected", reason: DOMException} // e.name == 'OperationError'
*/
Please note that I am aware that CBC has no authentication, and I am aware that GCM exists. I need CBC because I am implementing a variation of the Signal Protocol, which I most certainly do not intend to roll out in production without a proper crypto review. Thanks :-)
Also, I tested this on Firefox 77.0.1 and Chromium 83.0.4103.97 for Linux.
Upvotes: 0
Views: 1027
Reputation: 41967
There is no MAC, but there is padding. I'm not very familiar with WebCrypto but chances are you are using PKCS7 padding in your encryption algorithm specification -- either explicitly or by default. The padding bytes added to the end of the plaintext have the value k k ... k, where k is number of padding bytes needed, 1 <= k <= 16. Upon decryption, a check is made if the last byte k is in the range specified, and if the last k bytes are equal to k. If that check fails then something has gone wrong and the OperationError is returned.
Now, as for corrupted IV and corrupted ciphertext, the reason it works is a "feature" of CBC mode. If you look carefully at the diagram of the decrypt direction of CBC mode you'll note that following facts (remember, this is on decryption):
Therefore, try changing the ciphertext block before the last block and you should see your OperationError. However, the padding check is no substitute for a real MAC, and even with a corrupted key or last or next-to-last ciphertext block there is still a decent chance that the padding check will succeed. If the last byte of the final decrypted block equals 1 then the padding check succeeds. This probability of this is 1/256 for the corrupted items listed. (It is actually a little higher because if the last two bytes are equal 2, or the last 3 bytes are equal to 3,... etc., then the padding check also succeeds). So as an experiment try changing two bytes of the key about 500 or so times and you should 1 or 2 instances where the decryption succeeds without error.
Upvotes: 1