AES-128 CBC 模式下加密流末尾的 Crypto++ 额外块

Crypto++ extra block at the end of encrypted stream in AES-128 CBC mode

我正在尝试在 CBC 模式下使用 AES-128 加密 320 字节的二进制数据,并将密码存储到文件中。输出文件应该是 320 字节,但我得到了 336 字节。这是我的代码:

#include <iostream>
#include <fstream>
#include <crypto++/aes.h>
#include <crypto++/modes.h>
#include <crypto++/base64.h>
#include <crypto++/sha.h>
#include <cryptopp/osrng.h>
#include <crypto++/filters.h>
#include <crypto++/files.h>

namespace CryptoPP
{
    using byte = unsigned char;
}

void myAESTest()
{
    std::string password = "testPassWord";

    // hash the password string
    // -------------------------------
    CryptoPP::byte key[CryptoPP::AES::DEFAULT_KEYLENGTH], iv[CryptoPP::AES::BLOCKSIZE];
    CryptoPP::byte passHash[CryptoPP::SHA256::DIGESTSIZE];
    CryptoPP::SHA256().CalculateDigest(passHash, (CryptoPP::byte*) password.data(), password.size());
    std::memcpy(key, passHash, CryptoPP::AES::DEFAULT_KEYLENGTH);
    std::memcpy(iv, passHash+CryptoPP::AES::DEFAULT_KEYLENGTH, CryptoPP::AES::BLOCKSIZE);

    // encrypt
    // ---------------------------------
    int chunkSize = 20*CryptoPP::AES::BLOCKSIZE;

    CryptoPP::CBC_Mode<CryptoPP::AES>::Encryption encryptor;
    encryptor.SetKeyWithIV(key, sizeof(key), iv);

    std::ofstream testOut("./test.enc", std::ios::binary);
    CryptoPP::FileSink outSink(testOut);

    CryptoPP::byte message[chunkSize];

    CryptoPP::StreamTransformationFilter stfenc(encryptor, new CryptoPP::Redirector(outSink));

    for(int i = 0; i < chunkSize; i ++)
    {
        message[i] = (CryptoPP::byte)i;
    }

    stfenc.Put(message, chunkSize);
    stfenc.MessageEnd();
    testOut.close();

    // decrypt
    // ------------------------------------
    // Because of some unknown reason increase chuksize by 1 block
    // chunkSize+=16;

    CryptoPP::byte cipher[chunkSize], decrypted[chunkSize];

    CryptoPP::CBC_Mode<CryptoPP::AES>::Decryption decryptor;
    decryptor.SetKeyWithIV(key, sizeof(key), iv);

    std::ifstream inFile("./test.enc", std::ios::binary);
    inFile.read((char *)cipher, chunkSize);

    CryptoPP::ArraySink decSink(decrypted, chunkSize);
    CryptoPP::StreamTransformationFilter stfdec(decryptor, new CryptoPP::Redirector(decSink));

    stfdec.Put(cipher, chunkSize);
    stfdec.MessageEnd();
    inFile.close();

    for(int i = 0; i < chunkSize; i++)
    {
        std::cout << (int)decrypted[i] << ' ';
    }
    std::cout << std::endl;
}

int main(int argc, char* argv[]) 
{
    myAESTest();
    return 0;
}

我无法理解最后 16 个字节是如何生成的。如果我选择忽略解密中的最后 16 个字节,CryptoPP 会抛出 CryptoPP::InvalidCiphertext 错误:

terminate called after throwing an instance of 'CryptoPP::InvalidCiphertext'
  what():  StreamTransformationFilter: invalid PKCS #7 block padding found

I'm not able to understand how the last 16 bytes are generated. If I choose to ignore the last 16 bytes in the decryption, Crypto++ throws InvalidCiphertext error

最后16个字节是填充。填充由 StreamTransformationFilter 过滤器添加;请参阅手册中的 StreamTransformationFilter Class Reference。虽然不是很明显,DEFAULT_PADDINGPKCS_PADDING 对于 ECB_ModeCBC_Mode。对于其他模式如 OFB_ModeCTR_Mode,它是 NO_PADDING

您只需为加密和解密过滤器指定NO_PADDING。但是,您必须确保明文和密文是块大小的倍数,对于 AES 是 16。

您可以通过切换到另一种模式来避开块大小限制,例如 CTR_Mode。但是你必须非常小心密钥或 IV 重用,由于你使用的密码派生方案,这可能很困难。

所以代替:

CBC_Mode<AES>::Encryption encryptor;
...
StreamTransformationFilter stfenc(encryptor, new Redirector(outSink));

使用:

CBC_Mode<AES>::Encryption encryptor;
...
StreamTransformationFilter stfenc(encryptor, new Redirector(outSink), NO_PADDING);

另请参阅维基上的 CBC_Mode on the Crypto++ wiki. You might also be interested in Authenticated Encryption


为此,您还可以:

#ifndef CRYPTOPP_NO_GLOBAL_BYTE
namespace CryptoPP
{
    using byte = unsigned char;
}
#endif

CRYPTOPP_NO_GLOBAL_BYTE定义在C++17 std::byte fixes之后。如果未定义 CRYPTOPP_NO_GLOBAL_BYTE,则 byte 位于全局命名空间中(Crypto++ 5.6.5 及更早版本)。如果定义了 CRYPTOPP_NO_GLOBAL_BYTE,则 byteCryptoPP 命名空间中(Crypto++ 6.0 及更高版本)。


为此:

std::ofstream testOut("./test.enc", std::ios::binary);
FileSink outSink(testOut);

您还可以这样做:

FileSink outSink("./test.enc");

为此:

SHA256().CalculateDigest(passHash, (byte*) password.data(), password.size());
std::memcpy(key, passHash, AES::DEFAULT_KEYLENGTH);
std::memcpy(iv, passHash+AES::DEFAULT_KEYLENGTH, AES::BLOCKSIZE);

您可以考虑使用 HKDF 作为推导函数。使用一个密码,但使用两个不同的标签,以确保独立派生。一个标签可能是字符串 "AES key derivation version 1",另一个标签可能是 "AES iv derivation version 1".

该标签将用作 DeriveKeyinfo 参数。你只需要调用它两次,一次调用 key,一次调用 iv.

unsigned int DeriveKey (byte *derived, size_t derivedLen,
    const byte *secret, size_t secretLen,
    const byte *salt, size_t saltLen,
    const byte *info, size_t infoLen) const

secret是密码。如果您有 salt 则使用它。否则 HKDF 使用默认盐。

另请参阅 Crypto++ wiki 上的 HKDF


最后,关于这个:

You can sidestep the blocksize restriction by switching to another mode like CTR_Mode. But then you have to be very careful of key or IV reuse, which may be difficult due to the password derivation scheme you are using.

您也可以考虑使用集成加密方案,例如 Elliptic Curve Integrated Encryption Scheme. It is IND-CCA2,这是一个强大的安全概念。您需要的一切都被打包到加密方案中。

在 ECIES 下,每个用户都会得到一个 public/private 密钥对。然后,使用一个大的随机秘密作为 AES 密钥、iv 和 mac 密钥的种子。明文经过加密和验证。最后,种子在用户的 public 密钥下加密。密码还是用的,不过是用来解密私钥的