4. 固件镜像加密与解密

任务目标

在OTA固件升级场景中,镜像文件在传输或存储时可能被窃取或篡改。本案例使用AES-128-CBC算法对固件镜像(bin文件)进行加密与解密,确保镜像的机密性。加密后的镜像可通过不安全信道传输,解密后还原为原始固件。

环境与依赖

  • Python 3.6+(无需第三方库,使用标准库 hashlib 与 os 实现AES-128-CBC)
  • 或使用 pycryptodome 库(推荐,代码更简洁):pip install pycryptodome

实现方案

本案例提供两种实现方式:方案A 使用 pycryptodome 库(生产推荐);方案B 使用Python标准库纯手写AES-128-CBC(无第三方依赖,适合资源受限环境参考)。两种方案均实现相同的加密/解密接口,可互换使用。

关键完整代码

# ========== 方案A:使用 pycryptodome(推荐) ==========
# 安装:pip install pycryptodome
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os

def aes_encrypt_file(key: bytes, input_path: str, output_path: str):
    """AES-128-CBC加密文件,输出格式:16字节IV + 密文"""
    iv = os.urandom(16)  # 随机IV
    cipher = AES.new(key, AES.MODE_CBC, iv)
    with open(input_path, 'rb') as f:
        plaintext = f.read()
    ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
    with open(output_path, 'wb') as f:
        f.write(iv + ciphertext)  # IV与密文一起存储

def aes_decrypt_file(key: bytes, input_path: str, output_path: str):
    """解密由aes_encrypt_file生成的文件"""
    with open(input_path, 'rb') as f:
        data = f.read()
    iv, ciphertext = data[:16], data[16:]
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
    with open(output_path, 'wb') as f:
        f.write(plaintext)

# ========== 方案B:纯标准库实现(无第三方依赖) ==========
# 适用于无法安装pycryptodome的环境,AES-128-CBC完整实现
import os, hashlib

# --- AES核心表 ---
SBOX = [
    0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
    0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
    0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
    0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
    0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
    0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
    0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
    0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
    0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
    0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
    0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
    0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
    0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
    0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
    0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
    0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
]
INV_SBOX = [0]*256
for _i in range(256):
    INV_SBOX[SBOX[_i]] = _i
RCON = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]

def _xtime(a): return ((a << 1) ^ 0x1b) & 0xff if (a & 0x80) else (a << 1)
def _mul(a, b):
    res = 0
    while b:
        if b & 1: res ^= a
        a = _xtime(a); b >>= 1
    return res

def _key_expansion(key: bytes) -> list:
    """AES-128密钥扩展,返回44个字的轮密钥"""
    w = [list(key[4*i:4*i+4]) for i in range(4)]
    for i in range(4, 44):
        temp = w[i-1][:]
        if i % 4 == 0:
            temp = temp[1:] + temp[:1]  # RotWord
            temp = [SBOX[b] for b in temp]  # SubWord
            temp[0] ^= RCON[i//4 - 1]  # Rcon
        w.append([w[i-4][j] ^ temp[j] for j in range(4)])
    return w

def _add_round_key(state, w, rnd):
    for c in range(4):
        for r in range(4):
            state[r][c] ^= w[rnd*4+c][r]

def _sub_bytes(state, inv=False):
    sb = INV_SBOX if inv else SBOX
    for r in range(4):
        for c in range(4):
            state[r][c] = sb[state[r][c]]

def _shift_rows(state, inv=False):
    for r in range(1, 4):
        if inv:
            state[r] = state[r][-r:] + state[r][:-r]
        else:
            state[r] = state[r][r:] + state[r][:r]

def _mix_columns(state, inv=False):
    for c in range(4):
        a = [state[r][c] for r in range(4)]
        if inv:
            state[0][c] = _mul(a[0],14) ^ _mul(a[1],11) ^ _mul(a[2],13) ^ _mul(a[3],9)
            state[1][c] = _mul(a[0],9) ^ _mul(a[1],14) ^ _mul(a[2],11) ^ _mul(a[3],13)
            state[2][c] = _mul(a[0],13) ^ _mul(a[1],9) ^ _mul(a[2],14) ^ _mul(a[3],11)
            state[3][c] = _mul(a[0],11) ^ _mul(a[1],13) ^ _mul(a[2],9) ^ _mul(a[3],14)
        else:
            state[0][c] = _mul(a[0],2) ^ _mul(a[1],3) ^ a[2] ^ a[3]
            state[1][c] = a[0] ^ _mul(a[1],2) ^ _mul(a[2],3) ^ a[3]
            state[2][c] = a[0] ^ a[1] ^ _mul(a[2],2) ^ _mul(a[3],3)
            state[3][c] = _mul(a[0],3) ^ a[1] ^ a[2] ^ _mul(a[3],2)

def _encrypt_block(block: bytes, w: list) -> bytes:
    state = [[block[r+4*c] for r in range(4)] for c in range(4)]
    _add_round_key(state, w, 0)
    for rnd in range(1, 10):
        _sub_bytes(state); _shift_rows(state); _mix_columns(state); _add_round_key(state, w, rnd)
    _sub_bytes(state); _shift_rows(state); _add_round_key(state, w, 10)
    return bytes(state[r][c] for c in range(4) for r in range(4))

def _decrypt_block(block: bytes, w: list) -> bytes:
    state = [[block[r+4*c] for r in range(4)] for c in range(4)]
    _add_round_key(state, w, 10)
    for rnd in range(9, 0, -1):
        _inv_shift_rows(state); _inv_sub_bytes(state); _add_round_key(state, w, rnd); _inv_mix_columns(state)
    _inv_shift_rows(state); _inv_sub_bytes(state); _add_round_key(state, w, 0)
    return bytes(state[r][c] for c in range(4) for r in range(4))

def _inv_shift_rows(state): _shift_rows(state, inv=True)
def _inv_sub_bytes(state): _sub_bytes(state, inv=True)
def _inv_mix_columns(state): _mix_columns(state, inv=True)

def _xor_bytes(a: bytes, b: bytes) -> bytes:
    return bytes(x ^ y for x, y in zip(a, b))

def _cbc_encrypt(data: bytes, key: bytes, iv: bytes) -> bytes:
    w = _key_expansion(key)
    padded = data + b'\x00' * ((16 - len(data) % 16) % 16)  # PKCS7填充
    prev = iv; out = b''
    for i in range(0, len(padded), 16):
        block = _xor_bytes(padded[i:i+16], prev)
        enc = _encrypt_block(block, w)
        out += enc; prev = enc
    return out

def _cbc_decrypt(data: bytes, key: bytes, iv: bytes) -> bytes:
    w = _key_expansion(key)
    prev = iv; out = b''
    for i in range(0, len(data), 16):
        block = data[i:i+16]
        dec = _decrypt_block(block, w)
        out += _xor_bytes(dec, prev)
        prev = block
    # 去除PKCS7填充
    pad_len = out[-1]
    return out[:-pad_len] if 0 < pad_len <= 16 else out

def aes_encrypt_file_std(key: bytes, input_path: str, output_path: str):
    iv = os.urandom(16)
    with open(input_path, 'rb') as f: plaintext = f.read()
    ciphertext = _cbc_encrypt(plaintext, key, iv)
    with open(output_path, 'wb') as f: f.write(iv + ciphertext)

def aes_decrypt_file_std(key: bytes, input_path: str, output_path: str):
    with open(input_path, 'rb') as f: data = f.read()
    iv, ciphertext = data[:16], data[16:]
    plaintext = _cbc_decrypt(ciphertext, key, iv)
    with open(output_path, 'wb') as f: f.write(plaintext)

# ========== 使用示例(两种方案通用) ==========
if __name__ == '__main__':
    # 16字节密钥(AES-128)
    key = b'0123456789abcdef'
    # 生成测试固件
    with open('firmware.bin', 'wb') as f:
        f.write(b'\x00\x01\x02\x03' * 100)  # 400字节测试数据

    # 方案A调用(若已安装pycryptodome)
    try:
        aes_encrypt_file(key, 'firmware.bin', 'firmware.enc')
        aes_decrypt_file(key, 'firmware.enc', 'firmware_dec.bin')
        print('方案A完成')
    except ImportError:
        # 方案B调用(纯标准库)
        aes_encrypt_file_std(key, 'firmware.bin', 'firmware.enc')
        aes_decrypt_file_std(key, 'firmware.enc', 'firmware_dec.bin')
        print('方案B完成')

    # 验证:比较原始文件与解密文件
    with open('firmware.bin', 'rb') as f1, open('firmware_dec.bin', 'rb') as f2:
        assert f1.read() == f2.read(), '解密结果不一致!'
    print('验证通过:解密文件与原始固件完全一致')

运行与验证

  1. 将上述代码保存为 aes_firmware.py,在命令行执行 python aes_firmware.py。
  2. 程序会生成测试文件 firmware.bin(400字节),加密后输出 firmware.enc,再解密为 firmware_dec.bin。
  3. 通过条件:控制台输出“验证通过:解密文件与原始固件完全一致”,且 firmware.enc 内容与原始文件不同(可用十六进制编辑器查看)。

关键说明

  • 密钥管理:AES-128密钥长度为16字节。实际OTA系统中,密钥应安全存储在设备安全区或通过安全通道分发,不应硬编码在固件中。
  • IV处理:加密时生成随机IV并存储在密文文件头部(前16字节),解密时从文件读取IV。同一密钥下IV不同,确保相同明文加密结果不同。
  • 填充方式:使用PKCS7填充,保证任意长度文件均可加密。
  • 方案选择:方案A代码简洁、性能好,适合PC端工具;方案B无第三方依赖,逻辑完整,适合理解AES原理或移植到嵌入式环境。