mbedtls AES-CBC加密实战:详解IV的三大安全陷阱与正确实践 1. 项目概述当AES-CBC遇上mbedtlsIV的“魔鬼”藏在细节里在嵌入式开发、物联网设备安全乃至后端服务中AES高级加密标准加密是保护数据机密性的基石。而CBC密码分组链接模式因其良好的安全特性和广泛支持成为最常用的分组密码工作模式之一。mbedtls前身PolarSSL作为一个轻量级、可移植的加密库是资源受限环境下的首选。然而我见过太多项目代码里写着“AES-256-CBC加密”开发者自信满满却因为一个看似简单的“初始化向量”IV处理不当导致整个加密体系形同虚设。攻击者甚至无需破解密钥就能轻松篡改或窥探你的“加密”数据。这绝不是危言耸听。今天我们就聚焦于使用mbedtls实现AES-CBC模式时关于IV的三个最常见、也最危险的错误并给出经过实战检验的正确姿势。无论你是正在调试一个加密通信模块的嵌入式工程师还是负责设计API安全的后端开发理解这些细节是确保你的加密真正“安全”而非“心理安慰”的第一步。2. 核心概念重温为什么CBC模式离不开一个“好”的IV在深入错误之前我们必须彻底理解IV在CBC模式中的核心作用。这不仅仅是“需要填一个16字节的数组”那么简单。2.1 CBC模式的工作原理与IV的角色AES本身是一个分组密码算法它一次处理一个固定长度如128位16字节的数据块。原始数据明文通常远长于一个块因此需要一种“模式”来将多个块串联起来加密CBC就是其中一种。CBC模式的核心思想是“链接”每一个明文块在加密前会先与前一个密文块进行异或XOR操作。对于第一个块没有“前一个密文块”这个角色就由IV来扮演。公式很简单密文块[n] AES_Encrypt(密钥 明文块[n] XOR 密文块[n-1])其中密文块[-1]就是IV。IV的核心价值在于“随机化”。如果没有IV或者每次使用相同的IV那么相同的明文块比如数据包开头固定的协议头“LOGIN:”始终会生成相同的密文块。攻击者通过观察密文就能识别出模式甚至发起“重放攻击”或“字典攻击”。一个随机、不可预测的IV确保了即使完全相同的明文被加密多次产生的密文也完全不同这被称为“语义安全”。2.2 mbedtls中AES-CBC的相关接口在mbedtls中AES-CBC加解密通常涉及以下关键函数和结构体mbedtls_aes_context: 用于保存AES密钥扩展后的轮密钥。mbedtls_aes_setkey_enc/dec(): 设置加密或解密用的密钥。mbedtls_aes_crypt_cbc(): 执行CBC模式的加密或解密操作。其函数原型清晰地揭示了IV的角色int mbedtls_aes_crypt_cbc( mbedtls_aes_context *ctx, int mode, // MBEDTLS_AES_ENCRYPT 或 MBEDTLS_AES_DECRYPT size_t length, // 输入数据的长度必须是16的倍数 unsigned char iv[16], // 初始化向量既是输入也是输出 const unsigned char *input, unsigned char *output );请注意iv这个参数在加密时它作为输入提供函数执行后它的内容会被修改通常变为最后一个密文块。在解密时它同样作为输入通常是来自加密端的IV或上一个密文块函数内部也会修改它。这个“输入且会被修改”的特性正是许多错误的根源。3. 错误一静态IV或硬编码IV——为攻击者打开一扇窗这是最经典、也最不该犯的错误。3.1 错误示例与潜在风险// 错误示例静态IV unsigned char static_iv[16] {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}; void encrypt_data(const unsigned char* key, const unsigned char* plain, size_t len, unsigned char* cipher) { mbedtls_aes_context aes; mbedtls_aes_init(aes); mbedtls_aes_setkey_enc(aes, key, 256); // 假设使用256位密钥 // 每次加密都使用相同的IV unsigned char iv[16]; memcpy(iv, static_iv, 16); // 将静态IV复制到本地 mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_ENCRYPT, len, iv, plain, cipher); mbedtls_aes_free(aes); }风险分析假设你加密的消息是“USERadminCMDREAD”和“USERguestCMDREAD”。由于IV相同、密钥相同、明文前缀相同这两个消息加密后的前几个字节会高度相似。攻击者通过收集大量密文可以进行频率分析。更危险的是如果协议允许用户输入部分明文如“USER”之后的内容攻击者可以构造特定明文通过观察密文变化来推断加密内容这接近于“选择明文攻击”的场景。3.2 正确姿势使用密码学安全的随机数生成器CSPRNGIV必须是每次加密时唯一且不可预测的。这意味着你需要一个可靠的随机源。#include mbedtls/entropy.h #include mbedtls/ctr_drbg.h mbedtls_ctr_drbg_context ctr_drbg; mbedtls_entropy_context entropy; // 初始化随机数生成器通常在程序启动时做一次 int init_random() { mbedtls_entropy_init(entropy); mbedtls_ctr_drbg_init(ctr_drbg); // 使用一个个性化的字符串作为种子源增加唯一性 const char* personalization my_app_aes_iv_2024; return mbedtls_ctr_drbg_seed(ctr_drbg, mbedtls_entropy_func, entropy, (const unsigned char*)personalization, strlen(personalization)); } void safe_encrypt(const unsigned char* key, const unsigned char* plain, size_t len, unsigned char* cipher, unsigned char* output_iv) { mbedtls_aes_context aes; mbedtls_aes_init(aes); mbedtls_aes_setkey_enc(aes, key, 256); // 生成随机IV unsigned char iv[16]; if (mbedtls_ctr_drbg_random(ctr_drbg, iv, 16) ! 0) { // 处理错误随机数生成失败是严重的安全事件 mbedtls_aes_free(aes); return; } // 保存IV需要和密文一起传输给接收方 if (output_iv) { memcpy(output_iv, iv, 16); } // 注意这里需要将明文填充到16字节的倍数。mbedtls_aes_crypt_cbc本身不负责填充。 // 假设len已经是填充后的长度例如使用PKCS#7填充。 unsigned char local_iv[16]; memcpy(local_iv, iv, 16); // 复制IV因为mbedtls_aes_crypt_cbc会修改iv参数 mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_ENCRYPT, len, local_iv, plain, cipher); mbedtls_aes_free(aes); }注意mbedtls_ctr_drbg_random是线程安全的吗在mbedtls中mbedtls_ctr_drbg_context本身不是线程安全的。如果多线程使用每个线程应该有自己的上下文或者在使用全局上下文时加锁。更简单的做法是使用操作系统提供的安全随机源如Linux下的/dev/urandom或Windows的BCryptGenRandom但mbedtls的抽象层提供了更好的可移植性。4. 错误二IV传输或管理不当——安全链中最脆弱的一环生成了一个完美的随机IV但如果传输或存储不当一切归零。IV不需要保密但必须保证完整性和与密文的正确关联。4.1 错误示例IV与密文分离或篡改场景1忘记发送IV。解密端使用默认值如全零解密结果得到乱码还以为是密钥错了。场景2IV被篡改。CBC模式对IV的篡改有“有趣的”影响解密后的第一个明文块只有被篡改的比特位会出错。攻击者可以精心构造IV使得解密出的第一个块变成他想要的内容例如将加密的“转账给A 100元”中的“A”改为“B”而后面的所有块解密完全正常接收方如果只校验后面的数据比如MAC就可能被绕过。4.2 正确姿势IV与密文绑定传输并使用认证加密AEAD1. 绑定传输最朴素也最必要的方式就是将IV和密文作为一个整体消息发送。常见的格式是消息 IV (16字节) 密文。接收方先读取前16字节作为IV剩余部分作为密文进行解密。// 发送方封装 size_t total_len 16 cipher_len; // IV长度 密文长度 unsigned char *message malloc(total_len); memcpy(message, iv, 16); memcpy(message 16, cipher, cipher_len); // 发送 message 和 total_len // 接收方解封装 unsigned char received_iv[16]; unsigned char *received_cipher message 16; size_t received_cipher_len total_len - 16; // 使用 received_iv 解密 received_cipher2. 使用认证加密AEAD这是治本的方法。CBC模式本身只提供机密性不提供完整性。现代密码学实践强烈推荐使用如AES-GCM或AES-CCM这样的认证加密模式。这些模式在加密的同时会生成一个认证标签Tag接收方可以验证密文和IV在GCM中称为Nonce是否被篡改。mbedtls同样提供了完善的GCM/CCM支持。// 使用AES-GCM的示例强烈推荐替代CBC #include mbedtls/gcm.h void encrypt_gcm(const unsigned char* key, const unsigned char* plain, size_t plain_len, const unsigned char* additional_data, size_t add_len, unsigned char* cipher, unsigned char* tag, unsigned char* iv) { mbedtls_gcm_context gcm; mbedtls_gcm_init(gcm); mbedtls_gcm_setkey(gcm, MBEDTLS_CIPHER_ID_AES, key, 256); // 生成12字节的Nonce类似IV但GCM对它的要求不同 mbedtls_ctr_drbg_random(ctr_drbg, iv, 12); mbedtls_gcm_crypt_and_tag(gcm, MBEDTLS_GCM_ENCRYPT, plain_len, iv, 12, additional_data, add_len, plain, cipher, 16, tag); // 生成16字节的tag mbedtls_gcm_free(gcm); } // 解密时使用 mbedtls_gcm_auth_decrypt 进行验证和解密一步到位安全无忧。实操心得如果因为兼容性等原因必须使用CBC那么务必在加密后对 (IV 密文) 整体计算一个HMAC并将HMAC值一起发送。解密方先验证HMAC通过后再解密。这相当于手动为CBC模式添加了完整性保护。记住这个公式CBC HMAC 机密性 完整性。但最好还是直接迁移到GCM。5. 错误三加解密流程中IV的复用与混淆——状态污染的陷阱这个错误更隐蔽发生在代码的流程控制中尤其是涉及加解密上下文复用或者错误处理时。5.1 错误示例IV缓冲区复用污染void process_message(mbedtls_aes_context *aes, unsigned char* iv, const unsigned char* in, unsigned char* out, int is_encrypt) { // 假设aes上下文和iv缓冲区是外部传入并可能被多次使用的 int mode is_encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT; // 致命错误直接使用传入的iv指针mbedtls_aes_crypt_cbc会修改它的内容。 mbedtls_aes_crypt_cbc(aes, mode, len, iv, in, out); // 当这个函数被连续调用处理多个数据块时第二次调用传入的iv已经是第一次调用被修改后的值即上一个密文块。 // 对于加密这破坏了CBC的链式逻辑对于解密这将导致完全错误的解密结果。 }5.2 正确姿势严格管理IV的生命周期与拷贝核心原则传递给mbedtls_aes_crypt_cbc的iv参数你应该假设它在函数返回后已被破坏不再是最初的值。1. 加密时生成或获取一个随机IV。在调用加密函数前立即将该IV拷贝到一个临时缓冲区用这个临时缓冲区作为函数参数。将原始的IV未修改的保存下来用于后续传输。void correct_encrypt_step(mbedtls_aes_context *aes, const unsigned char* original_iv, const unsigned char* plain, size_t len, unsigned char* cipher) { unsigned char working_iv[16]; memcpy(working_iv, original_iv, 16); // 关键步骤使用副本 mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_ENCRYPT, len, working_iv, plain, cipher); // original_iv 保持不变可用于传输或记录 }2. 解密时从接收到的数据中提取出发送方使用的IV。同样在调用解密函数前将该IV拷贝到一个临时缓冲区。使用临时缓冲区进行解密操作。void correct_decrypt_step(mbedtls_aes_context *aes, const unsigned char* received_iv, const unsigned char* cipher, size_t len, unsigned char* plain) { unsigned char working_iv[16]; memcpy(working_iv, received_iv, 16); // 关键步骤使用副本 mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_DECRYPT, len, working_iv, cipher, plain); }3. 流式处理处理超长数据分块进行如果需要加密一个很大的文件无法一次读入内存需要分块调用mbedtls_aes_crypt_cbc。这时IV的传递就变成了链式传递。对于加密第一次调用使用随机IV之后每次调用使用**上一次调用输出的iv缓冲区即上一个密文块**作为下一次调用的输入IV。mbedtls_aes_crypt_cbc的设计正好支持这一点——它修改传入的iv使其指向最后一个密文块。对于解密过程类似但方向相反。第一次调用使用传输过来的IV之后使用上一次调用输入的密文块作为下一次的IV。这里要格外小心缓冲区管理确保传入的iv和input指针指向正确的数据。// 流式加密伪代码示例 unsigned char iv[16]; generate_random_iv(iv); unsigned char block[BLOCK_SIZE]; unsigned char cipher_block[BLOCK_SIZE]; while (read_data(block, BLOCK_SIZE)) { unsigned char temp_iv[16]; memcpy(temp_iv, iv, 16); // 使用当前IV的副本 mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_ENCRYPT, BLOCK_SIZE, temp_iv, block, cipher_block); write_data(cipher_block, BLOCK_SIZE); memcpy(iv, temp_iv, 16); // 更新IV为当前最后一个密文块用于下一轮 } // 最终最初的随机IV需要单独保存和传输。6. 完整实战一个带HMAC的AES-CBC安全封装示例将以上所有正确姿势结合起来我们实现一个相对安全的AES-CBC加密解密辅助函数。它包含随机IV生成、IV与密文绑定、以及可选的HMAC-SHA256完整性验证。#include mbedtls/aes.h #include mbedtls/sha256.h #include mbedtls/entropy.h #include mbedtls/ctr_drbg.h #include string.h #define AES_KEY_SIZE 32 // 256位 #define AES_BLOCK_SIZE 16 #define HMAC_KEY_SIZE 32 // HMAC密钥建议与AES密钥长度一致但需独立生成 #define HMAC_OUT_SIZE 32 // SHA-256输出32字节 // 全局随机数生成器上下文需初始化 extern mbedtls_ctr_drbg_context g_ctr_drbg; // PKCS#7填充 static int pkcs7_pad(unsigned char* buf, size_t buf_len, size_t data_len) { if (data_len buf_len) return -1; int pad_len buf_len - data_len; memset(buf data_len, pad_len, pad_len); return 0; } static int pkcs7_unpad(const unsigned char* buf, size_t buf_len, size_t* data_len) { if (buf_len 0) return -1; int pad_len buf[buf_len - 1]; if (pad_len 0 || pad_len AES_BLOCK_SIZE || pad_len buf_len) return -1; // 简单验证填充字节是否正确生产环境需要更严格的验证 for (int i 0; i pad_len; i) { if (buf[buf_len - 1 - i] ! pad_len) return -1; } *data_len buf_len - pad_len; return 0; } // 安全加密函数 // 输出格式: HMAC(32字节) IV(16字节) 填充后的密文 int safe_cbc_encrypt(const unsigned char* aes_key, const unsigned char* hmac_key, const unsigned char* plain, size_t plain_len, unsigned char* output, size_t* output_len) { // 1. 计算填充后长度 size_t padded_len ((plain_len / AES_BLOCK_SIZE) 1) * AES_BLOCK_SIZE; size_t total_len HMAC_OUT_SIZE AES_BLOCK_SIZE padded_len; // HMAC IV Cipher if (*output_len total_len) return -1; // 缓冲区检查 *output_len total_len; unsigned char* hmac_pos output; unsigned char* iv_pos output HMAC_OUT_SIZE; unsigned char* cipher_pos iv_pos AES_BLOCK_SIZE; // 2. 生成随机IV if (mbedtls_ctr_drbg_random(g_ctr_drbg, iv_pos, AES_BLOCK_SIZE) ! 0) { return -2; } // 3. 准备填充明文 unsigned char padded_plain[padded_len]; memcpy(padded_plain, plain, plain_len); if (pkcs7_pad(padded_plain, padded_len, plain_len) ! 0) { return -3; } // 4. 执行AES-CBC加密 mbedtls_aes_context aes; mbedtls_aes_init(aes); if (mbedtls_aes_setkey_enc(aes, aes_key, AES_KEY_SIZE * 8) ! 0) { mbedtls_aes_free(aes); return -4; } unsigned char working_iv[AES_BLOCK_SIZE]; memcpy(working_iv, iv_pos, AES_BLOCK_SIZE); // 使用IV副本 if (mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_ENCRYPT, padded_len, working_iv, padded_plain, cipher_pos) ! 0) { mbedtls_aes_free(aes); return -5; } mbedtls_aes_free(aes); // 5. 计算HMAC(IV || Cipher) mbedtls_sha256_context sha256; unsigned char hmac_input[AES_BLOCK_SIZE padded_len]; memcpy(hmac_input, iv_pos, AES_BLOCK_SIZE); memcpy(hmac_input AES_BLOCK_SIZE, cipher_pos, padded_len); mbedtls_sha256_init(sha256); // 使用HMAC密钥进行SHA256计算简化版实际应用应使用mbedtls_md_hmac系列函数 // 这里为演示先计算 key ^ ipad/opad 的标准HMAC流程简化起见我们直接使用mbedtls_md_hmac // 假设我们有一个hmac_sha256函数 if (hmac_sha256(hmac_key, HMAC_KEY_SIZE, hmac_input, AES_BLOCK_SIZE padded_len, hmac_pos) ! 0) { return -6; } // 实际代码应替换为mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), ...) return 0; // 成功 } // 安全解密函数 int safe_cbc_decrypt(const unsigned char* aes_key, const unsigned char* hmac_key, const unsigned char* input, size_t input_len, unsigned char* plain, size_t* plain_len) { // 1. 检查最小长度 size_t min_len HMAC_OUT_SIZE AES_BLOCK_SIZE AES_BLOCK_SIZE; if (input_len min_len || (input_len - HMAC_OUT_SIZE - AES_BLOCK_SIZE) % AES_BLOCK_SIZE ! 0) { return -1; // 长度无效 } const unsigned char* hmac_pos input; const unsigned char* iv_pos input HMAC_OUT_SIZE; const unsigned char* cipher_pos iv_pos AES_BLOCK_SIZE; size_t cipher_len input_len - HMAC_OUT_SIZE - AES_BLOCK_SIZE; // 2. 验证HMAC unsigned char computed_hmac[HMAC_OUT_SIZE]; if (hmac_sha256(hmac_key, HMAC_KEY_SIZE, iv_pos, AES_BLOCK_SIZE cipher_len, computed_hmac) ! 0) { return -2; } if (memcmp(hmac_pos, computed_hmac, HMAC_OUT_SIZE) ! 0) { return -3; // HMAC验证失败数据可能被篡改**绝对不要继续解密** } // 3. 执行AES-CBC解密 mbedtls_aes_context aes; mbedtls_aes_init(aes); if (mbedtls_aes_setkey_dec(aes, aes_key, AES_KEY_SIZE * 8) ! 0) { mbedtls_aes_free(aes); return -4; } unsigned char padded_plain[cipher_len]; unsigned char working_iv[AES_BLOCK_SIZE]; memcpy(working_iv, iv_pos, AES_BLOCK_SIZE); // 使用IV副本 if (mbedtls_aes_crypt_cbc(aes, MBEDTLS_AES_DECRYPT, cipher_len, working_iv, cipher_pos, padded_plain) ! 0) { mbedtls_aes_free(aes); return -5; } mbedtls_aes_free(aes); // 4. 去除填充 size_t unpadded_len; if (pkcs7_unpad(padded_plain, cipher_len, unpadded_len) ! 0) { return -6; // 填充错误可能是错误的密钥或数据损坏 } if (*plain_len unpadded_len) { return -7; // 输出缓冲区不足 } *plain_len unpadded_len; memcpy(plain, padded_plain, unpadded_len); return 0; // 成功 }这个示例将IV管理、完整性验证和错误处理结合在一起。请注意其中的hmac_sha256函数需要你根据mbedtls的HMAC API实现。核心的安全理念是先验MAC后解密。一旦HMAC校验失败立即返回错误不执行任何解密操作这可以抵御填充Oracle攻击的变种。7. 常见问题与排查技巧实录在实际集成和调试中你肯定会遇到各种诡异的问题。下面是我踩过坑后总结的一些排查思路。7.1 解密失败乱码或填充错误这是最高频的问题。请按以下清单逐项核对密钥一致吗加解密双方使用的密钥必须完全一样字节对字节。检查密钥的生成、存储、传输环节。是不是一个用了十六进制字符串另一个用了原始字节是不是不小心截断了IV一致吗解密端使用的IV必须是加密端生成的那个随机IV并且没有被修改。确认传输格式是IV 密文并且解密端正确地从数据流中剥离出了前16字节作为IV。数据对齐和填充吗mbedtls_aes_crypt_cbc要求输入数据长度是16字节的整数倍。你的明文在加密前填充了吗使用的是PKCS#7吗解密后去除填充的逻辑正确吗一个常见的错误是明文本身长度就是16的倍数按照PKCS#7标准需要额外填充一个完整的16字节块值全部为0x10。很多自制填充函数会漏掉这种情况。加密模式和解密模式匹配吗确认调用mbedtls_aes_crypt_cbc时mode参数是否正确。加密用MBEDTLS_AES_ENCRYPT解密用MBEDTLS_AES_DECRYPT。同时解密时必须使用mbedtls_aes_setkey_dec设置密钥而不是mbedtls_aes_setkey_enc。缓冲区污染吗检查是否有数组越界、指针错误导致IV或密文缓冲区在函数调用前后被意外修改。使用调试器或打印十六进制日志仔细比对。7.2 性能问题与优化建议在资源紧张的嵌入式设备上AES-CBC加解密可能成为性能瓶颈。启用硬件加速许多现代MCU如STM32系列、NXP i.MX RT带有硬件AES加速器。mbedtls通过MBEDTLS_AES_ALT宏和相应的底层硬件驱动接口来支持。启用后性能会有数量级的提升。务必查阅芯片手册和mbedtls移植指南。减少内存拷贝上面示例中出于安全 clarity我们进行了多次memcpy。在性能敏感且确保缓冲区不会意外被修改的场景下可以谨慎地减少拷贝。例如如果生成的IV立刻用于加密且之后不再需要可以直接将其作为iv参数传入但需清楚它会被修改。上下文复用对于需要加密大量数据的场景初始化mbedtls_aes_context并设置密钥mbedtls_aes_setkey_enc/dec是一次性开销。应该复用这个上下文而不是每次加密/解密都重新初始化和设置密钥。7.3 调试与日志记录技巧当问题出现时系统的日志是你的第一手资料。十六进制转储编写一个简单的函数将关键缓冲区密钥、IV、明文块、密文块以十六进制形式打印出来。对比加密端和解密端的这些值差异点往往就是问题所在。void hex_dump(const char* label, const unsigned char* buf, size_t len) { printf(%s (%zu bytes):\n, label, len); for (size_t i 0; i len; i) { printf(%02X , buf[i]); if ((i 1) % 16 0) printf(\n); } printf(\n); } // 在加密后调用hex_dump(Generated IV, iv, 16); hex_dump(Ciphertext, cipher, cipher_len); // 在解密前调用hex_dump(Received IV, received_iv, 16); hex_dump(Received Cipher, received_cipher, cipher_len);检查返回值mbedtls的所有函数几乎都有返回值。永远不要忽略它们mbedtls_aes_setkey_enc,mbedtls_aes_crypt_cbc,mbedtls_ctr_drbg_random这些函数的返回值都指示了成功0或特定的错误码。在生产代码中必须进行错误处理。单元测试为你的加密/解密函数编写单元测试覆盖边界情况空数据、刚好一个块的数据、非块对齐的数据、重复加密相同数据等。使用已知的测试向量可以从NIST标准文档或mbedtls测试套件中找来验证你的实现是否正确。7.4 从CBC迁移到GCM的注意事项如果你决定听从建议从CBCHMAC迁移到更现代的AES-GCM需要注意以下几点Nonce类似IV长度GCM通常使用12字节的Nonce而不是CBC的16字节。Nonce同样需要唯一性但可以是一个计数器。附加认证数据AADGCM允许你加密一些不需要保密但需要验证完整性的数据如协议头、消息序号。这是一个非常有用的特性善用它。认证标签Tag加密后会生成一个Tag通常16字节必须将其和密文一起传输。解密时先验证Tag失败则中止。接口变化GCM的接口与CBC不同是mbedtls_gcm_crypt_and_tag和mbedtls_gcm_auth_decrypt。仔细阅读文档理解每个参数的含义。性能在无硬件加速的平台上GCM可能比CBC略慢因为它除了加密还进行了GMAC运算。但在有AES和GHASH硬件加速的平台上这不再是问题。其带来的安全性提升是绝对值得的。加密无小事IV虽小却是CBC模式安全大厦的基石。希望这篇从错误到正确、从原理到实战的梳理能帮你扫清mbedtls AES-CBC使用路上的陷阱。在实际项目中多一份对细节的审慎就少一份系统被攻破的风险。