ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python实现AES-128加密算法:从Rijndael原理到代码实践

Python实现AES-128加密算法:从Rijndael原理到代码实践 简介这是一份基于Rijndael算法实现AES(128)加密/解密功能的Python源码工具包面向学习对称加密原理的初学者以及需要在项目中快速集成加密能力的开发者。工具严格遵循AES-128标准规范采用128位密钥在合理使用下具备较高的安全性支持对任意字节类型文件进行加解密不限于文本且密钥长度限制在16个符号以内。代码提供两种调用方式可将aes128.py作为模块导入到自己的项目中使用也可直接运行main.py进入命令行交互界面完成加解密操作输入输出类型均在文档字符串中有清晰说明。包内共4个文件包含2个Python脚本、1个说明文档及1个gitignore配置压缩包仅6KB结构轻量便于逐行阅读核心加密逻辑并进行二次封装。目前已有218人学习浏览适合作为算法入门参考或小型工具直接使用。1. 从 Rijndael 到 AES-128这个 Python 实现解决什么问题拿 Rijndael 密码算法练手很多人第一反应是去查 OpenSSL 命令其实一个 Python 文件也能把 AES-128 的加解密完整跑通。这个项目把 Rijndael 按 128 位密钥实现成aes128.py再配一个main.py做交互式 CLI。它能解决两类需求在 Python 里加密配置或临时文件不想为一个功能引入 pycryptodome或者你想找一份足够短的代码逐行读懂 S 盒、行移位、列混合这些教材概念。128 位密钥对应 16 字节所以密钥里的符号必须能用单字节表示ord()返回值小于 255中文或 Emoji 直接当密钥会出问题。2. 先看懂 Rijndael密钥扩展与轮函数的字节级运算AES-128 的输入输出都是一个 4 乘 4 的字节矩阵密钥也是 16 字节。加密过程可以概括为「轮函数循环 10 次」最后输出 16 字节密文。这一章只讲和代码直接相关的三件事密钥怎么扩展、轮函数里四个操作分别做什么、以及如何用已知测试向量验证你的实现没有写歪。2.1 为什么是 128 位密钥与 10 轮Rijndael 算法支持 128/192/256 三种密钥长度对应的轮数分别是 10/12/14。这个项目明确只支持 128 位意味着你要传入 16 字节密钥数据分组也是 16 字节。密钥长度决定轮数轮数越多扩散越充分但也越慢。对大多数业务数据来说128 位与 192 位之间的差距在实际攻击成本上几乎可以忽略选择哪版主要是看兼容性要求。密钥长度密钥字节数轮数扩展后密钥长度128 位1610176 字节192 位2412208 字节256 位3214240 字节选型理由128 位是性价比最高的档位不追求超高安全等级时足够用。该实现把长度限制在 16 字节反而省掉了检查三种长度的分支适合教学和内部工具。2.2 密钥扩展从 16 字节到 176 字节的编排AES 每一轮需要一个 16 字节轮密钥10 轮加初始密钥一共 11 组也就是 176 字节。常见做法是把 16 字节密钥先切分成 4 个 4 字节字再迭代 40 轮生成 44 个 4 字节字。每轮的关键动作是 RotWord、SubWord 和轮常数 RCON。# 密钥扩展输入 16 字节输出长度为 11 的子密钥列表 def expand_key(key: bytes, sbox: list[int]) - list[bytes]: assert len(key) 16, AES-128 key must be 16 bytes rcon (0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36) # 初始 4 个字每字 4 字节 w [bytearray(key[i:i 4]) for i in range(0, 16, 4)] for i in range(4, 44): temp bytearray(w[i - 1]) if i % 4 0: # RotWord: 循环左移一个字节 temp temp[1:] temp[:1] # SubWord: 逐个查 S 盒 temp bytearray(sbox[b] for b in temp) # 与轮常数异或只影响第一个字节 temp[0] ^ rcon[i // 4 - 1] # 新的字 前 4 轮之前的字异或 temp w.append(bytearray(a ^ b for a, b in zip(w[i - 4], temp))) return [bytes(w[i]) for i in range(0, 44, 4)]sbox 是 256 字节查找表这里假设已经生成好。rcon 只用了 10 个值索引i // 4 - 1在第 4 轮开始使用。注意zip(w[i - 4], temp)是逐字节异或不要直接对 bytes 做加法。输出是 11 份 16 字节的轮密钥后续每一轮加密取用一份。2.3 加密轮函数四大步骤的 Python 对照16 字节明文按列填充成状态矩阵然后进行 10 轮变换。这里给出常规实现的轮函数骨架def encrypt_block(state: bytearray, round_keys: list[bytes]) - bytes: # 初始轮密钥加 state bytearray(a ^ b for a, b in zip(state, round_keys[0])) # 中间 9 轮完整的四步 for r in range(1, 10): state sub_bytes(state) # 字节代换 state shift_rows(state) # 行移位 state mix_columns(state) # 列混合 state bytearray(a ^ b for a, b in zip(state, round_keys[r])) # 最后一轮不做列混合 state sub_bytes(state) state shift_rows(state) state bytearray(a ^ b for a, b in zip(state, round_keys[10])) return bytes(state)round_keys是上一节expand_key的返回值长度 11每个元素 16 字节。sub_bytes和shift_rows操作的是同一个 16 字节状态行移位本质是索引重排所以源码里经常用查表实现。最后一轮省去列混合不是偷懒而是 Rijndael 设计文档里的标准结构解密时对应逆过程同样要遵循这一结构。验证方法AES 标准文档 FIPS-197 附录 B 有一个经典测试向量密钥为000102...0f明文为001122...ff密文应为69c4e0d86a7b0430d8cdb78070b4c55a。如果你的aes128.py暴露了内部函数可以这样自检from aes128 import encrypt_block, expand_key key bytes.fromhex(000102030405060708090a0b0c0d0e0f) pt bytes.fromhex(00112233445566778899aabbccddeeff) ct encrypt_block(pt, expand_key(key)) assert ct.hex() 69c4e0d86a7b0430d8cdb78070b4c55a print(ok)注意不同实现里expand_key的返回类型不一致有的项目把所有轮密钥拼成一个 176 字节的 bytes有的返回 11 个子密钥列表。当你自检测试向量失败时先确认这里再查sub_bytes和mix_columns的顺序。轮函数只要差一步密文就会全盘错位而这种错位在断言里不会有任何中间提示。3. 导入 aes128.py按字节输入输出才是正路这个库的使用文档很短核心就一句把aes128.py放到项目目录或 PYTHONPATH然后导入。真正容易踩坑的是密钥长度和编码下面把三种调用方式写清楚。3.1 先决定文件放哪项目目录还是 PYTHONPATH最简单的方式是把文件放在当前项目根目录然后直接导入。检查导入是否成功ls -l aes128.py python -c import aes128; print(aes128.__file__)如果文件放在其他路径可以通过 PYTHONPATH 临时加入export PYTHONPATH/your/path/to/aes:$PYTHONPATH python -c from aes128 import encrypt; print(encrypt)PYTHONPATH 环境变量只在当前 shell 生效写进脚本更稳妥的办法是在文件顶部用sys.path.insert(0, /your/path/to/aes)临时追加路径但不建议污染项目路径。放在项目目录是唯一不需要额外配置的方式也是这个压缩包的默认使用姿势。3.2 密钥长度与字符集的硬约束项目说明里写了「密钥应少于 16 个符号」严格说应该是 16 字节。如果你传入少于 16 字节常见处理是用0x00在尾部补齐达到 16 字节如果多于 16 字节大概率触发 AssertionError。这里有一个容易踩的点ord()必须小于 255也就是说密钥只能由单字节的 Latin-1 字符组成。中文的ord()会大于 255存成 bytes 时是多个字节不等价于单符号。因此密钥建议直接用byteskey b0123456789abcdef # 16 字节合法 key2 short-key.encode(latin-1) # 9 字节缺了会补 0如果你确实需要输入一个中文口令先做哈希再截断到 16 字节例如import hashlib key hashlib.sha256(你的口令.encode(utf-8)).digest()[:16]这样得到的密钥是 16 字节且与你输入的中文口令一一对应。注意哈希后密钥长度固定解密时用同样的口令就能恢复。3.3 一段可直接抄的加解密代码假设aes128.py暴露的是函数式接口常见命名是encrypt(plaintext, key)和decrypt(ciphertext, key)。拿到代码后先看一眼def行如果里面是类就改成cipher AES128(key)的调用方式。函数式版本from aes128 import encrypt, decrypt key b0123456789abcdef plaintext bhello Rijndael ciphertext encrypt(plaintext, key) recovered decrypt(ciphertext, key) assert recovered plaintext所有输入输出都是bytes不是字符串。encrypt返回的密文长度一般等于明文长度经分组填充后的长度如果明文长度正好是 16 的整数倍输出长度和输入一致。decrypt会做逆操作最后返回去除填充后的原始字节流。如果源码是类封装则from aes128 import AES128 cipher AES128(key) ciphertext cipher.encrypt(plaintext) recovered cipher.decrypt(ciphertext)类的构造函数通常接收密钥之后复用同一个密钥做多次加解密函数式接口则每次都要把 key 作为参数传进去两者各有适用场景。类封装更接近面向对象习惯函数式写起来更直接。3.4 报错信息与排查方向下表是常见问题按出现频率排序报错或现象原因处理方式AssertionError密钥长度不是 16 字节补足或截断到 16 字节TypeError传入 str 而不是 bytes用.encode(latin-1)转换UnicodeEncodeError密钥含中文或 Emoji改用 ASCII 密钥或先哈希再截断解密后得到乱码密钥不一致或未去掉 PKCS7 填充核对密钥检查解密函数是否自动 unpad密文长度变成 0输入为空 bytes填充逻辑没处理手动补一整个分组再调用空 bytes 是很多 AES 实现会忽略的边界。如果库里没有做填充空输入可能会在分组循环里直接返回空内容。对文件加密来说空文件也应该加密成一整个分组否则无法区分空文件和未加密的失败在正式使用前先用一个空文件跑一次加解密确认输出长度是 16 的倍数。这一点在跑main.py时尤其明显下一章会看到 CLI 的表现。4. main.py 的 CLI交互式加密文件的操作示范虽然多数人用这个项目是 import但main.py提供了一个不用写 Python 也能加密文件的入口。README 说它是个「shy CLI」意思就是没有 argparse 参数全靠input()一步一步问。这在大批量操作时不方便但配合管道重定向依然能脚本化。4.1 启动与交互流程进入项目目录后直接运行python main.py启动后会依次询问操作类型、文件路径、密钥。交互过程中会问你三件事含义如下其中操作类型只有e和d两个合法值传其他字符会走 else 分支导致流程重来。[e]ncrypt or [d]ecrypt? e Input file: ./demo.txt Key: 0123456789abcdef Saved to ./demo.txt.aes输入项典型值说明操作类型e或de加密d解密输入文件./demo.txt路径可以是相对路径或绝对路径密钥0123456789abcdef程序内部同样受 16 字节限制如果你第一次运行时报模块找不到多半是先运行了main.py但aes128.py不在同一个目录。把两个文件放在同一层再试。另外如果你在虚拟环境里运行确认当前目录在sys.path中Python 3 默认不会把脚本所在目录以外的地方加进来。4.2 用 heredoc 自动应答完成文件加密交互式 CLI 的优点是直观缺点是每次都要敲三行。想批处理时可以用 heredoc 把输入一次喂给程序printf hello AES-128\n demo.txt python main.py EOF e demo.txt 0123456789abcdef EOF命令逻辑printf生成一个测试文件heredoc 的三行严格按照main.py的input()顺序排列分别是操作类型、文件名、密钥。如果程序还要求确认覆盖输出之类的额外输入根据实际提示再补一行。运行结束后检查输出ls -l demo.txt demo.txt.aes xxd demo.txt.aes | headdemo.txt.aes就是加密产物内容已经是二进制乱码。解密时把操作类型改成d输入文件改成demo.txt.aes密钥保持一致即可。4.3 CLI 在二进制、空文件和大文件上的表现main.py读取文件时用的是字节流而非文本模式所以对图片、压缩包、SQLite 数据库都一样处理。需要注意三个边界文件类型预期表现注意事项文本文件正常加密/解密用xxd或od查看密文二进制文件正常处理不要用文本编辑器打开密文空文件可能输出 0 字节或 16 字节取决于是否做了填充先确认再用于正式流程超大文件一次性读入内存几百 MB 文件可能内存紧张应改流式读取如果你要把这个 CLI 接入自动化流程建议不要直接用input()交互而是写成命令行参数版本。一个常见做法是复制main.py再加argparse把交互逻辑换成import argparse parser argparse.ArgumentParser() parser.add_argument(action, choices[e, d]) parser.add_argument(input, typestr) parser.add_argument(key, typestr) args parser.parse_args()这段代码替换掉原来的input()就能在 CI 或 shell 脚本里直接传参。因为aes128.py本身不依赖第三方库改造后部署到服务器很轻。5. 进一步加固给 ECB 补上 CBC 与完整性校验到这里你已经能加解密了但要注意如果aes128.py只实现了最基本的 ECB 模式同一明文块永远得到同一密文块这在文件头部有固定结构时可能泄露信息。一个低成本改进是手动包装 CBC 模式把 IV 和密文放一起保存。最小 CBC 加密核心import os from aes128 import encrypt, decrypt iv os.urandom(16) # 每个文件生成一次 prev iv output bytearray(iv) # 密文文件 IV 加密后的分组 for i in range(0, len(plaintext), 16): block plaintext[i:i16] if len(block) 16: block block b\x00 * (16 - len(block)) # 简单补零 block bytes(a ^ b for a, b in zip(block, prev)) enc encrypt(block, key) output enc prev enc解密时先读前 16 字节作为 IV再对每个密文分组解密然后与前一密文分组异或。plaintext必须是bytesencrypt函数返回 16 字节定长密文prev在加密侧是当前分组的密文在解密侧就是前一个密文分组iv不需要保密但要保持随机。补零填充比 PKCS7 简单但解密后无法判断原始长度。更好的做法是记录原始长度或使用 PKCS7。验证时也要防止「解密成功但内容是脏数据」可以把hmac结果附在密文尾部import hmac, hashlib mac hmac.new(key, output, hashlib.sha256).hexdigest() with open(demo.bin, wb) as f: f.write(output mac.encode(ascii))运行时再算一次比较两个 mac 是否相等不等就说明文件被改动或密钥不对。这个技巧特别适合替代简单 CRC 校验因为 HMAC 带密钥伪造不了。本文还有配套的精品资源点击获取
返回列表