
最近整理了一个挺有意思的小项目自制的 RGB 加解密法。思路不复杂就是拿一张图片的 RGB 像素值去对文本做加解密。你输入一段文字程序读图的像素把像素展开成字节流再和文本字节做异或解密时用同一张图的像素还原。标题里的“鬼脑发力”挺贴切因为它不算正经密码算法更像一个把文本和图片强行缝合在一起的实验玩具。先给结论这玩意能跑能加解密过程也挺好玩但绝不能用来保护真实数据。它真正适合的场景是学习、练手、做课堂作业或者作为一个有话题性的开源小项目。下面按我实际跑通的过程把思路、代码、坑和开源发布建议都拆开讲。1. 这套 RGB 加解密到底在做什么适合谁玩1.1 名字听起来很玄本质是字节流变换“RGB 加解密”这个叫法容易让人误以为是一种新的密码学标准。实际拆开看就是把三样东西串起来文本要加密的内容先编码成字节。图片每个像素都有 R、G、B 三个分量取值都是 0 到 255。字节流把像素分量拉直就得到一串数字本质上就是字节。加密时程序把文本字节和图片像素字节做某种可逆运算得到密文解密时再拿同一张图片的像素字节做逆运算把密文变回原文。整个过程不神秘核心就是字节和字节之间的变换。1.2 适合人群和学习价值这个项目适合三类人Python 初学者可以从头到尾看到文本、字节、进制、异或、图片像素这些概念怎么在实际代码里串起来。图像处理入门者想搞清楚 PIL、OpenCV 怎么读取像素RGB 和 BGR 到底有什么区别。想给开源仓库攒一个有趣项目的人这个主题有话题性写 README 时比较好讲清楚。不推荐把它当安全工具用。后面我会专门解释原因。2. 核心思路拆解文本、字节、RGB 三者怎么串起来2.1 从文本到字节Python 里一行代码就能把字符串转成字节plain 你好世界 data plain.encode(utf-8) print(data)加密之后再想还原就调用decode(utf-8)。这里用 UTF-8 而不是 GBK是为了让中文在不同系统之间保持一致也避免表情符号这类字符编码失败。2.2 从 RGB 到字节PIL 读一张图每个像素是 (R, G, B) 三元组from PIL import Image img Image.open(key.png).convert(RGB) pixels list(img.getdata()) print(pixels[:3])三元组展开后就是一长串 0 到 255 的整数可以直接当成字节来用。2.3 两种方向RGB 当钥匙还是 RGB 当密文我整理思路时发现“RGB 加解密”可以往两个方向做都有意思方向思路密文形态RGB 当钥匙用图片像素字节生成密钥流和文本字节做异或一段 HEX 字符串RGB 当密文把文本加解密后的结果按三个字节一组写进像素一张长得像噪点的 PNG 图片两种方向代码都不长下面分别给出一个可运行的示例版本。3. 本地环境准备Python 读取图片 RGB 值的正确姿势3.1 安装依赖本地环境我用的是 Python 3.10核心依赖只需要 Pillow。顺手装上 NumPy 和 OpenCV方便对比不同读取方式。pip install pillow numpy opencv-python如果只是跑下面的示例执行pip install pillow就够了。3.2 三种读取 RGB 的方式方式一PILfrom PIL import Image img Image.open(key.png).convert(RGB) pixels list(img.getdata())方式二NumPyimport numpy as np arr np.array(Image.open(key.png).convert(RGB)) print(arr.shape) # (height, width, 3)方式三OpenCVimport cv2 img cv2.imread(key.png) rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB)3.3 RGB 和 BGR 的坑OpenCV 默认读出来是 BGR不是 RGB。如果直接把img[:, :, 0]当 R 通道通道顺序就反了。解决办法是cv2.cvtColor(img, cv2.COLOR_BGR2RGB)或者用img[:, :, ::-1]把通道倒过来。还有一个更隐蔽的坑很多图片查看器显示正常但像素实际做过色彩管理。做实验时最好统一用 PNG不要用 JPEG。JPEG 是有损压缩同一张图保存两次像素值就可能变密钥就变了。我用的是自己生成的 256x256 纯色测试图先把格式问题排除掉。4. 第一版实现把图片 RGB 当密钥流对文本做异或4.1 生成密钥流的思路最简单的做法是把图片像素字节拉长重复和文本字节逐位异或。但这样做有一个明显弱点如果图片是纯色块或大面积渐变密钥流会有规律密文会出现可观察的重复模式。我用的思路是先把图像素字节做 SHA-256得到 32 字节种子再用种子加计数器不断做哈希展开成足够长的密钥流。这个思路接近“用哈希构造流密码”但只是为了好玩不代表它安全。import hashlib def derive_key_stream(pixel_bytes: bytes, data_len: int) - bytes: seed hashlib.sha256(pixel_bytes).digest() stream b counter 0 while len(stream) data_len: stream hashlib.sha256(seed counter.to_bytes(8, big)).digest() counter 1 return stream[:data_len]counter.to_bytes(8, big)的作用是让每次哈希输入都不同避免生成重复块。4.2 加密和解密函数from PIL import Image def load_pixel_bytes(image_path: str, size(256, 256)) - bytes: img Image.open(image_path).convert(RGB).resize(size) pixels list(img.getdata()) return bytes(v for pixel in pixels for v in pixel) def xor_encrypt(text: str, image_path: str) - str: data text.encode(utf-8) stream derive_key_stream(load_pixel_bytes(image_path), len(data)) cipher bytes([a ^ b for a, b in zip(data, stream)]) return cipher.hex() def xor_decrypt(hex_cipher: str, image_path: str) - str: cipher bytes.fromhex(hex_cipher) stream derive_key_stream(load_pixel_bytes(image_path), len(cipher)) data bytes([a ^ b for a, b in zip(cipher, stream)]) return data.decode(utf-8)加密和解密都依赖同一个image_path。解密必须使用同一张图换成任何其他图片密钥流就不同解出来就是乱码。4.3 用一张测试图验证key_image key.png origin 这是一段用来测试 RGB 加解密的中文文本。 cipher_text xor_encrypt(origin, key_image) print(cipher_text) plain_text xor_decrypt(cipher_text, key_image) print(plain_text)如果输出末尾能看到原始中文说明流程通了。我建议先跑这一版再去看下一版因为这一版已经把“图片像素”和“文本字节”的关系讲清楚了。5. 第二版实现把密文写进 RGB 像素生成一张噪点图5.1 文本转 RGB 图片第二版更视觉化把文本字节按三个一组塞进像素的 R、G、B 通道生成一张图片。为了还原时不丢尾部空字节我在开头用 4 个字节记录原始数据长度。import struct from PIL import Image def text_to_rgb_image(text: str, output_path: str, width128): data text.encode(utf-8) header struct.pack(I, len(data)) payload header data if len(payload) % 3 ! 0: payload b\x00 * (3 - len(payload) % 3) pixels [tuple(payload[i:i3]) for i in range(0, len(payload), 3)] height (len(pixels) width - 1) // width pixels [(0, 0, 0)] * (width * height - len(pixels)) img Image.new(RGB, (width, height)) img.putdata(pixels) img.save(output_path) print(foutput: {output_path}, size: {width}x{height})struct.pack(I, len(data))用 4 字节大端整数存长度。这一步很重要否则尾部填充的\x00会被当成正常数据导致中文解码失败。5.2 从 RGB 图片还原文本def rgb_image_to_text(image_path: str) - str: img Image.open(image_path).convert(RGB) pixels list(img.getdata()) payload b.join(bytes(pixel) for pixel in pixels) data_len struct.unpack(I, payload[:4])[0] data payload[4:4 data_len] return data.decode(utf-8)逻辑是先读长度再按长度截取剩下的填充字节直接丢弃。5.3 验证方法和边界origin Hello, RGB Cipher! 这是一张会说话的图片。 text_to_rgb_image(origin, cipher.png, width100) restored rgb_image_to_text(cipher.png) print(restored origin) # True生成出来的 cipher.png 看起来就是一堆彩色噪点因为任意字节映射到 RGB 后基本没有视觉规律。这个效果本身也适合当演示素材。边界情况有三点空文本长度是 0生成一张只有头部 4 个字节的图可以正常运行。非 ASCII 文本只要编码用 UTF-8中文、日文、表情符号都能放。超长文本图片会变高建议控制文本长度在几 KB 级别或者适当加大 width。6. 性能和安全性和 AES、3DES 比差距在哪里6.1 速度差距很大而且不在一个量级有人会拿这个项目和“3des 与 aes 加解密速度对比”放一起讨论。实际上AES 在主流 CPU 上有硬件加速速度非常快3DES 属于老算法虽然不如 AES但也比纯 Python 循环快很多。而这个 RGB 方案每次加解密要经历打开图片文件转 RGB、缩放、展开像素做 SHA-256 展开密钥流再逐字节异或。其中图片 I/O 和哈希展开都是 Python 层耗时。我拿一张 256x256 的 PNG 当密钥加密几千字节文本耗时在毫秒级到几十毫秒之间。单次看不算慢但放到批量任务里几百次调用叠加起来就很明显。一句话这个项目适合教学和实验不适合做高性能数据通道。6.2 为什么不建议用于真实安全场景必须把话说清楚这是自制算法不是密码学意义上的安全方案。问题至少有四层算法没有公开审计。我写的时候优先保证“能跑通、能还原”没有考虑侧信道、时序攻击、选择明文攻击这些模型。密钥本质是一张图片。图片可能被复制也可能带有可预测内容密钥熵不稳定。没有完整性校验。密文被篡改解密出来就是乱码程序不会主动发现“数据被改了”。没有标准填充和认证机制。工程上要求的安全边界这个项目都没有。真实保护数据建议用成熟库from cryptography.hazmat.primitives.ciphers.aead import AESGCM或者直接用 GPG、age 这类工具。自制算法最大的价值是帮你理解加解密的基本原理而不是替代标准算法。6.3 哪些场景可以放心玩课堂作业和课程设计把原理讲清楚。给朋友做一个“用照片解锁文本”的小谜题。用文本生成艺术噪点图再还原文字作为编程分享的演示。作为学习开源项目维护的载体练习 README、许可证、issue 管理。7. 开源发布仓库结构、许可证和 README 建议7.1 仓库结构既然是开源项目就不要只丢一个 .py 文件。我建议仓库结构这样放rgb-cipher/ ├── README.md ├── LICENSE ├── requirements.txt ├── rgb_cipher.py ├── examples/ │ ├── key.png │ ├── demo_encode.py │ └── demo_decode.py ├── tests/ │ └── test_rgb_cipher.py └── .gitignore.gitignore至少要排除__pycache__/、.venv/、.idea/、.vscode/。不然别人克隆下来第一眼看到一堆缓存文件观感很差。7.2 许可证怎么选仓库里有代码就要有 LICENSE 文件。常见选择许可证特点适合场景MIT允许商用、修改、再发布只要保留版权声明玩具项目、教学项目Apache-2.0类似 MIT额外包含专利授权条款想考虑专利风险的开发者GPL-3.0要求衍生作品同样开源希望别人改完也必须开源这类 RGB 加解密项目我建议直接选 MIT。最省事别人拿去改、拿去演示都不需要专门来问你。7.3 README 怎么写README 至少要回答四个问题这个项目能干什么用文本生成 RGB 图片或用图片当密钥做异或加解密。怎么安装给出pip install -r requirements.txt。怎么运行给出完整命令行示例。有什么限制不是安全算法不要用于真实数据。可以放一张生成出来的噪点图当截图视觉上很直观。8. 常见报错与排查顺序8.1 图片打不开、像素读不全先看路径。路径有中文、空格或者相对路径和运行目录不一致都容易报FileNotFoundError。其次看图片格式统一换成 PNG 再试。最后看图片模式Image.open()之后一定要.convert(RGB)否则灰度图或 RGBA 图的通道数不一样展开成字节时会出问题。8.2 解密出来乱码乱码是这类项目最高频的问题按这个顺序排查密钥流不一致换过图片、图片被缩放、图片被重新保存都会导致像素值变化。长度不一致密文 HEX 被截断或拼接错误。编码不一致加密时用 UTF-8解密时也要用 UTF-8。填充处理错误第二版里如果没读头部长度直接用rstrip(b\x00)文本末尾原本有\x00时会被误删。我一般会先打印密文长度和密钥流长度再打印解密后的字节。如果能看到b\xe4\xbd\xa0这样中文 UTF-8 前缀说明字节流对上了问题出在最后的 decode 环节。8.3 和图片处理相关的其他坑如果做图像处理时遇到“RGB 转 YCbCr 为负数”的问题先别急着当异常处理。用 BT.601 或 BT.709 的矩阵做 RGB 转 YCbCr 时Cb、Cr 在部分色域下出现负数是正常的。显示时一般要位移或裁剪到 0 到 255但做计算时不要随便把负数裁掉否则颜色会偏。放到这个项目里的提示是凡是基于像素做加解密像素值必须严格保持 0 到 255 的原始字节范围不要做任何裁剪、归一化、色彩空间转换。任何一步改变像素值都等于改变密钥。8.4 内存和性能问题如果把一张几千万像素的大图直接list(img.getdata())内存会涨得很快。建议先resize((256, 256))再用。如果文本很长密钥流展开时间也会明显上升可以先跑小段文本确认逻辑正确再考虑扩展。这轮玩下来的感受是所谓“RGB 加解密”本质就是文本字节和图片字节互相转换的实验。它最大的价值不是发明了新算法而是把编码、字节、异或、像素、文件 I/O 这些零散知识点串在了一起。如果想继续深入可以把两个方向合并先用图片像素生成密钥流再把密文写进另一张图片玩法会更有意思。唯一要记住的是真正要保护数据时请回到 AES-GCM 这类标准方案而不是自己拍脑袋拼一个字节变换。