【问题标题】:OpenSSL C example of AES-GCM using EVP interfaces使用 EVP 接口的 AES-GCM 的 OpenSSL C 示例
【发布时间】:2012-08-22 14:12:34
【问题描述】:

对于AES-GCM加密/解密,我试过这个,但是有问题。

ctx     = EVP_CIPHER_CTX_new();

//Get the cipher.
cipher  = EVP_aes_128_gcm ();


#define     GCM_IV      "000000000000"
#define     GCM_ADD     "0000"
#define     TAG_SIZE    16
#define     ENC_SIZE    64


//Encrypt the data first.
//Set the cipher and context only.
retv    = EVP_EncryptInit (ctx, cipher, NULL, NULL);

//Set the nonce and tag sizes.
//Set IV length. [Optional for GCM].

retv    = EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_GCM_SET_IVLEN, strlen((const char *)GCM_IV), NULL);

//Now initialize the context with key and IV. 
retv    = EVP_EncryptInit (ctx, NULL, (const unsigned char *)keybuf, (const unsigned char *)GCM_IV);

//Add Additional associated data (AAD). [Optional for GCM]
retv    = EVP_EncryptUpdate (ctx, NULL, (int *)&enclen, (const unsigned char *)GCM_ADD, strlen(GCM_ADD));

//Now encrypt the data.
retv    = EVP_EncryptUpdate (ctx, (unsigned char *)encm, (int *)&enclen, (const unsigned char *)msg, _tcslen (msg) *sizeof(Char));

//Finalize.
retv    = EVP_EncryptFinal (ctx, (unsigned char *)encm + enclen, (int *)&enclen2);
enclen  += enclen2;


//Append authentication tag at the end.
retv    = EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_GCM_GET_TAG, TAG_SIZE, (unsigned char *)encm + enclen);

//DECRYPTION PART
//Now Decryption of the data.
//Then decrypt the data.
//Set just cipher.
retv    = EVP_DecryptInit(ctx, cipher, NULL, NULL);

//Set Nonce size.
retv    = EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_GCM_SET_IVLEN, strlen((const char *)GCM_IV), NULL);

//Set Tag from the data.
retv    = EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_GCM_SET_TAG, TAG_SIZE, (unsigned char *)encm + enclen);

//Set key and IV (nonce).
retv    = EVP_DecryptInit (ctx, NULL, (const unsigned char*)keybuf, (const unsigned char *)GCM_IV);

//Add Additional associated data (AAD).
retv    = EVP_DecryptUpdate (ctx, NULL, (int *)&declen, (const unsigned char *)GCM_ADD,
                             strlen((const char *)GCM_ADD));

//Decrypt the data.
retv    = EVP_DecryptUpdate (ctx, decm, (int *)&declen, (const unsigned char *)encm, enclen);


//Finalize.
retv    = EVP_DecryptFinal (ctx, (unsigned char*)decm + declen, (int *)&declen2);

这段代码运行良好(有一些修改)。它正在加密和解密消息。 问题是在解密之前修改密文时,它仍然解密文本(但是,错误)。 根据我对认证加密的理解,在这种情况下,它不应该解密修改后的密文。

我哪里错了? 我可以使用 OpenSSL 的 EVP 接口获得任何合适的 AES-GCM 示例吗?

【问题讨论】:

  • 仅供参考,您对 EVP_EncryptInit 和 EVP_DecryptInit 的重复调用会泄漏内存。

标签: c openssl aes aes-gcm


【解决方案1】:

以下是每次调用更新时加密和解密 128 字节的示例:

  int howmany, dec_success, len;
  const EVP_CIPHER *cipher;
  switch(key_len)
  {
  case 128: cipher  = EVP_aes_128_gcm ();break;
  case 192: cipher  = EVP_aes_192_gcm ();break;
  case 256: cipher  = EVP_aes_256_gcm ();break;
  default:break;
  }
  // Encrypt
  EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
  EVP_EncryptInit (ctx, cipher, KEY, IV);
  EVP_EncryptUpdate (ctx, NULL, &howmany, AAD, aad_len);
  len = 0;
  while(len <= in_len-128)
  {
     EVP_EncryptUpdate (ctx, CIPHERTEXT+len, &howmany, PLAINTEXT+len, 128);
     len+=128;
  }
  EVP_EncryptUpdate (ctx, CIPHERTEXT+len, &howmany, PLAINTEXT+len, in_len - len);
  EVP_EncryptFinal (ctx, TAG, &howmany);
  EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_GCM_GET_TAG, 16, TAG);  
  EVP_CIPHER_CTX_free(ctx);
  // Decrypt
  ctx = EVP_CIPHER_CTX_new();      
  EVP_DecryptInit (ctx, cipher, KEY, IV);
  EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_GCM_SET_TAG, 16, ref_TAG);
  EVP_DecryptInit (ctx, NULL, KEY, IV);
  EVP_DecryptUpdate (ctx, NULL, &howmany, AAD, aad_len);
  len = 0;
  while(len <= in_len-128)
  {
     EVP_DecryptUpdate (ctx, decrypted_CT+len, &howmany, CIPHERTEXT+len, 128);
     len+=128;
  }
  EVP_DecryptUpdate (ctx, decrypted_CT+len, &howmany, CIPHERTEXT+len, in_len-len);
  dec_success = EVP_DecryptFinal (ctx, dec_TAG, &howmany);
  EVP_CIPHER_CTX_free(ctx);

最后,您应该检查 dec_success 的值是否为 1。 如果修改 CIPHERTEXT,在解密之前,应该得到 0 的值。

【讨论】:

  • 这看起来很合乎逻辑。你能告诉我解密时它是如何在内部完成的吗?我知道如果我们有一些包含原始数据的散列值(md5 等)的摘要表达式可以完成,但是如果我们没有任何类型的有关原始数据的信息?
  • 在调用EVP_DecryptFinal之前调用EVP_DecryptUpdate之前使用EVP_CTRL_GCM_SET_TAG设置标签是否合法?有没有关于这个参数的官方文档?
【解决方案2】:

为现代性而编辑的答案:

您必须检查调用 EVP_DecryptFinal()(或 EVP_DecryptFinal_ex())的返回值,以确定您是否已成功解密密文。

OpenSSL 现在提供了一个功能完善的 AES GCM 示例,用 C 编写。它甚至包括测试向量。您可以在这里找到它https://github.com/openssl/openssl/blob/master/demos/evp/aesgcm.c 或搜索“openssl evp aesgcm.c”

5 年前的原始问题及其接受的答案显示了使用 EVP_*Init() 和 EVP_*Final() API 的代码。这些已被 EVP_*Init_ex() 和 EVP_*Final_ex() 弃用并取代,“因为它们可以重用现有上下文,而无需在每次调用时分配和释放它。” (openssl citation)

根据我的经验,如果您正在为这些调用编写包装函数,不要调用 EVP_EncryptUpdate_ex() 并为 AAD 设置 NULL 和 0。这可能在较新版本的 OpenSSL 中发生了变化,但截至 2013 年,它导致了加密失败。

这远远超出了这个问题的范围,但如果它对任何人都有帮助,这里有一个使用 OpenSSL API 的有效 iOS / Objective C 实现。

#include <openssl/rand.h>
#include <openssl/ecdsa.h>
#include <openssl/obj_mac.h>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/evp.h>

#define AES_256_KEY_LENGTH      32
#define AES_256_KEY_LENGTH_BITS 256
#define AES_256_IVEC_LENGTH     12
#define AES_256_GCM_TAG_LENGTH  16

// encrypt plaintext.
// key, ivec and tag buffers are required, aad is optional
// depending on your use, you may want to convert key, ivec, and tag to NSData/NSMutableData
+ (BOOL) aes256gcmEncrypt:(NSData*)plaintext
               ciphertext:(NSMutableData**)ciphertext
                      aad:(NSData*)aad
                      key:(const unsigned char*)key
                     ivec:(const unsigned char*)ivec
                      tag:(unsigned char*)tag {

    int status = 0;
    *ciphertext = [NSMutableData dataWithLength:[plaintext length]];
    if (! *ciphertext)
        return NO;

    // set up to Encrypt AES 256 GCM
    int numberOfBytes = 0;
    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
    EVP_EncryptInit_ex (ctx, EVP_aes_256_gcm(), NULL, NULL, NULL);

    // set the key and ivec
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, AES_256_IVEC_LENGTH, NULL);
    EVP_EncryptInit_ex (ctx, NULL, NULL, key, ivec);

    // add optional AAD (Additional Auth Data)
    if (aad)
        status = EVP_EncryptUpdate( ctx, NULL, &numberOfBytes, [aad bytes], [aad length]);

    unsigned char * ctBytes = [*ciphertext mutableBytes];
    EVP_EncryptUpdate (ctx, ctBytes, &numberOfBytes, [plaintext bytes], (int)[plaintext length]);
    status = EVP_EncryptFinal_ex (ctx, ctBytes+numberOfBytes, &numberOfBytes);

    if (status && tag) {
        status = EVP_CIPHER_CTX_ctrl (ctx, EVP_CTRL_GCM_GET_TAG, AES_256_GCM_TAG_LENGTH, tag);
    }
    EVP_CIPHER_CTX_free(ctx);
    return (status != 0); // OpenSSL uses 1 for success
}

// decrypt ciphertext.
// key, ivec and tag buffers are required, aad is optional
// depending on your use, you may want to convert key, ivec, and tag to NSData/NSMutableData
+ (BOOL) aes256gcmDecrypt:(NSData*)ciphertext
                plaintext:(NSMutableData**)plaintext
                      aad:(NSData*)aad
                      key:(const unsigned char *)key
                     ivec:(const unsigned char *)ivec
                      tag:(unsigned char *)tag {

    int status = 0;

    if (! ciphertext || !plaintext || !key || !ivec)
        return NO;

    *plaintext = [NSMutableData dataWithLength:[ciphertext length]];
    if (! *plaintext)
        return NO;

    // set up to Decrypt AES 256 GCM
    int numberOfBytes = 0;
    EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
    EVP_DecryptInit_ex (ctx, EVP_aes_256_gcm(), NULL, NULL, NULL);

    // set the key and ivec
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, AES_256_IVEC_LENGTH, NULL);
    status = EVP_DecryptInit_ex (ctx, NULL, NULL, key, ivec);

    // Set expected tag value. A restriction in OpenSSL 1.0.1c and earlier requires the tag before any AAD or ciphertext
    if (status && tag)
        EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, AES_256_GCM_TAG_LENGTH, tag);

    // add optional AAD (Additional Auth Data)
    if (aad)
        EVP_DecryptUpdate(ctx, NULL, &numberOfBytes, [aad bytes], [aad length]);

    status = EVP_DecryptUpdate (ctx, [*plaintext mutableBytes], &numberOfBytes, [ciphertext bytes], (int)[ciphertext length]);
    if (! status) {
        //DDLogError(@"aes256gcmDecrypt: EVP_DecryptUpdate failed");
        return NO;
    }
    EVP_DecryptFinal_ex (ctx, NULL, &numberOfBytes);
    EVP_CIPHER_CTX_free(ctx);
    return (status != 0); // OpenSSL uses 1 for success
}

【讨论】:

  • 你有处理文件的 github 代码吗?我很难让它发挥作用。
  • 您的代码似乎不正确。用EVP_CTRL_GCM_SET_TAG 拨打EVP_CIPHER_CTX_ctrl 的电话在哪里?另请参阅 OpenSSL wiki 上的 EVP Authenticated Encryption and Decryption
  • @jww 首先,这是加密,所以正确的迂腐问题应该是关于 EVP_CTRL_GCM_GET_TAG。其次,我也没有显示对 EVP_EncryptFinal_ex 的调用,您也没有抱怨!第三,我提供了完整功能示例代码的链接!这不是要点,是这样,我认为答案应该提供有用的信息,而不仅仅是代码块。不过,我是个好人,所以如果你需要,这里有一个完整的加密和解密实现:gist.github.com/eliburke/24f06a1590d572e86a01504e1b38b27f
  • @Eli - 你应该把你的代码和你的答案放在一起。就目前而言,您的代码在一个站点上,而代码注释在另一个站点上。您在引用 API 时遗漏了细节,但随后您尝试找到安全港,因为它们只是注释。另见Your answer is in another castle: when is an answer not an answer?Are link-only answers poor practice?
  • @jww 天哪,老兄,除了废话 4 年前的答案之外,你没有什么比这更好的事了?回顾一下:这个问题有一个公认的答案。我有一些关于不同平台/语言的附加信息,所以我发布了它。它甚至得到了一些支持,所以它对某人很有用。代码没有错,你只是犯了一个错误。没关系,它发生了。也许如果你想扔石头,你应该首先编辑你的整个历史以符合你当前的标准,因为我看到 2011-2014 年有很多 0 和否定排名的答案,有很多链接和不完整的代码 sn-ps。跨度>
【解决方案3】:

OpenSSL 有一个关于使用 AES-GCM 密码的不错的 wiki 页面。还提供了代码示例。该页面的链接是Authenticated_Decryption_using_GCM_mode

我关注了这个 wiki 并为 AES-GCM 进行了解密。下面复制代码段

int decrypt(unsigned char *ciphertext, int ciphertext_len, unsigned char *aad,
    int aad_len, unsigned char *tag, unsigned char *key, unsigned char *iv,
    unsigned char *plaintext)
{
    EVP_CIPHER_CTX *ctx;
    int len;
    int plaintext_len;
    int ret;

    /* Create and initialise the context */
    if(!(ctx = EVP_CIPHER_CTX_new())) handleErrors();

    /* Initialise the decryption operation. */
    if(!EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL))
        handleErrors();

    /* Set IV length. Not necessary if this is 12 bytes (96 bits) */
    if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 16, NULL))
        handleErrors();

    /* Initialise key and IV */
    if(!EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv)) handleErrors();

    /* Provide any AAD data. This can be called zero or more times as
     * required
     */
    if(!EVP_DecryptUpdate(ctx, NULL, &len, aad, aad_len))
        handleErrors();

    /* Provide the message to be decrypted, and obtain the plaintext output.
     * EVP_DecryptUpdate can be called multiple times if necessary
     */
    if(!EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, ciphertext_len))
        handleErrors();
    plaintext_len = len;

    /* Set expected tag value. Works in OpenSSL 1.0.1d and later */
    if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag))
        handleErrors();

    /* Finalise the decryption. A positive return value indicates success,
     * anything else is a failure - the plaintext is not trustworthy.
     */
    ret = EVP_DecryptFinal_ex(ctx, plaintext + len, &len);

    /* Clean up */
    EVP_CIPHER_CTX_free(ctx);

    if(ret > 0)
    {
        /* Success */
        plaintext_len += len;
        return plaintext_len;
    }
    else
    {
        /* Verify failed */
        return -1;
    }
}

另外,正如人们所指出的,您应该检查从 EVP_DecryptFinal_ex() 返回的值。如果你的密文稍作修改,仍然可以解密,但最终返回值不会为真,因为无法验证身份验证标签(或mac)。

【讨论】:

    【解决方案4】:

    OpenSSL 不负责身份验证。您应该检查EVP_DecryptFinal 的返回值。如果为 1 则解密数据的认证 TAG 等于你提供的 TAG。

    如果标签不同,则应将解密数据视为伪造而丢弃。 如果标签相等,那么数据就ok了。

    由于身份验证是增量的,并且可能需要多次调用 Update,因此必须先解密数据才能完成身份验证。

    【讨论】:

    • 感谢您的回答。能否提供一个示例或代码sn-ps?
    • 当整个数据都在一个缓冲区中时,我没有问题。但我的问题是我有大量数据要加密,无法容纳在单个缓冲区中。我需要多个 IO 操作来读写。你能建议如何在这种情况下实现这种行为吗?
    • -1:GCM 是一种经过身份验证的加密模式。身份验证内置于模式中。如果数据被篡改,或者标签(认证标签)被篡改,OpenSSL 会返回错误。
    • 你不觉得这个目的应该由其他一些像md5这样的哈希函数来确保密文在解密前是否被修改过吗?
    猜你喜欢
    • 2015-12-19
    • 2015-01-06
    • 2015-10-03
    • 2014-09-11
    • 1970-01-01
    • 2020-06-07
    • 1970-01-01
    • 2015-02-18
    • 1970-01-01
    相关资源
    最近更新 更多