
1. 為什么要在 EVASH Ultra EEPROM 上做 AES 加密存儲EVASH Ultra EEPROM 是一類面向嵌入式場景的非易失存儲器件常見容量從幾 KB 到幾百 KB接口以 I2C 和 SPI 為主工作電壓覆蓋 1.8V 到 5.5V。它的定位很明確給 MCU 提供一塊掉電不丟數(shù)據(jù)的小容量存儲區(qū)用來放設(shè)備序列號、校準(zhǔn)參數(shù)、License、用戶配置、傳感器標(biāo)定值這些東西。問題也恰恰出在這里——這些數(shù)據(jù)里往往混著敏感信息比如設(shè)備密鑰、Wi-Fi 憑據(jù)、支付令牌、醫(yī)療設(shè)備的患者參數(shù)。如果直接明文寫進(jìn) EEPROM任何人把芯片焊下來、用編程器一讀數(shù)據(jù)就全暴露了。我見過不少項目硬件工程師覺得「EEPROM 在板子內(nèi)部別人拿不到」于是把密鑰明文寫進(jìn)去。等到產(chǎn)品做安全認(rèn)證或者被客戶做滲透測試時這一條直接判不合格。EVASH Ultra EEPROM 本身不提供加密引擎它只是一塊存儲介質(zhì)加密這件事必須由 MCU 側(cè)的軟件來完成。所以「EVASH Ultra EEPROM AES 加密算法示例」這個需求本質(zhì)上是問怎么在資源受限的嵌入式環(huán)境里把 AES 加解密和 EEPROM 讀寫串成一個可靠的閉環(huán)。AES 是對稱分組密碼分組固定 128 位16 字節(jié)密鑰長度支持 128、192、256 位。嵌入式里最常用的是 AES-128因為它在安全性和算力開銷之間比較平衡。AES-256 更安全但密鑰調(diào)度和輪運算的耗時大約是 AES-128 的 1.4 倍對主頻只有幾十 MHz 的 MCU 來說這個差距在頻繁加解密時是能感知的。至于分組模式ECB 模式簡單但相同明文塊會產(chǎn)生相同密文塊泄露數(shù)據(jù)模式不建議用于結(jié)構(gòu)化數(shù)據(jù)CBC 模式需要 IV初始化向量每個塊加密前先和前一塊密文異或安全性好很多是嵌入式里最常用的選擇CTR 模式把分組密碼變成流密碼可以并行、不需要填充適合大數(shù)據(jù)量但 IV 絕對不能重復(fù)。這篇文章面向的是正在用 EVASH Ultra EEPROM 做產(chǎn)品、需要把敏感數(shù)據(jù)加密落盤的嵌入式開發(fā)者。我會給出一套可以直接復(fù)制改造的 C 代碼覆蓋密鑰寫入、AES 參數(shù)設(shè)置、數(shù)據(jù)加解密、讀寫校驗四個環(huán)節(jié)并且把常見的坑比如 401 類鑒權(quán)失敗、地址越界、IV 復(fù)用都拆開講清楚。如果你手上正好有 EVASH Ultra EEPROM 的樣片和一塊 STM32 或者 ESP32跟著做一遍就能跑通。需要說明的是AES 庫的選擇很關(guān)鍵。裸機(jī)環(huán)境常用 tiny-AES-c、mbedTLS 的 AES 模塊或者芯片廠商 SDK 自帶的硬件加密外設(shè)。如果你的 MCU 有 AES 硬件加速比如 STM32 的 CRYP 外設(shè)、ESP32 的 AES 加速器優(yōu)先用硬件速度快一個數(shù)量級而且不容易被側(cè)信道攻擊。下面示例我用 tiny-AES-c 的接口風(fēng)格來寫因為它足夠小、可移植幾乎任何平臺都能編譯。2. EVASH Ultra EEPROM 與 AES 密鑰的前置準(zhǔn)備在寫代碼之前有幾件事必須先定下來否則后面會反復(fù)返工。第一是密鑰從哪來。絕對不要在源碼里硬編碼uint8_t aes_key[16] {0x01, 0x23, ...}這種密鑰一旦固件被 dump 出來就等于沒有。正確做法是產(chǎn)線階段用安全燒錄工具把密鑰寫進(jìn) MCU 的 OTP 區(qū)或者安全 Flash 區(qū)運行時從那里讀或者用 MCU 的硬件唯一 IDUID配合一個設(shè)備級鹽值通過 HKDF 派生出每臺設(shè)備不同的密鑰。EVASH Ultra EEPROM 里存的應(yīng)該是「被加密后的業(yè)務(wù)數(shù)據(jù)」而不是密鑰本身。如果非要在 EEPROM 里存密鑰那至少要再套一層用 MCU 內(nèi)部密鑰加密后再存形成兩級保護(hù)。第二是 EEPROM 的地址規(guī)劃。EVASH Ultra EEPROM 的擦寫壽命通常在 100 萬次量級但這是按頁Page算的不是按字節(jié)。頻繁改寫同一地址會加速局部磨損。所以規(guī)劃時要把「頻繁寫的區(qū)域」和「只寫一次的區(qū)域」分開。密鑰、設(shè)備證書這類只寫一次的數(shù)據(jù)放在低地址區(qū)運行日志、計數(shù)器這類頻繁更新的數(shù)據(jù)放在高地址區(qū)并且做磨損均衡wear leveling。下面示例里我用0x0000存加密后的敏感數(shù)據(jù)0x0100存 IV0x0200存校驗用的 HMAC 或 CRC。第三是 AES 參數(shù)。我建議用 AES-128-CBC PKCS#7 填充。原因CBC 能隱藏明文模式PKCS#7 填充規(guī)則簡單嵌入式實現(xiàn)容易。IV 必須是隨機(jī)數(shù)每次加密都換絕對不能復(fù)用。IV 不需要保密可以明文存在 EEPROM 里但必須和密文一一對應(yīng)。如果你用 CTR 模式IV 就變成了計數(shù)器復(fù)用會導(dǎo)致災(zāi)難性的密鑰流復(fù)用兩段密文異或就能還原明文這個坑一定要避開。第四是數(shù)據(jù)長度。AES 分組是 16 字節(jié)明文長度必須是 16 的整數(shù)倍不夠就填充。EVASH Ultra EEPROM 的頁大小常見是 32 字節(jié)或 64 字節(jié)寫的時候要按頁對齊跨頁寫需要拆成多次。讀的時候沒有對齊要求但建議也按頁讀減少 I2C/SPI 事務(wù)次數(shù)。下面這張表把關(guān)鍵參數(shù)列清楚方便你對照自己的硬件改參數(shù)推薦值說明AES 密鑰長度128 位嵌入式平衡之選有硬件加速可上 256分組模式CBC需要 IV隱藏明文模式填充方式PKCS#7明文補到 16 字節(jié)整數(shù)倍IV 長度16 字節(jié)每次加密隨機(jī)生成明文存儲EEPROM 數(shù)據(jù)區(qū)0x0000 起存密文EEPROM IV 區(qū)0x0100 起存 IVEEPROM 校驗區(qū)0x0200 起存 CRC32 或 HMAC如果你在開發(fā)過程中需要調(diào)用云端大模型來輔助生成測試向量、校驗 AES 實現(xiàn)是否正確可以用 TaoToken 的模型對話能力做交叉驗證把加密前后的十六進(jìn)制貼進(jìn)去讓它幫你比對。接入方式在下一節(jié)講。3. 可復(fù)制的 AES 加解密與 EEPROM 讀寫配置這一節(jié)是核心我給出完整的 C 代碼按「密鑰準(zhǔn)備 → IV 生成 → 加密 → 寫 EEPROM → 讀 EEPROM → 解密 → 校驗」的順序組織。代碼基于 tiny-AES-c 的 API 風(fēng)格你可以直接替換成自己平臺的庫函數(shù)。先看頭文件和宏定義。EEPROM 的讀寫函數(shù)我用evash_eeprom_write和evash_eeprom_read表示你需要替換成 EVASH 官方 SDK 或者你自己寫的 I2C/SPI 驅(qū)動。#include stdint.h #include string.h #include aes.h // tiny-AES-c 或你的 AES 庫 #include evash_eeprom.h // EVASH Ultra EEPROM 驅(qū)動 #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]; // 從安全區(qū)讀取密鑰這里用占位實現(xiàn) // 實際應(yīng)替換為從 OTP / 安全 Flash / HKDF 派生 static void load_aes_key(uint8_t *key) { // 示例從 MCU UID 派生實際項目請用安全方案 // 這里僅作演示切勿在生產(chǎn)環(huán)境硬編碼 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 的硬件隨機(jī)數(shù)發(fā)生器RNG。如果 MCU 沒有 RNG可以用 ADC 采樣懸空引腳的噪聲作為熵源但質(zhì)量差很多不建議用于生產(chǎn)。下面用hal_get_random占位。static void generate_iv(uint8_t *iv) { // 優(yōu)先使用硬件 RNG例如 STM32 的 RNG 外設(shè) // 這里用占位函數(shù)實際請?zhí)鎿Q for (int i 0; i AES_BLOCK_LEN; i) { iv[i] (uint8_t)hal_get_random(); } }PKCS#7 填充明文長度對 16 取模差多少就補多少個「差值」字節(jié)。比如差 5 字節(jié)就補 5 個 0x05。如果明文正好是 16 的整數(shù)倍要額外補一整塊 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會在內(nèi)部更新 IV 緩沖所以傳入的 IV 數(shù)組會被修改寫 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 數(shù)據(jù)被篡改或者寫入不完整CRC 會先攔住避免解密出亂碼還繼續(xù)用。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; // 這里假設(shè)密文長度已知實際項目應(yīng)把長度也存進(jìn) 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你需要自己實現(xiàn)或者用庫函數(shù)。CRC32 的多項式用0xEDB88320反射或0x04C11DB7非反射兩邊保持一致就行。注意 CRC 只防意外錯誤不防惡意篡改。如果威脅模型里有主動攻擊者應(yīng)該用 HMAC-SHA256 替代 CRC密鑰和 AES 密鑰分開。如果你在調(diào)試階段需要快速驗證 AES 實現(xiàn)是否符合標(biāo)準(zhǔn)可以用 TaoToken 的模型對話把測試向量貼進(jìn)去比對。比如 NIST 的 AES-128-CBC 測試向量密鑰2b7e151628aed2a6abf7158809cf4f3cIV000102030405060708090a0b0c0d0e0f明文6bc1bee22e409f96e93d7e117393172a期望密文7649abac8119b246cee98e9b12e9197d。把你的輸出和這個對比一致就說明 AES 核心沒問題。4. 驗證請求與成功結(jié)果確認(rèn)代碼寫完了怎么確認(rèn)它真的跑通了我建議分三步驗證每一步都有明確的成功標(biāo)志。第一步單元測試 AES 核心。在 PC 上編譯 tiny-AES-c跑 NIST 測試向量。成功標(biāo)志是加密輸出和標(biāo)準(zhǔn)向量逐字節(jié)一致。這一步排除庫本身的問題。如果你用的是硬件 AES 外設(shè)同樣先用測試向量驗證外設(shè)配置正確。第二步在目標(biāo)板上做 EEPROM 讀寫回環(huán)。先不加密直接寫一段已知數(shù)據(jù)到DATA_ADDR讀回來比對。成功標(biāo)志是讀回的數(shù)據(jù)和寫入的完全一致。這一步排除 I2C/SPI 驅(qū)動和地址映射的問題。EVASH Ultra EEPROM 的地址是字節(jié)地址還是頁地址不同型號可能不一樣一定要看數(shù)據(jù)手冊確認(rèn)。第三步跑完整的secure_store和secure_load。寫入一段明文比如SN:EVASH-2024-0001然后讀回解密。成功標(biāo)志是解密后的字符串和原始明文完全一致且 CRC 校驗通過。你可以用串口打印十六進(jìn)制來確認(rèn)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); }串口應(yīng)該輸出store ok load ok: SN:EVASH-2024-0001如果輸出的是亂碼或者load failed按下一節(jié)的排查表逐項檢查。還有一個容易被忽略的驗證點掉電重啟后數(shù)據(jù)是否還在。EEPROM 的價值就在于非易失所以寫完數(shù)據(jù)后斷電重新上電再讀一次確認(rèn)數(shù)據(jù)完整。我實測下來EVASH Ultra EEPROM 在正常寫入后掉電數(shù)據(jù)保持是沒問題的但如果你在寫入過程中斷電可能寫到一半這時候 CRC 校驗就能發(fā)揮作用讀出來會報-4錯誤提示數(shù)據(jù)損壞。另外如果你需要把加密后的數(shù)據(jù)同步到云端做備份或者遠(yuǎn)程校驗可以通過 TaoToken 的 API 接入大模型做數(shù)據(jù)格式轉(zhuǎn)換和校驗邏輯生成。API 地址是https://taotoken.net/api具體接入方式參考官方文檔。注意 API 地址不帶 UTM 參數(shù)直接訪問即可。5. 本篇常見錯誤排查這一節(jié)我把實際調(diào)試中最容易遇到的報錯和現(xiàn)象列出來對照排查?,F(xiàn)象一load failed返回 -4CRC 校驗不通過。最常見的原因是寫入和讀取的長度不一致。比如你寫入了 32 字節(jié)但讀取時讀了 64 字節(jié)后面 32 字節(jié)是 EEPROM 里的殘留數(shù)據(jù)CRC 自然對不上。解決方法是把密文長度也存進(jìn) EEPROM讀取時先讀長度再讀對應(yīng)字節(jié)數(shù)。另一個原因是 EEPROM 寫入后需要等待寫周期完成通常 5ms 左右如果你寫完立刻讀可能讀到舊數(shù)據(jù)。EVASH Ultra EEPROM 的數(shù)據(jù)手冊里會標(biāo)注tWR參數(shù)寫完后延時或者輪詢 ACK 再讀。現(xiàn)象二解密出來是亂碼但 CRC 通過。這說明密文完整但密鑰或 IV 不對。檢查load_aes_key讀到的密鑰和加密時用的是不是同一份。如果你在加密和解密之間重啟了設(shè)備而密鑰是從 RNG 臨時生成的那肯定對不上。IV 也要確認(rèn)加密時生成的 IV 備份寫進(jìn)了IV_ADDR解密時從同一地址讀如果地址寫錯了比如寫0x0100讀0x0101IV 就錯了。CBC 模式下 IV 錯一個字節(jié)第一個塊解密就全錯后續(xù)塊也會連鎖錯誤?,F(xiàn)象三編譯報錯undefined reference to AES_init_ctx_iv。這是鏈接問題tiny-AES-c 需要把aes.c加入編譯并且確認(rèn)AES_CBC宏已啟用。tiny-AES-c 默認(rèn)可能只編譯 ECB你需要在aes.h里定義#define AES_CBC 1或者在編譯選項里加-DAES_CBC1。如果用的是 mbedTLS需要確認(rèn)MBEDTLS_CIPHER_MODE_CBC已開啟?,F(xiàn)象四evash_eeprom_write返回非 0寫入失敗。檢查 I2C 地址是否正確。EVASH Ultra EEPROM 的 I2C 從機(jī)地址通常是0xA0或0xA17 位地址左移一位具體看型號。如果地址對了還失敗用邏輯分析儀抓 I2C 波形看 ACK 有沒有正常拉低。SPI 接口的話檢查 CS、CLK、MOSI、MISO 四根線的時序和模式CPOL/CPHA。還有一種情況是寫保護(hù)引腳WP被拉低導(dǎo)致寫入被硬件拒絕?,F(xiàn)象五local proxy failed或401類錯誤。這類錯誤通常出現(xiàn)在你通過云端服務(wù)做密鑰管理或者遠(yuǎn)程校驗時。401表示鑒權(quán)失敗檢查你的 API Key 是否有效、是否過期、請求頭里的Authorization格式是否正確。local proxy failed一般是本地網(wǎng)絡(luò)配置問題檢查你的設(shè)備是否能正常訪問外網(wǎng)DNS 解析是否正常。如果你用的是 TaoToken 的 API確認(rèn) Base URL 填的是https://taotoken.net/apiKey 從控制臺的 API Keys 頁面獲取。現(xiàn)象六AES 加密后數(shù)據(jù)長度不對。比如明文 16 字節(jié)加密后應(yīng)該是 32 字節(jié)因為 PKCS#7 要補一整塊。如果你得到 16 字節(jié)說明填充邏輯沒生效或者你用了 NoPadding 模式。CBC 模式必須填充除非明文本身就是 16 的整數(shù)倍且你明確知道不需要填充。檢查pkcs7_pad的返回值如果返回 0 說明緩沖區(qū)不夠?,F(xiàn)象七設(shè)備運行一段時間后 EEPROM 寫入失敗。這很可能是擦寫壽命到了。EVASH Ultra EEPROM 的擦寫次數(shù)是有限的如果你在同一個地址高頻寫入比如每秒寫一次日志幾天就能把那一頁寫壞。解決方法是做磨損均衡把寫操作分散到多個頁輪流使用或者用 RAM 緩沖攢夠一批再寫。另外寫之前先判斷數(shù)據(jù)是否真的變了沒變就不寫能省很多壽命。排查的時候我習(xí)慣先用最小系統(tǒng)驗證只跑 EEPROM 讀寫不加密通了再加 AES再加 CRC。每加一層就測一次出問題能快速定位是哪一層的鍋。一上來就跑完整流程出錯了兩眼一抹黑。6. 長期編碼與 Agent 場景的接入建議如果你只是偶爾寫一段 AES 加解密的代碼上面這些足夠了。但如果你在做的是一個長期迭代的嵌入式安全項目涉及多個型號的 EVASH EEPROM、多種 MCU 平臺、多套密鑰管理體系那靠手動復(fù)制代碼效率很低而且容易在不同項目之間引入不一致。這種場景下我建議把常用的加密存儲邏輯封裝成可復(fù)用的模塊配合 Coding Plan 做長期的代碼生成和重構(gòu)。Coding Plan 適合需要持續(xù)產(chǎn)出代碼、維護(hù)多個分支、做 Agent 自動化的場景。你可以把 EVASH EEPROM 的驅(qū)動接口、AES 參數(shù)配置、CRC 校驗規(guī)則寫成模板讓 Agent 根據(jù)不同的目標(biāo)平臺自動生成適配代碼。比如從 STM32 遷移到 ESP32只需要改 EEPROM 驅(qū)動層AES 和校驗邏輯不用動。具體接入時Base URL 用https://taotoken.net/apiKey 從控制臺的 API Keys 頁面生成Model ID 根據(jù)你的任務(wù)選代碼生成用通用編碼模型安全審計用推理能力強(qiáng)的模型。三件套配齊后在 Claude Code 或者 Cline 里配置好就能讓 Agent 直接讀寫你的項目文件、生成代碼、跑測試。對于需要快速驗證加密邏輯的場景比如你想確認(rèn)某段密文用另一個密鑰解密會得到什么結(jié)果直接用模型對話更輕量。把密鑰、IV、密文貼進(jìn)去讓它幫你算比你自己寫測試代碼快。模型對話的入口在官網(wǎng)導(dǎo)航里能找到。最后說一個實際經(jīng)驗嵌入式安全里密鑰管理比加密算法本身更重要。AES 算法是公開的、經(jīng)過驗證的你只要用對模式、管好 IV就不會出大問題。但密鑰如果硬編碼在固件里、明文存在 EEPROM 里、或者通過不安全的通道傳輸那再強(qiáng)的加密也是擺設(shè)。我踩過的坑是早期項目把密鑰和密文存在同一塊 EEPROM 里攻擊者讀一次就全拿到了。后來改成密鑰放 MCU 安全區(qū)、EEPROM 只存密文和 IV安全性才達(dá)標(biāo)。你在設(shè)計階段就要把這條邊界劃清楚。