在 WebCrypto 中使用 AES-CBC 时,为什么错误的密钥会导致 OperationError 但错误的密文或 IV 不会?

When using AES-CBC in WebCrypto, why does a wrong key cause an OperationError but a wrong ciphertext or IV don't?

据我所知,使用错误的密钥(具有正确的大小)来解密带有 AES-CBC 的东西应该只会输出一些垃圾。 CBC没有任何类型的MAC,所以你真的只能看看解密的结果,然后自己决定这是否是你想要的明文。

但是用SubtleCrypto解密时,错误的密钥会导致OperationError,但错误的密文不会,错误的IV也不会。我原以为这三种情况都有相似的行为。

实现如何知道密钥错误而不是任何其他输入?除了大小之外,键是否必须具有特定结构?在那种情况下,密钥 space 将小于密钥的广告位长度,不是吗?

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'
*/

请注意,我知道 CBC 没有身份验证,而且我知道 GCM 存在。我需要 CBC,因为我正在实施 Signal Protocol 的变体,我肯定不打算在没有适当的加密审查的情况下将其投入生产。谢谢:-)

此外,我在 Firefox 77.0.1 和 Chromium 83.0.4103.97 上测试了 Linux。

没有MAC,但是有padding。我对 WebCrypto 不是很熟悉,但很可能您在加密算法规范中使用了 PKCS7 填充——无论是显式的还是默认的。添加到明文末尾的填充字节的值为 k k ... k,其中 k 是所需的填充字节数,1 <= k <= 16。解密时,将检查最后一个字节 k 是否在指定的范围,以及最后 k 个字节是否等于 k。如果该检查失败,则说明出现问题并返回 OperationError。

现在,对于损坏的 IV 和损坏的密文,它起作用的原因是 "feature" CBC 模式。如果您仔细查看 diagram of the decrypt direction of CBC mode,您会注意到以下事实(请记住,这是关于解密的):

  1. 损坏的 IV 仅影响明文的第一个块。其余全部解密正确
  2. 损坏的密文仅影响当前明文块和下一个块。正确解密前后的所有块。

因此,尝试更改最后一个块之前的密文块,您应该会看到您的 OperationError。然而,填充检查不能替代真正的 MAC,即使密钥损坏或最后一个或倒数第二个密文块,填充检查仍有很大的机会成功。如果最终解密块的最后一个字节等于 1,则填充检查成功。对于列出的损坏项目,此概率为 1/256。 (它实际上有点高,因为如果最后两个字节等于 2,或者最后 3 个字节等于 3,...等等,那么填充检查也会成功)。因此,作为一个实验,尝试将密钥的两个字节更改大约 500 次左右,您应该有 1 或 2 次解密成功且没有错误的实例。