carloscn/blog

3.3_Security_对称密钥算法之AEAD

carloscn opened this issue · 0 comments

3.3_Security_对称密钥算法之AEAD

简单的说,把3.0_Security_对称密钥算法加解密3.2_Security_对称密钥算法之MAC(CMAC/HMAC)结合在一起,就是AEAD。全称为:Authenticated Encryption (AE) and Authenticated Encryption with Associated Data (AEAD)

对于AEAD的编程接口:

  • 加密:
    • 输入:明文、密钥、header(未加密,可选);
    • 输出:密文和MAC(tag)
  • 解密:
    • 输入:密文,密钥,tag,header(在加密的时候使用的,可选)
    • 输出:明文,验证tag的结果,如果有header也有验证header的结果;

注意,header的目的是提供认证和完整性的保护,应用场景在网络和存储的metadata,是对不需要保证安全性但是需要认证功能的数据。

常用的AEAD包含:

1. 安全背景

对称加密算法只能提供保密性,MAC只能提供认证的功能,如果想让两个结合,那么就需要人工的使用加密算法及MAC,这种组合是多种多样的,并没有一个完善的标准规定,而且易错(error prone)。大量的攻击可以侵入,由于加密+认证错误的实现。大概2000年的时候,Charanjit Jutla,这个人集成了CBC模式的加密与认证混合的工具IAPM mode。在2009年,又逐步的定义了6种认证加密模式:

在2013年,CAESAR competition 推行认证加密模式。在2015年,ChaCha20-Poly1305被加入到GCM中。

至此,一种又可以加密又可以保证完整性的算法工具诞生了。

2. AEAD

单纯的对称加密算法,其解密步骤是无法确认密钥是否正确的。也就是说,加密后的数据可以用任何密钥执行解密运算,得到一组疑似原始数据,而不知道密钥是否是正确的,也不知道解密出来的原始数据是否正确。因此,需要在单纯的加密算法之上,加上一层验证手段,来确认解密步骤是否正确。简单地把加密算法和认证算法组合,可以实现上述目的,并由此产生了几个方案:

  • EtM (Encryption then MAC)
  • E&M (Encryption and MAC)
  • MtE (MAC then Encryption)

2.1 AEAD前身

2.1.1 (old) Encrypt-then-MAC (EtM)

先加密,然后对密文进行 MAC 运算(一般用各种 HMAC),把二者拼接起来,发给接收方。接收方先验证 MAC,如果验证通过,则证明密钥是正确的,然后执行解密运算。

参考: RFC-7366.

Various EtM ciphersuites exist for SSHv2 as well,(e.g., hmac-sha1-etm@openssh.com).

2.1.2 (old) Encrypt-and-MAC (E&M)

同时对原始数据执行加密和 MAC 运算,把二者拼接起来,发给接收方。接收方先进行解密,然后对解密结果执行 MAC 运算,比对发来的 MAC,验证正确性。

Used in, e.g., SSH

2.1.3 (old) MAC-then-Encrypt (MtE)

与 EtM 相反,先对原始数据执行 MAC 运算,与原始数据拼接后,执行加密算法,将密文发送给接收方。接受方先进行解密,然后执行 MAC 运算,验证解密结果是否正确。

AEAD is used in SSL/TLS

2.2 AEAD

业内逐渐意识到以上通过组合加密和认证算法来实现 AEAD 的方案都是有安全问题的。从 2008 年起,业内开始提出,需要在一个算法在内部同时实现加密和认证。基于以上三个方法的**,一些新的算法被提出,这些算法被称为真正的 AEAD 算法。

  • AES-128-GCM
  • AES-192-GCM
  • AES-256-GCM
  • ChaCha20-IETF-Poly1305
  • XChaCha20-IETF-Poly1305

在具备 AES 加速的 CPU(桌面,服务器)上,建议使用 AES-XXX-GCM 系列,移动设备建议使用 ChaCha20-IETF-Poly1305 系列。在设计加密系统的时候,请务必选用 AEAD算法,抛弃旧的 MtE,EtM,E&M方案

2.2.1 GCM

GCM原理

GCM 全称为 Galois/Counter Mode (GCM),可以看出 G 是指 GMAC,C 是指 CTR。它在 CTR 加密的基础上增加 GMAC 的特性,解决了 CTR 不能对加密消息进行完整性校验的问题。

GCM 加密所需数据:明文 P、加密密钥 Key、初始向量 IV、附加消息 F。

GCM 加密步骤如下

  • 将 P 分为 P1、P2、…、Pn,Px 长度 <= 128
  • 生成累加计数器 c0、c1、c2、…、cn,由密钥 Key 计算出密钥 H
  • 将 IV、c0 进行运算(连接、加和、异或等)得到 IV_c0,用 Key 加密 IV_c0 得到 IVC0
  • 将 IV、c1 进行运算(连接、加和、异或等)得到 IV_c1,用 Key 加密 IV_c1 得到 IVC1,将 IVC1、P1 做异或运算得到 C1,用密钥 H 通过 GMAC 算法将附加消息 F 计算出 F1, F1 与 C1 做异或运算得到 FC1
  • 将 IV、c2 进行运算(连接、加和、异或等)得到 IV_c2,用 Key 加密 IV_c2 得到 IVC2,将 IVC2、P2 做异或运算得到 C2,用密钥 H 通过 GMAC 算法将附加消息 FC1 计算出 F2, F2 与 C2 做异或运算得到 FC2
  • 将 IV、cn 进行运算(连接、加和、异或等)得到 IV_cn,用 Key 加密 IV_cn 得到 IVCn,将 IVCn、Pn 做异或运算得到 Cn,用密钥 H 通过 GMAC 算法将附加消息 FC(n-1) 计算出 Fn, Fn 与 Cn 做异或运算得到 FCn
  • 拼接 C1、…Cn 得到密文 C,用密钥 H 通过 GMAC 算法结合 FCn 和 IVC0 最终计算出 MAC

For any given key and initialization vector combination, GCM is limited to encrypting 239−256 bits of plain text (64 GiB)

参考:AES Galois Counter Mode (GCM) Cipher Suites for TLS

需要注意的是,GCM能够使用128/192/256的key长度,并且总以128bits作为块长度。 参考: https://crypto.stackexchange.com/questions/77750/why-gcm-operation-mode-with-aes-128-is-recomended-and-can-we-use-aes-192-and-aes

GCM mbedtls库

void setup() {
	  mbedtls_gcm_context aes;
	  char *key = "abcdefghijklmnop";
	  char *input = "Mark C's ESP32 GCM Example code!";
	  char *iv = "abababababababab";
	  unsigned char output[64] = {0};
	  unsigned char fin[64] = {0};
	  Serial.println("[i] Encrypted into buffer:");
	  
	  // init the context...
	  mbedtls_gcm_init( &aes );
	  // Set the key. This next line could have CAMELLIA or ARIA as our GCM mode cipher...
	  mbedtls_gcm_setkey( &aes,MBEDTLS_CIPHER_ID_AES , (const unsigned char*) key, strlen(key) * 8);
	  // Initialise the GCM cipher...
	  mbedtls_gcm_starts(&aes, MBEDTLS_GCM_ENCRYPT, (const unsigned char*)iv, strlen(iv),NULL, 0);
	  // Send the intialised cipher some data and store it...
	  mbedtls_gcm_update(&aes,strlen(input),(const unsigned char*)input, output);
	  // Free up the context.
	  mbedtls_gcm_free( &aes );
	  
	  for (int i = 0; i < strlen(input); i++) {  
	    char str[3];
	    sprintf(str, "%02x", (int)output[i]);
	    Serial.print(str);
	  }
	  
	  Serial.println("[i] Decrypted from buffer:");
	  mbedtls_gcm_init( &aes );
	  mbedtls_gcm_setkey( &aes,MBEDTLS_CIPHER_ID_AES , (const unsigned char*) key, strlen(key) * 8);
	  mbedtls_gcm_starts(&aes, MBEDTLS_GCM_DECRYPT, (const unsigned char*)iv, strlen(iv),NULL, 0);
	  mbedtls_gcm_update(&aes,64,(const unsigned char*)output, fin);
	  mbedtls_gcm_free( &aes );
}

参考: https://gist.github.com/unprovable/892a677d672990f46bca97194ae549bc

2.2.2 ChaCha20-IETF-Poly1305

ChaCha20-Poly1305是Google所采用的一种新式加密算法,性能强大,在CPU为精简指令集的ARM平台上尤为显著(ARM v8前效果较明显),在同等配置的手机中表现是AES的4倍(ARM v8之后加入了AES指令,所以在这些平台上的设备,AES方式反而比chacha20-Poly1305方式更快,性能更好),可减少加密解密所产生的数据量进而可以改善用户体验,减少等待时间,节省电池寿命等。谷歌选择了ChaCha20和伯恩斯坦的Poly1305消息认证码取代过去一直在互联网安全领域使用的基于OpenSSL的RC4密码,谷歌最初是为了保证能够在Android手机上的Chrome浏览器和谷歌网站间的HTTPS(TLS/SSL)通讯。在谷歌采用TLS不久后,ChaCha20和Poly1305均用在OpenSSH中的ChaCha20-Poly1305新算法中,这使得OpenSSH可能避免因编译时间对OpenSSL产生依赖。ChaCha20还用于OpenBSD(一种多平台类UNIX操作系统)中的RC4随机数生成器,在DragonFlyBSD中作为内核的伪随机数产生器(Cryptographically Secure Pseudo-Random Number Generator,简称CSPRNG)的子程序。

ChaCha20-Poly1305是由ChaCha20流密码Poly1305消息认证码(MAC)结合的一种应用在互联网安全协议中的认证加密算法,由Google公司率先在Andriod移动平台中的Chrome中代替RC4使用。由于其算法精简、安全性强、兼容性强等特点,目前Google致力于全面将其在移动端推广。

Daniel J. Bernstein教授,德裔美籍数学家、密码学家、程序设计师。Eindhoven University of Technology的数学与计算机教授,设计了salsa、chacha等著名的流密码算法和Poly1305消息认证码,对国际密码学界影响深远。

参考: https://datatracker.ietf.org/doc/html/rfc8439

ChaCha20-IETF-Poly1305原理

chacha20 过程如下:

加密参数

输入:

输入项 长度(Bytes) 说明
key 32 共享秘钥
iv 12 干扰项,每次不同
AAD N 关联数据
plaintext N 待加密数据明文

输出:

输出项 长度(Bytes) 说明
ciphertext N 加密后的密文,长度与原始明文一致
TAG 16 认证标签
解密参数

输入:

输入项 长度(Bytes) 说明
key 32 共享秘钥
iv 12 干扰项,每次不同
AAD N 关联数据
ciphertext N 待加密数据明文
TAG 16 认证标签

输出:

输出项 长度(Bytes) 说明
plaintext N 原始明文
result 1 完整性检查结果(1成功,0失败)

代码示例

int chachapoly_test(void)
{
	/*  Key 共享秘钥 */
    unsigned char skey2[32] = {
    0x2e,0xff,0xe4,0x85,0x1e,0x23,0x72,0xef,
    0x5c,0x44,0x14,0x75,0x61,0xd8,0xf0,0xa3,
    0xde,0x91,0x09,0x00,0x24,0x03,0x51,0x3c,
    0xf2,0xf6,0x6d,0x16,0xbd,0x78,0xd2,0x63};

    int ret = 0;
    EVP_CIPHER_CTX* ctx = NULL;
    EVP_CIPHER_CTX* dctx = NULL;
	/*  干扰项 iv */
    unsigned char iv[12] = {
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b
    };
	/*  外部关联数据 AAD */
    unsigned char aad[128] = 
    {0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10,0x11,0x12,0x13,0x14,0x15,
     0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1f,0x20,0x21,0x22,0x23,0x24,0x25,0x26,
     0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10,0x11,0x12,0x13,0x14,0x15,
     0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1f,0x20,0x21,0x22,0x23,0x24,0x25,0x26,
     0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10,0x11,0x12,0x13,0x14,0x15,
     0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1f,0x20,0x21,0x22,0x23,0x24,0x25,0x26,
     0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f,0x10,0x11,0x12,0x13,0x14,0x15,
     0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1f,0x20,0x21,0x22,0x23,0x24,0x25,0x26};
    unsigned char ciphertext[1024] = {0};
    unsigned char res[1024] = {0};
    
    /* 原始明文 plaintext */
    unsigned char msg[] = "0123456789abcdefghijklmnopqrstuvwxyz";
    ctx = EVP_CIPHER_CTX_new();

    ret = EVP_EncryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL, skey2, iv);
    printf("EncryptInit ret: %d\n", ret);

    int outlen, finallen, reslen;
	/* 加密输入 ADD */
    ret = EVP_EncryptUpdate(ctx, NULL, &outlen, aad, 32);
    ret = EVP_EncryptUpdate(ctx, NULL, &outlen, aad+64, 32);
    printf("Ret: %d Update AAD len: %d\n", ret, outlen);
	/* 加密原始数据 */
    ret = EVP_EncryptUpdate(ctx, ciphertext, &outlen, msg, 37);
    printf("Ret: %d Update len: %u\n", ret, outlen);
    print_strhex(ciphertext, outlen);
    
    ret = EVP_EncryptFinal(ctx, ciphertext, &finallen);
    printf("Ret: %d Final len: %u\n", ret, finallen);
    outlen += finallen;
    //print_strhex(ciphertext, outlen);

    /* 生成认证标签TAG */
    unsigned char* tag_data = ciphertext + outlen;
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, tag_data);
    outlen += 16;
    print_strhex(ciphertext, outlen);



    dctx = EVP_CIPHER_CTX_new();
    //iv[0] = 0x0f;
    //skey2[0] = 0xff;
    //tag_data[0] = 0x01;
    //aad[0] = 0x01;
    ret = EVP_DecryptInit_ex(dctx, EVP_chacha20_poly1305(), NULL, skey2, iv);
    printf("DecryptInit ret: %d\n", ret);

    ret = EVP_CIPHER_CTX_ctrl(dctx, EVP_CTRL_GCM_SET_TAG, 16, tag_data);
    printf("Ret: %d CTX set TAG\n", ret);
    
    ret = EVP_DecryptUpdate(dctx, NULL, &reslen, aad, 32);
    ret = EVP_DecryptUpdate(dctx, NULL, &reslen, aad+64, 32);
    printf("Ret: %d Update AAD len: %d\n", ret, reslen);

    ret = EVP_DecryptUpdate(dctx, res, &reslen, ciphertext, outlen - 16);
    printf("Ret: %d DecryUpdate once len: %u\n", ret, reslen);
    int totallen = reslen;

    ret = EVP_DecryptFinal(dctx, res, &reslen);
    printf("Ret: %d DecryptFinal len: %u\n", ret, reslen);
    //reslen += finallen;

    //print_strhex(res, totallen);
    printf("%s\n", res);
}