3. 密码学基础:对称加密(AES)、非对称加密(RSA/ECC)、哈希函数(SHA-256)在嵌入式中的实现

好,咱们直接切入正题。这一章聊的是密码学在嵌入式里的落地。说实话,很多工程师觉得密码学是数学家的活儿,跟我们搞硬件的没关系。我以前也这么想,直到有一次产品送审被打了回来——合规报告上写着「数据存储未加密,存在泄露风险」。嗯,从那以后,我再也不敢小看这几行加密代码了。

在医疗贴片这种资源受限的设备上,密码学不是「要不要用」的问题,而是「怎么用才不把MCU跑死」的问题。你想想看,一个Cortex-M0内核,主频才几十兆,Flash和RAM都抠抠搜搜的,你还想跑完整的OpenSSL?不现实。所以,咱们得挑最合适的算法,用最省资源的方式去实现。

3.1 对称加密:AES在MCU上的轻量实现

对称加密,说白了就是加密和解密用同一把钥匙。在嵌入式里,AES是绝对的主流。我个人习惯用AES-128,为什么?因为AES-256虽然更安全,但在低端MCU上,密钥扩展和轮数计算会多耗不少时间。对于医疗贴片这种数据量不大、实时性要求高的场景,AES-128完全够用。

我在项目中遇到过一个问题:直接用软件实现AES,一个128位的数据块加密要花掉好几毫秒。这在心电数据采集里是不能接受的——你这边还在加密,那边新的采样点已经来了。后来我换成了硬件AES加速器,很多MCU(比如STM32、NXP的LPC系列)都内置了这个外设。配置好之后,加密一个块只需要几十个时钟周期。

关键点: 优先使用MCU内置的硬件AES引擎。如果必须用软件实现,选择查表法(T-table)实现,比逐位运算快3-5倍。

这里给一个简单的硬件AES初始化示例(伪代码):

// 假设MCU有AES外设
void aes_hw_init(void) {
    // 使能AES时钟
    RCC->AHBENR |= RCC_AHBENR_AESEN;
    
    // 配置密钥(128位)
    AES->KEYR0 = key[0];
    AES->KEYR1 = key[1];
    AES->KEYR2 = key[2];
    AES->KEYR3 = key[3];
    
    // 选择模式:ECB或CBC
    AES->CR |= AES_CR_MODE_ENCRYPT | AES_CR_CHMOD_ECB;
    
    // 启动
    AES->CR |= AES_CR_EN;
}

void aes_hw_encrypt(uint32_t *plaintext, uint32_t *ciphertext) {
    // 写入明文
    AES->DINR = plaintext[0];
    AES->DINR = plaintext[1];
    AES->DINR = plaintext[2];
    AES->DINR = plaintext[3];
    
    // 等待完成
    while(!(AES->SR & AES_SR_CCF));
    
    // 读取密文
    ciphertext[0] = AES->DOUTR;
    ciphertext[1] = AES->DOUTR;
    ciphertext[2] = AES->DOUTR;
    ciphertext[3] = AES->DOUTR;
}
注意: 千万不要在ECB模式下加密重复性高的医疗数据(比如体温曲线)。ECB模式下,相同的明文块会产生相同的密文块,会泄露数据模式。我建议用CBC模式,或者更安全的GCM模式(同时提供认证)。

3.2 非对称加密:RSA与ECC的取舍

非对称加密,就是公钥加密、私钥解密。在医疗贴片里,它的主要用途是密钥交换和数字签名。RSA和ECC是两大主流,但说实话,在嵌入式里,我强烈推荐ECC。

为什么?我给你算笔账。RSA-2048的密钥长度是2048位,而ECC-256(比如secp256r1曲线)提供相同的安全强度,密钥长度只有256位。在MCU上,这意味着更少的存储空间、更少的传输带宽、更快的计算速度。我曾经在一个项目里对比过:同样的签名操作,RSA-2048用了2.3秒,ECC-256只用了0.4秒。在医疗贴片这种电池供电的设备上,这差距就是能不能用的区别。

不过,ECC的实现比RSA复杂得多。你想想看,椭圆曲线上的点加、点乘、模逆运算,每一步都要小心溢出和时序攻击。我个人习惯用mbedTLS或者MicroECC这样的成熟库,而不是自己手写。自己写的ECC,十有八九有侧信道漏洞。

小技巧: 如果MCU支持硬件随机数生成器(TRNG),一定要用它来生成密钥对。伪随机数生成器(PRNG)在资源受限的嵌入式环境里,很容易被预测。

这里展示一个ECC密钥生成的简化流程:

// 使用MicroECC库生成密钥对
uint8_t private_key[32];  // 256位私钥
uint8_t public_key[64];   // 256位公钥(x和y坐标各32字节)

// 生成随机数作为私钥
trng_get_random_bytes(private_key, 32);

// 计算公钥:公钥 = 私钥 * G(基点)
ecc_make_key(public_key, private_key);

// 注意:私钥必须安全存储,比如写入MCU的OTP区域或安全元件

3.3 哈希函数:SHA-256的固件完整性校验

哈希函数,说白了就是「数字指纹」。SHA-256能把任意长度的数据,压缩成一个256位的固定长度摘要。在医疗贴片里,它的主要用途是固件完整性校验和消息认证。

我记得有一次,客户要求我们的贴片在每次启动时都要校验固件是否被篡改。实现方式很简单:在固件烧录时,计算整个固件镜像的SHA-256哈希值,存储在Flash的末尾。每次启动时,Bootloader重新计算哈希,跟存储的值比对。如果不一致,说明固件被改了,直接拒绝启动。

SHA-256在嵌入式里的实现,我建议用硬件加速器(如果有的话),或者用经过优化的软件实现。很多MCU厂商都提供了硬件SHA引擎,比如STM32的HASH外设。如果没有硬件支持,mbedTLS的软件实现也足够快——在72MHz的Cortex-M3上,处理1KB数据大约需要1-2毫秒。

避坑指南: 我曾经踩过一个坑——用SHA-256做密码验证。用户输入密码后,计算SHA-256,跟存储的哈希值比对。看起来没问题对吧?但问题是,如果攻击者拿到了哈希值,他可以用彩虹表反向查找密码。正确的做法是加盐(salt),每次存储时生成一个随机盐值,跟密码拼接后再哈希。

一个简单的固件校验流程:

// Bootloader中的固件校验
bool verify_firmware(void) {
    uint8_t expected_hash[32];
    uint8_t computed_hash[32];
    
    // 从Flash末尾读取预存的哈希值
    memcpy(expected_hash, (uint8_t*)(FIRMWARE_END - 32), 32);
    
    // 计算固件区域的SHA-256
    sha256_start(&ctx);
    sha256_update(&ctx, (uint8_t*)FIRMWARE_START, FIRMWARE_SIZE - 32);
    sha256_finish(&ctx, computed_hash);
    
    // 比对
    if (memcmp(expected_hash, computed_hash, 32) == 0) {
        return true;  // 固件完整
    } else {
        return false; // 固件被篡改
    }
}

3.4 三种算法的协同使用

在实际的医疗贴片产品里,这三种算法不是孤立使用的,而是协同工作。我给你画一个典型的场景:

  1. 设备配对阶段: 使用ECC生成密钥对,通过非对称加密交换AES的会话密钥。
  2. 数据传输阶段: 使用AES-CBC或AES-GCM加密患者的心电数据。
  3. 固件升级阶段: 使用SHA-256校验固件包的完整性,再用ECC签名验证固件包的来源。

你想想看,如果只用AES,密钥怎么安全地分发给设备?如果只用ECC,加密速度又太慢。所以,混合使用才是王道。我习惯把ECC叫做「钥匙管理员」,AES叫做「数据搬运工」,SHA-256叫做「数据检验员」。各司其职,效率最高。

重要提醒: 密钥管理是整个密码系统的命门。在医疗贴片里,密钥绝对不能硬编码在代码里。我见过太多产品,密钥就写在源码里,反编译一下就暴露了。正确的做法是:使用MCU的OTP区域、安全元件(SE)或者TPM芯片来存储密钥。如果实在没有硬件支持,至少也要用设备唯一ID(比如MCU的UID)派生密钥。

好了,这一章的内容就到这里。密码学在嵌入式里的实现,说白了就是「在资源受限的条件下,找到安全与性能的平衡点」。下一章我们会聊到安全启动和固件签名,到时候会用到今天讲的哈希和ECC知识。嗯,先消化一下这些内容吧。