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('验证通过:解密文件与原始固件完全一致')
运行与验证
- 将上述代码保存为
aes_firmware.py,在命令行执行python aes_firmware.py。 - 程序会生成测试文件
firmware.bin(400字节),加密后输出firmware.enc,再解密为firmware_dec.bin。 - 通过条件:控制台输出“验证通过:解密文件与原始固件完全一致”,且
firmware.enc内容与原始文件不同(可用十六进制编辑器查看)。
关键说明
- 密钥管理:AES-128密钥长度为16字节。实际OTA系统中,密钥应安全存储在设备安全区或通过安全通道分发,不应硬编码在固件中。
- IV处理:加密时生成随机IV并存储在密文文件头部(前16字节),解密时从文件读取IV。同一密钥下IV不同,确保相同明文加密结果不同。
- 填充方式:使用PKCS7填充,保证任意长度文件均可加密。
- 方案选择:方案A代码简洁、性能好,适合PC端工具;方案B无第三方依赖,逻辑完整,适合理解AES原理或移植到嵌入式环境。