ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

EVASH Ultra EEPROM AES加密算法示例:从密钥存储到加解密验证的完整配置

EVASH Ultra EEPROM AES加密算法示例:从密钥存储到加解密验证的完整配置 1. 为什么要在 EVASH Ultra EEPROM 上做 AES 加密存储EVASH Ultra EEPROM 是一类面向嵌入式场景的非易失存储器件常见容量从几 KB 到几百 KB接口以 I2C 和 SPI 为主工作电压覆盖 1.8V 到 5.5V。它的定位很明确给 MCU 提供一块掉电不丢数据的小容量存储区用来放设备序列号、校准参数、License、用户配置、传感器标定值这些东西。问题也恰恰出在这里——这些数据里往往混着敏感信息比如设备密钥、Wi-Fi 凭据、支付令牌、医疗设备的患者参数。如果直接明文写进 EEPROM任何人把芯片焊下来、用编程器一读数据就全暴露了。我见过不少项目硬件工程师觉得「EEPROM 在板子内部别人拿不到」于是把密钥明文写进去。等到产品做安全认证或者被客户做渗透测试时这一条直接判不合格。EVASH Ultra EEPROM 本身不提供加密引擎它只是一块存储介质加密这件事必须由 MCU 侧的软件来完成。所以「EVASH Ultra EEPROM AES 加密算法示例」这个需求本质上是问怎么在资源受限的嵌入式环境里把 AES 加解密和 EEPROM 读写串成一个可靠的闭环。AES 是对称分组密码分组固定 128 位16 字节密钥长度支持 128、192、256 位。嵌入式里最常用的是 AES-128因为它在安全性和算力开销之间比较平衡。AES-256 更安全但密钥调度和轮运算的耗时大约是 AES-128 的 1.4 倍对主频只有几十 MHz 的 MCU 来说这个差距在频繁加解密时是能感知的。至于分组模式ECB 模式简单但相同明文块会产生相同密文块泄露数据模式不建议用于结构化数据CBC 模式需要 IV初始化向量每个块加密前先和前一块密文异或安全性好很多是嵌入式里最常用的选择CTR 模式把分组密码变成流密码可以并行、不需要填充适合大数据量但 IV 绝对不能重复。这篇文章面向的是正在用 EVASH Ultra EEPROM 做产品、需要把敏感数据加密落盘的嵌入式开发者。我会给出一套可以直接复制改造的 C 代码覆盖密钥写入、AES 参数设置、数据加解密、读写校验四个环节并且把常见的坑比如 401 类鉴权失败、地址越界、IV 复用都拆开讲清楚。如果你手上正好有 EVASH Ultra EEPROM 的样片和一块 STM32 或者 ESP32跟着做一遍就能跑通。需要说明的是AES 库的选择很关键。裸机环境常用 tiny-AES-c、mbedTLS 的 AES 模块或者芯片厂商 SDK 自带的硬件加密外设。如果你的 MCU 有 AES 硬件加速比如 STM32 的 CRYP 外设、ESP32 的 AES 加速器优先用硬件速度快一个数量级而且不容易被侧信道攻击。下面示例我用 tiny-AES-c 的接口风格来写因为它足够小、可移植几乎任何平台都能编译。2. EVASH Ultra EEPROM 与 AES 密钥的前置准备在写代码之前有几件事必须先定下来否则后面会反复返工。第一是密钥从哪来。绝对不要在源码里硬编码uint8_t aes_key[16] {0x01, 0x23, ...}这种密钥一旦固件被 dump 出来就等于没有。正确做法是产线阶段用安全烧录工具把密钥写进 MCU 的 OTP 区或者安全 Flash 区运行时从那里读或者用 MCU 的硬件唯一 IDUID配合一个设备级盐值通过 HKDF 派生出每台设备不同的密钥。EVASH Ultra EEPROM 里存的应该是「被加密后的业务数据」而不是密钥本身。如果非要在 EEPROM 里存密钥那至少要再套一层用 MCU 内部密钥加密后再存形成两级保护。第二是 EEPROM 的地址规划。EVASH Ultra EEPROM 的擦写寿命通常在 100 万次量级但这是按页Page算的不是按字节。频繁改写同一地址会加速局部磨损。所以规划时要把「频繁写的区域」和「只写一次的区域」分开。密钥、设备证书这类只写一次的数据放在低地址区运行日志、计数器这类频繁更新的数据放在高地址区并且做磨损均衡wear leveling。下面示例里我用0x0000存加密后的敏感数据0x0100存 IV0x0200存校验用的 HMAC 或 CRC。第三是 AES 参数。我建议用 AES-128-CBC PKCS#7 填充。原因CBC 能隐藏明文模式PKCS#7 填充规则简单嵌入式实现容易。IV 必须是随机数每次加密都换绝对不能复用。IV 不需要保密可以明文存在 EEPROM 里但必须和密文一一对应。如果你用 CTR 模式IV 就变成了计数器复用会导致灾难性的密钥流复用两段密文异或就能还原明文这个坑一定要避开。第四是数据长度。AES 分组是 16 字节明文长度必须是 16 的整数倍不够就填充。EVASH Ultra EEPROM 的页大小常见是 32 字节或 64 字节写的时候要按页对齐跨页写需要拆成多次。读的时候没有对齐要求但建议也按页读减少 I2C/SPI 事务次数。下面这张表把关键参数列清楚方便你对照自己的硬件改参数推荐值说明AES 密钥长度128 位嵌入式平衡之选有硬件加速可上 256分组模式CBC需要 IV隐藏明文模式填充方式PKCS#7明文补到 16 字节整数倍IV 长度16 字节每次加密随机生成明文存储EEPROM 数据区0x0000 起存密文EEPROM IV 区0x0100 起存 IVEEPROM 校验区0x0200 起存 CRC32 或 HMAC如果你在开发过程中需要调用云端大模型来辅助生成测试向量、校验 AES 实现是否正确可以用 TaoToken 的模型对话能力做交叉验证把加密前后的十六进制贴进去让它帮你比对。接入方式在下一节讲。3. 可复制的 AES 加解密与 EEPROM 读写配置这一节是核心我给出完整的 C 代码按「密钥准备 → IV 生成 → 加密 → 写 EEPROM → 读 EEPROM → 解密 → 校验」的顺序组织。代码基于 tiny-AES-c 的 API 风格你可以直接替换成自己平台的库函数。先看头文件和宏定义。EEPROM 的读写函数我用evash_eeprom_write和evash_eeprom_read表示你需要替换成 EVASH 官方 SDK 或者你自己写的 I2C/SPI 驱动。#include stdint.h #include string.h #include aes.h // tiny-AES-c 或你的 AES 库 #include evash_eeprom.h // EVASH Ultra EEPROM 驱动 #define AES_KEY_LEN 16 #define AES_BLOCK_LEN 16 #define DATA_ADDR 0x0000 #define IV_ADDR 0x0100 #define CRC_ADDR 0x0200 #define MAX_DATA_LEN 64 static uint8_t g_aes_key[AES_KEY_LEN]; // 从安全区读取密钥这里用占位实现 // 实际应替换为从 OTP / 安全 Flash / HKDF 派生 static void load_aes_key(uint8_t *key) { // 示例从 MCU UID 派生实际项目请用安全方案 // 这里仅作演示切勿在生产环境硬编码 const uint8_t demo_key[AES_KEY_LEN] { 0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C }; memcpy(key, demo_key, AES_KEY_LEN); }IV 生成用 MCU 的硬件随机数发生器RNG。如果 MCU 没有 RNG可以用 ADC 采样悬空引脚的噪声作为熵源但质量差很多不建议用于生产。下面用hal_get_random占位。static void generate_iv(uint8_t *iv) { // 优先使用硬件 RNG例如 STM32 的 RNG 外设 // 这里用占位函数实际请替换 for (int i 0; i AES_BLOCK_LEN; i) { iv[i] (uint8_t)hal_get_random(); } }PKCS#7 填充明文长度对 16 取模差多少就补多少个「差值」字节。比如差 5 字节就补 5 个 0x05。如果明文正好是 16 的整数倍要额外补一整块 16 个 0x10否则解密时无法判断是否有填充。static size_t pkcs7_pad(uint8_t *buf, size_t len, size_t buf_size) { size_t pad AES_BLOCK_LEN - (len % AES_BLOCK_LEN); if (len pad buf_size) return 0; for (size_t i 0; i pad; i) { buf[len i] (uint8_t)pad; } return len pad; } static size_t pkcs7_unpad(uint8_t *buf, size_t len) { if (len 0 || len % AES_BLOCK_LEN ! 0) return 0; uint8_t pad buf[len - 1]; if (pad 0 || pad AES_BLOCK_LEN) return 0; for (size_t i 0; i pad; i) { if (buf[len - 1 - i] ! pad) return 0; } return len - pad; }加密并写入 EEPROM。注意 CBC 模式需要 IVtiny-AES-c 的AES_CBC_encrypt会在内部更新 IV 缓冲所以传入的 IV 数组会被修改写 EEPROM 前要先把原始 IV 备份出来。int secure_store(const uint8_t *plain, size_t plain_len) { uint8_t buf[MAX_DATA_LEN AES_BLOCK_LEN]; uint8_t iv[AES_BLOCK_LEN]; uint8_t iv_backup[AES_BLOCK_LEN]; struct AES_ctx ctx; if (plain_len MAX_DATA_LEN) return -1; memcpy(buf, plain, plain_len); size_t total pkcs7_pad(buf, plain_len, sizeof(buf)); if (total 0) return -2; generate_iv(iv); memcpy(iv_backup, iv, AES_BLOCK_LEN); AES_init_ctx_iv(ctx, g_aes_key, iv); AES_CBC_encrypt_buffer(ctx, buf, total); // 先写 IV再写密文最后写 CRC if (evash_eeprom_write(IV_ADDR, iv_backup, AES_BLOCK_LEN) ! 0) return -3; if (evash_eeprom_write(DATA_ADDR, buf, total) ! 0) return -4; uint32_t crc crc32_calc(buf, total); if (evash_eeprom_write(CRC_ADDR, (uint8_t *)crc, 4) ! 0) return -5; return 0; }读取并解密。先读 IV 和密文再读 CRC 校验校验通过才解密。这一步很重要如果 EEPROM 数据被篡改或者写入不完整CRC 会先拦住避免解密出乱码还继续用。int secure_load(uint8_t *plain, size_t *plain_len, size_t max_len) { uint8_t buf[MAX_DATA_LEN AES_BLOCK_LEN]; uint8_t iv[AES_BLOCK_LEN]; uint32_t crc_stored, crc_calc; struct AES_ctx ctx; if (evash_eeprom_read(IV_ADDR, iv, AES_BLOCK_LEN) ! 0) return -1; if (evash_eeprom_read(DATA_ADDR, buf, MAX_DATA_LEN AES_BLOCK_LEN) ! 0) return -2; if (evash_eeprom_read(CRC_ADDR, (uint8_t *)crc_stored, 4) ! 0) return -3; // 这里假设密文长度已知实际项目应把长度也存进 EEPROM size_t cipher_len MAX_DATA_LEN AES_BLOCK_LEN; crc_calc crc32_calc(buf, cipher_len); if (crc_calc ! crc_stored) return -4; AES_init_ctx_iv(ctx, g_aes_key, iv); AES_CBC_decrypt_buffer(ctx, buf, cipher_len); size_t real_len pkcs7_unpad(buf, cipher_len); if (real_len 0 || real_len max_len) return -5; memcpy(plain, buf, real_len); *plain_len real_len; return 0; }上面这段代码里crc32_calc你需要自己实现或者用库函数。CRC32 的多项式用0xEDB88320反射或0x04C11DB7非反射两边保持一致就行。注意 CRC 只防意外错误不防恶意篡改。如果威胁模型里有主动攻击者应该用 HMAC-SHA256 替代 CRC密钥和 AES 密钥分开。如果你在调试阶段需要快速验证 AES 实现是否符合标准可以用 TaoToken 的模型对话把测试向量贴进去比对。比如 NIST 的 AES-128-CBC 测试向量密钥2b7e151628aed2a6abf7158809cf4f3cIV000102030405060708090a0b0c0d0e0f明文6bc1bee22e409f96e93d7e117393172a期望密文7649abac8119b246cee98e9b12e9197d。把你的输出和这个对比一致就说明 AES 核心没问题。4. 验证请求与成功结果确认代码写完了怎么确认它真的跑通了我建议分三步验证每一步都有明确的成功标志。第一步单元测试 AES 核心。在 PC 上编译 tiny-AES-c跑 NIST 测试向量。成功标志是加密输出和标准向量逐字节一致。这一步排除库本身的问题。如果你用的是硬件 AES 外设同样先用测试向量验证外设配置正确。第二步在目标板上做 EEPROM 读写回环。先不加密直接写一段已知数据到DATA_ADDR读回来比对。成功标志是读回的数据和写入的完全一致。这一步排除 I2C/SPI 驱动和地址映射的问题。EVASH Ultra EEPROM 的地址是字节地址还是页地址不同型号可能不一样一定要看数据手册确认。第三步跑完整的secure_store和secure_load。写入一段明文比如SN:EVASH-2024-0001然后读回解密。成功标志是解密后的字符串和原始明文完全一致且 CRC 校验通过。你可以用串口打印十六进制来确认uint8_t plain[] SN:EVASH-2024-0001; uint8_t out[64]; size_t out_len; load_aes_key(g_aes_key); if (secure_store(plain, sizeof(plain) - 1) 0) { printf(store ok\r\n); } else { printf(store failed\r\n); } if (secure_load(out, out_len, sizeof(out)) 0) { out[out_len] \0; printf(load ok: %s\r\n, out); } else { printf(load failed\r\n); }串口应该输出store ok load ok: SN:EVASH-2024-0001如果输出的是乱码或者load failed按下一节的排查表逐项检查。还有一个容易被忽略的验证点掉电重启后数据是否还在。EEPROM 的价值就在于非易失所以写完数据后断电重新上电再读一次确认数据完整。我实测下来EVASH Ultra EEPROM 在正常写入后掉电数据保持是没问题的但如果你在写入过程中断电可能写到一半这时候 CRC 校验就能发挥作用读出来会报-4错误提示数据损坏。另外如果你需要把加密后的数据同步到云端做备份或者远程校验可以通过 TaoToken 的 API 接入大模型做数据格式转换和校验逻辑生成。API 地址是https://taotoken.net/api具体接入方式参考官方文档。注意 API 地址不带 UTM 参数直接访问即可。5. 本篇常见错误排查这一节我把实际调试中最容易遇到的报错和现象列出来对照排查。现象一load failed返回 -4CRC 校验不通过。最常见的原因是写入和读取的长度不一致。比如你写入了 32 字节但读取时读了 64 字节后面 32 字节是 EEPROM 里的残留数据CRC 自然对不上。解决方法是把密文长度也存进 EEPROM读取时先读长度再读对应字节数。另一个原因是 EEPROM 写入后需要等待写周期完成通常 5ms 左右如果你写完立刻读可能读到旧数据。EVASH Ultra EEPROM 的数据手册里会标注tWR参数写完后延时或者轮询 ACK 再读。现象二解密出来是乱码但 CRC 通过。这说明密文完整但密钥或 IV 不对。检查load_aes_key读到的密钥和加密时用的是不是同一份。如果你在加密和解密之间重启了设备而密钥是从 RNG 临时生成的那肯定对不上。IV 也要确认加密时生成的 IV 备份写进了IV_ADDR解密时从同一地址读如果地址写错了比如写0x0100读0x0101IV 就错了。CBC 模式下 IV 错一个字节第一个块解密就全错后续块也会连锁错误。现象三编译报错undefined reference to AES_init_ctx_iv。这是链接问题tiny-AES-c 需要把aes.c加入编译并且确认AES_CBC宏已启用。tiny-AES-c 默认可能只编译 ECB你需要在aes.h里定义#define AES_CBC 1或者在编译选项里加-DAES_CBC1。如果用的是 mbedTLS需要确认MBEDTLS_CIPHER_MODE_CBC已开启。现象四evash_eeprom_write返回非 0写入失败。检查 I2C 地址是否正确。EVASH Ultra EEPROM 的 I2C 从机地址通常是0xA0或0xA17 位地址左移一位具体看型号。如果地址对了还失败用逻辑分析仪抓 I2C 波形看 ACK 有没有正常拉低。SPI 接口的话检查 CS、CLK、MOSI、MISO 四根线的时序和模式CPOL/CPHA。还有一种情况是写保护引脚WP被拉低导致写入被硬件拒绝。现象五local proxy failed或401类错误。这类错误通常出现在你通过云端服务做密钥管理或者远程校验时。401表示鉴权失败检查你的 API Key 是否有效、是否过期、请求头里的Authorization格式是否正确。local proxy failed一般是本地网络配置问题检查你的设备是否能正常访问外网DNS 解析是否正常。如果你用的是 TaoToken 的 API确认 Base URL 填的是https://taotoken.net/apiKey 从控制台的 API Keys 页面获取。现象六AES 加密后数据长度不对。比如明文 16 字节加密后应该是 32 字节因为 PKCS#7 要补一整块。如果你得到 16 字节说明填充逻辑没生效或者你用了 NoPadding 模式。CBC 模式必须填充除非明文本身就是 16 的整数倍且你明确知道不需要填充。检查pkcs7_pad的返回值如果返回 0 说明缓冲区不够。现象七设备运行一段时间后 EEPROM 写入失败。这很可能是擦写寿命到了。EVASH Ultra EEPROM 的擦写次数是有限的如果你在同一个地址高频写入比如每秒写一次日志几天就能把那一页写坏。解决方法是做磨损均衡把写操作分散到多个页轮流使用或者用 RAM 缓冲攒够一批再写。另外写之前先判断数据是否真的变了没变就不写能省很多寿命。排查的时候我习惯先用最小系统验证只跑 EEPROM 读写不加密通了再加 AES再加 CRC。每加一层就测一次出问题能快速定位是哪一层的锅。一上来就跑完整流程出错了两眼一抹黑。6. 长期编码与 Agent 场景的接入建议如果你只是偶尔写一段 AES 加解密的代码上面这些足够了。但如果你在做的是一个长期迭代的嵌入式安全项目涉及多个型号的 EVASH EEPROM、多种 MCU 平台、多套密钥管理体系那靠手动复制代码效率很低而且容易在不同项目之间引入不一致。这种场景下我建议把常用的加密存储逻辑封装成可复用的模块配合 Coding Plan 做长期的代码生成和重构。Coding Plan 适合需要持续产出代码、维护多个分支、做 Agent 自动化的场景。你可以把 EVASH EEPROM 的驱动接口、AES 参数配置、CRC 校验规则写成模板让 Agent 根据不同的目标平台自动生成适配代码。比如从 STM32 迁移到 ESP32只需要改 EEPROM 驱动层AES 和校验逻辑不用动。具体接入时Base URL 用https://taotoken.net/apiKey 从控制台的 API Keys 页面生成Model ID 根据你的任务选代码生成用通用编码模型安全审计用推理能力强的模型。三件套配齐后在 Claude Code 或者 Cline 里配置好就能让 Agent 直接读写你的项目文件、生成代码、跑测试。对于需要快速验证加密逻辑的场景比如你想确认某段密文用另一个密钥解密会得到什么结果直接用模型对话更轻量。把密钥、IV、密文贴进去让它帮你算比你自己写测试代码快。模型对话的入口在官网导航里能找到。最后说一个实际经验嵌入式安全里密钥管理比加密算法本身更重要。AES 算法是公开的、经过验证的你只要用对模式、管好 IV就不会出大问题。但密钥如果硬编码在固件里、明文存在 EEPROM 里、或者通过不安全的通道传输那再强的加密也是摆设。我踩过的坑是早期项目把密钥和密文存在同一块 EEPROM 里攻击者读一次就全拿到了。后来改成密钥放 MCU 安全区、EEPROM 只存密文和 IV安全性才达标。你在设计阶段就要把这条边界划清楚。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进