使用EVP接口的AES-GCM的OpenSSL C示例

OpenSSL C example of AES-GCM using EVP interfaces

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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的任何合适示例吗?


这是一个示例,该示例在每次调用更新时加密和解密128个字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  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值。


为现代而编辑的答案:

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

OpenSSL现在提供了用C编写的AES GCM的功能完善的示例。它甚至包括测试向量。您可以在这里找到它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引文)

以我的经验,如果您正在为这些调用编写包装函数,请不要使用NULL和AAD的0调用EVP_EncryptUpdate_ex()。在更新版本的OpenSSL中可能已更改,但从2013年开始,它导致加密失败。

对于这个问题,这已经远远超出了范围,但是如果它可以帮助任何人,这是使用OpenSSL API的有效的iOS / Objective C实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#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
}


OpenSSL在使用AES-GCM密码方面有一个不错的Wiki页面。还提供了代码示例。该页面的链接为Authenticated_Decryption_using_GCM_mode

我按照此Wiki进行了AES-GCM的解密。代码段复制到下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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)。


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

如果标签不同,则应将伪造的解密数据丢弃。
如果标签相等,则数据正常。

由于身份验证是增量式的,并且可以对Update进行多次调用,因此必须先解密数据,然后才能完成身份验证。