本篇目录
最近一直在更新我的工具小程序,想要增加一个图片加密功能,就是把一张图片进行加密后还是一张图片,只是内容完全看不出来任何有用的信息,只有正确输入解密密码后才能查看图片。为了不要太多增加加密后图片的大小,第一个想到的就是使用凯撒密码算法来实现图片加密(当然是我提想法,AI负责代码实现),在与DeepSeek多轮沟通后最终确认了小程序上图片加密的实现。
一、最初的想法:凯撒密码的像素版本
凯撒密码是最古老的加密方式之一——通过将字母按固定位移来加密文本。把这个思想应用到图片上,我很自然地提出了这个图片加密方案:把图片的像素进行位移轮转。

等DS写完代码后测试发现,单纯按单个像素进行凯撒位移,加密后的图片仍然能看出原始的大致轮廓。这是因为自然图像中相邻像素通常具有较高的相关性,简单的位移无法彻底打乱这种相关性。

二、分块凯撒:从像素到块
为了增强混淆效果,又让DS尝试了分块凯撒位移——将图片切成固定大小的小块(如 8×8 或 16×16),然后对这些块进行循环位移。

这个方案在混淆程度上有所提升,但对于大面积同色区域的图片,仍然能看出原始内容的轮廓。问题的根源在于:只改变位置而不改变像素值,统计特征依然保留。

三、双重加密:位置 + 颜色值
继续沟通后,又陆续尝试使用块位移 + RGB 值凯撒轮转的双重加密方案、增强型混沌凯撒变体 效果还是不够理想,要么就是不能还原。


到这里解密还是不行,已经倾向于放弃凯撒算法及变种算法了,就想着让它借鉴碎纸机切分来实现图片打乱。

这个版本生成的结果会有冗余信息,看起来比之前好多了,但是展示的可用信息还是有点多,不够安全。

四、Arnold 猫映射的尝试
后来我直接放弃了自己指导DS进行图片加密的想法了,直接让它推荐不就行了,为什么要我这个外行指导内行呢?当时就直接选了它推荐的Arnold 方案。

Arnold 猫映射是一种经典的图像置乱算法,通过矩阵变换将像素位置彻底打乱。它有一个优雅的特性:经过一定次数的迭代后,图像会自动恢复原状(周期性)。然而,Arnold 映射要求图像是正方形的。对于非正方形的图片,需要进行填充处理。

这个方案测试后发觉效果是真不错,加密后图片完全看不清,然后解密也能正常解出来。不出意外的话居然出意外了,就在我让它改完交互后发现我随便填写什么密码都能解析出“原图”只是没那么清晰而已!


真的想放弃了,深深怀疑是DeepSeek 能力不行还是我提示词不对?
五、最终方案:纯位置置乱
中间又沟通了几轮,包括使用AES-GCM + 图像编码方案,还是达不到预期验收效果,这里放出最后的方案(中间的N过程已忽略,太累人了)。
- 只改变像素位置,不改变像素值——这样即使像素值有微小偏移也不影响解密
- 使用 Fisher-Yates 洗牌算法——基于密码生成确定性的置乱映射
- 多轮置乱——增强混淆效果,每轮使用不同的密码种子
- 完全可逆——解密时使用相同的密码种子生成逆映射

解密也正常,而且密码错误解密出来的还是这种杂色图片。这种加密的映射表通过密码 + 盐值 + 轮数确定性地生成,确保加密和解密使用完全相同的映射。

该版本html源码
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>图片加密 - 真正独立的加密解密</title><style>* { margin: 0; padding: 0; box-sizing: border-box; }body {background: #0a0f1a;min-height: 100vh;display: flex;justify-content: center;align-items: center;font-family: 'Segoe UI', system-ui, sans-serif;padding: 20px;}.container {max-width: 1000px;width: 100%;background: rgba(16,20,35,0.96);border-radius: 2rem;padding: 2rem;box-shadow: 0 30px 60px rgba(0,0,0,0.9);border: 1px solid rgba(200,170,80,0.3);color: #d4dce8;}h1 { text-align: center; color: #e8c454; font-size: 1.5rem; margin-bottom: 0.3rem; }.subtitle { text-align: center; color: #8899aa; margin-bottom: 1rem; font-size: 0.8rem; }.tabs {display: flex; gap: 0.4rem; justify-content: center; margin-bottom: 1rem;}.tab {padding: 0.5rem 1.5rem; border-radius: 2rem;background: #1a1a35; border: 1px solid #445566;color: #8899aa; cursor: pointer; font-size: 0.8rem;}.tab.active { background: #4a5a2a; border-color: #bfa34e; color: #fff; }.upload-zone {border: 2px dashed #445566; border-radius: 1.5rem;padding: 1.5rem; text-align: center; cursor: pointer; margin-bottom: 1rem;}.upload-zone:hover { border-color: #d4a838; }.upload-zone.drag { border-color: #ffd700; background: rgba(40,40,20,0.3); }.preview-row {display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem; margin: 1rem 0;}.preview-box {background: #151530; border-radius: 1rem; padding: 0.6rem; text-align: center;}.preview-box h3 { font-size: 0.7rem; color: #ad9755; margin-bottom: 0.4rem; }canvas { width: 100%; height: auto; border-radius: 0.5rem; background: #000; }.controls { display: flex; flex-direction: column; gap: 0.7rem; }.input-row {display: flex; gap: 0.5rem; background: #1a1a35;border-radius: 3rem; padding: 0.3rem 0.3rem 0.3rem 1.5rem; border: 1px solid #334455;}.input-row input {flex: 1; background: transparent; border: none;padding: 0.8rem; font-size: 1rem; color: #f0e6c8; outline: none;}.params {display: flex; gap: 1rem; flex-wrap: wrap;font-size: 0.72rem; color: #8899aa; align-items: center;}.params select, .params input[type="number"] {background: #1a1a35; color: #dcc77c;border: 1px solid #445566; border-radius: 0.4rem; padding: 0.2rem; text-align: center;}.params input[type="number"] { width: 50px; }.btn-row {display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center;}.btn {padding: 0.7rem 1.3rem; border-radius: 2.5rem;border: 1px solid #445566; background: #1a2a3a;color: #e5daaa; cursor: pointer; font-weight: 600;font-size: 0.78rem; white-space: nowrap;}.btn:hover { background: #253545; }.btn.primary { background: #4a5a2a; border-color: #bfa34e; color: #fff; }.btn.success { background: #2a4a3a; border-color: #5a8a5a; }.status {text-align: center; padding: 0.5rem; border-radius: 1rem;font-size: 0.75rem; min-height: 2rem; margin-top: 0.5rem;}.status.info { background: rgba(100,150,200,0.15); color: #8ab4d8; }.status.success { background: rgba(50,200,100,0.15); color: #8fbc8f; }.status.error { background: rgba(200,50,50,0.15); color: #ff6b6b; }.info-text { font-size: 0.62rem; color: #667788; margin-top: 0.2rem; }</style></head><body><div class="container"><h1>🔐 像素级图片加密</h1><div class="subtitle">密码决定一切 · 加密解密完全独立 · 下载后可解密</div><div class="tabs"><div class="tab active" id="tabEnc">🔒 加密</div><div class="tab" id="tabDec">🔓 解密</div></div><div class="upload-zone" id="uploadZone"><div style="font-size:2rem;">📁</div><p id="uploadLabel">点击或拖放图片</p><input type="file" id="fileInput" accept="image/*" hidden></div><div class="preview-row"><div class="preview-box"><h3 id="labelIn">📷 输入</h3><canvas id="canvasIn"></canvas><div class="info-text" id="infoIn"></div></div><div class="preview-box"><h3 id="labelOut">🔐 输出</h3><canvas id="canvasOut"></canvas><div class="info-text" id="infoOut"></div></div></div><div class="controls"><div class="input-row"><span>🔑</span><input type="password" id="pwdInput" placeholder="输入密码" autocomplete="off"></div><div class="params"><label>轮数: <input type="number" id="rounds" value="3" min="1" max="10"></label><label>模式:<select id="modeSelect"><option value="standard">标准</option><option value="strong">强加密</option></select></label><span style="color:#cfb87c;">🔐 FINAL_V10</span></div><div class="btn-row"><button class="btn primary" id="btnGo">🔒 加密</button><button class="btn success" id="btnVerify">✅ 验证可逆</button><button class="btn" id="btnDownload">⬇️ 下载</button><button class="btn" id="btnReset">🔄 重置</button></div></div><div class="status info" id="statusBar">就绪 - 请上传图片并输入密码</div></div><script>(function() {const SALT = "FINAL_SALT_V10";const tabEnc = document.getElementById('tabEnc');const tabDec = document.getElementById('tabDec');const uploadZone = document.getElementById('uploadZone');const uploadLabel = document.getElementById('uploadLabel');const fileInput = document.getElementById('fileInput');const canvasIn = document.getElementById('canvasIn');const canvasOut = document.getElementById('canvasOut');const labelIn = document.getElementById('labelIn');const labelOut = document.getElementById('labelOut');const pwdInput = document.getElementById('pwdInput');const roundsInput = document.getElementById('rounds');const modeSelect = document.getElementById('modeSelect');const btnGo = document.getElementById('btnGo');const btnVerify = document.getElementById('btnVerify');const btnDownload = document.getElementById('btnDownload');const statusBar = document.getElementById('statusBar');const infoIn = document.getElementById('infoIn');const infoOut = document.getElementById('infoOut');const ctxIn = canvasIn.getContext('2d', { willReadFrequently: true });const ctxOut = canvasOut.getContext('2d', { willReadFrequently: true });let mode = 'encrypt';let inputImage = null;let outputImage = null;// ============ 模式切换 ============function setMode(m) {mode = m;tabEnc.classList.toggle('active', m === 'encrypt');tabDec.classList.toggle('active', m === 'decrypt');if (m === 'encrypt') {uploadLabel.textContent = '点击或拖放原始图片';labelIn.textContent = '📷 原始图片';labelOut.textContent = '🔒 加密图片';btnGo.textContent = '🔒 加密';} else {uploadLabel.textContent = '点击或拖放加密图片';labelIn.textContent = '🔒 加密图片';labelOut.textContent = '🔓 解密图片';btnGo.textContent = '🔓 解密';}resetAll();status(m === 'encrypt' ? '加密模式 - 上传原图' : '解密模式 - 上传加密图(需相同密码和参数)', 'info');}function resetAll() {inputImage = null; outputImage = null;canvasIn.width = 0; canvasIn.height = 0;canvasOut.width = 0; canvasOut.height = 0;infoIn.textContent = ''; infoOut.textContent = '';}function status(msg, type = 'info') {statusBar.textContent = msg;statusBar.className = 'status ' + type;}tabEnc.addEventListener('click', () => setMode('encrypt'));tabDec.addEventListener('click', () => setMode('decrypt'));// ============ 核心算法 ============// 确定性哈希function hash(str) {let h = 0x811c9dc5;for (let i = 0; i < str.length; i++) {h ^= str.charCodeAt(i);h = Math.imul(h, 0x01000193) >>> 0;}return h >>> 0;}// 创建随机数生成器function createRNG(seed) {let s = seed >>> 0;return {next: function() { s = (s * 1664525 + 1013904223) >>> 0; return s; },nextInt: function(max) { return max <= 0 ? 0 : this.next() % max; }};}// 生成置乱映射(基于密码和轮数,完全确定性)function generateShuffleMap(totalPixels, password, round) {const seed = hash(password + SALT + "_MAP_" + round);const rng = createRNG(seed);const map = new Uint32Array(totalPixels);for (let i = 0; i < totalPixels; i++) map[i] = i;for (let i = totalPixels - 1; i > 0; i--) {const j = rng.nextInt(i + 1);const tmp = map[i];map[i] = map[j];map[j] = tmp;}return map;}// 生成逆映射function generateInverseMap(map) {const inverse = new Uint32Array(map.length);for (let i = 0; i < map.length; i++) {inverse[map[i]] = i;}return inverse;}// 应用置乱(加密方向:像素i移到map[i]位置)function applyShuffle(pixels, map) {const result = new Uint8ClampedArray(pixels.length);const totalPixels = map.length;for (let i = 0; i < totalPixels; i++) {const src = i * 4;const dst = map[i] * 4;result[dst] = pixels[src];result[dst+1] = pixels[src+1];result[dst+2] = pixels[src+2];result[dst+3] = pixels[src+3];}return result;}// 应用逆置乱(解密方向:从map[i]位置取回像素到i)function applyUnshuffle(pixels, map) {const result = new Uint8ClampedArray(pixels.length);const totalPixels = map.length;for (let i = 0; i < totalPixels; i++) {const src = map[i] * 4;const dst = i * 4;result[dst] = pixels[src];result[dst+1] = pixels[src+1];result[dst+2] = pixels[src+2];result[dst+3] = pixels[src+3];}return result;}// XOR加密function xorEncrypt(pixels, password, round) {const seed = hash(password + SALT + "_XOR_" + round);const rng = createRNG(seed);const result = new Uint8ClampedArray(pixels.length);for (let i = 0; i < pixels.length; i += 4) {result[i] = pixels[i] ^ (rng.next() & 0xFF);result[i+1] = pixels[i+1] ^ (rng.next() & 0xFF);result[i+2] = pixels[i+2] ^ (rng.next() & 0xFF);result[i+3] = pixels[i+3];}return result;}// 扩散(强加密模式)function diffuse(pixels, password, round) {const seed = hash(password + SALT + "_DIF_" + round);const rng = createRNG(seed);const result = new Uint8ClampedArray(pixels.length);result[0]=pixels[0]; result[1]=pixels[1]; result[2]=pixels[2]; result[3]=pixels[3];for (let i = 4; i < pixels.length; i += 4) {const factor = (rng.next() & 0x7F) + 1;for (let j = 0; j < 3; j++) {result[i+j] = (pixels[i+j] + result[i-4+j] * factor) & 0xFF;}result[i+3] = pixels[i+3];}return result;}// 逆扩散function undiffuse(pixels, password, round) {const seed = hash(password + SALT + "_DIF_" + round);const rng = createRNG(seed);const result = new Uint8ClampedArray(pixels.length);result[0]=pixels[0]; result[1]=pixels[1]; result[2]=pixels[2]; result[3]=pixels[3];for (let i = 4; i < pixels.length; i += 4) {const factor = (rng.next() & 0x7F) + 1;for (let j = 0; j < 3; j++) {result[i+j] = (pixels[i+j] - result[i-4+j] * factor) & 0xFF;}result[i+3] = pixels[i+3];}return result;}// ============ 加密/解密(完全对称,可独立调用) ============function encrypt(pixels, password, rounds, isStrong) {let data = new Uint8ClampedArray(pixels);const totalPixels = pixels.length / 4;for (let r = 0; r < rounds; r++) {// 1. 置乱const map = generateShuffleMap(totalPixels, password, r);data = applyShuffle(data, map);// 2. XORdata = xorEncrypt(data, password, r);// 3. 扩散if (isStrong) {data = diffuse(data, password, r);}}return data;}function decrypt(pixels, password, rounds, isStrong) {let data = new Uint8ClampedArray(pixels);const totalPixels = pixels.length / 4;for (let r = rounds - 1; r >= 0; r--) {// 逆向:扩散 -> XOR -> 逆置乱if (isStrong) {data = undiffuse(data, password, r);}data = xorEncrypt(data, password, r); // XOR自逆const map = generateShuffleMap(totalPixels, password, r);data = applyUnshuffle(data, map);}return data;}// ============ UI ============function loadImage(file) {return new Promise((resolve, reject) => {if (!file || !file.type.match(/image\//)) return reject(new Error('请选择图片'));const reader = new FileReader();reader.onload = e => {const img = new Image();img.onload = () => resolve(img);img.onerror = () => reject(new Error('加载失败'));img.src = e.target.result;};reader.onerror = () => reject(new Error('读取失败'));reader.readAsDataURL(file);});}function displayInput(img) {canvasIn.width = img.width;canvasIn.height = img.height;ctxIn.drawImage(img, 0, 0);inputImage = {data: new Uint8ClampedArray(ctxIn.getImageData(0, 0, img.width, img.height).data),width: img.width,height: img.height};infoIn.textContent = `${img.width}×${img.height}`;outputImage = null;canvasOut.width = 0; canvasOut.height = 0;infoOut.textContent = '';}function displayOutput(data, w, h) {canvasOut.width = w;canvasOut.height = h;ctxOut.putImageData(new ImageData(data, w, h), 0, 0);outputImage = { data, width: w, height: h };infoOut.textContent = `${w}×${h}`;}// 按钮事件btnGo.addEventListener('click', () => {if (!inputImage) { status('请先上传图片', 'error'); return; }const pwd = pwdInput.value.trim();if (!pwd) { status('请输入密码', 'error'); return; }const rounds = parseInt(roundsInput.value) || 3;const isStrong = modeSelect.value === 'strong';try {if (mode === 'encrypt') {const result = encrypt(inputImage.data, pwd, rounds, isStrong);displayOutput(result, inputImage.width, inputImage.height);status(`✅ 加密完成!${rounds}轮${isStrong?'强加密':''}`, 'success');} else {const result = decrypt(inputImage.data, pwd, rounds, isStrong);displayOutput(result, inputImage.width, inputImage.height);status(`✅ 解密完成!请检查是否与原图一致`, 'success');}} catch(e) {status('操作失败: ' + e.message, 'error');console.error(e);}});btnVerify.addEventListener('click', () => {if (!inputImage) { status('请先上传图片', 'error'); return; }const pwd = pwdInput.value.trim();if (!pwd) { status('请输入密码', 'error'); return; }const rounds = parseInt(roundsInput.value) || 3;const isStrong = modeSelect.value === 'strong';try {const enc = encrypt(inputImage.data, pwd, rounds, isStrong);displayOutput(enc, inputImage.width, inputImage.height);setTimeout(() => {const dec = decrypt(enc, pwd, rounds, isStrong);displayOutput(dec, inputImage.width, inputImage.height);const orig = inputImage.data;let match = orig.length === dec.length;let diffs = 0;if (match) {for (let i = 0; i < orig.length; i++) {if (orig[i] !== dec[i]) { match = false; diffs++; }}}status(match ? '✅ 验证通过!100%可逆,密码决定一切' : `❌ ${diffs}字节差异`, match ? 'success' : 'error');}, 200);} catch(e) {status('验证失败: ' + e.message, 'error');}});btnDownload.addEventListener('click', () => {if (!outputImage) { status('无结果可下载', 'error'); return; }const c = document.createElement('canvas');c.width = outputImage.width;c.height = outputImage.height;c.getContext('2d').putImageData(new ImageData(outputImage.data, outputImage.width, outputImage.height), 0, 0);c.toBlob(b => {const a = document.createElement('a');a.href = URL.createObjectURL(b);a.download = `encrypted_${Date.now()}.png`;a.click();status('✅ 下载完成!解密时上传此图片,使用相同密码和参数', 'success');});});btnReset.addEventListener('click', () => {resetAll();pwdInput.value = '';fileInput.value = '';status('已重置', 'info');});// 文件上传uploadZone.addEventListener('click', () => fileInput.click());fileInput.addEventListener('change', e => {if (e.target.files[0]) loadImage(e.target.files[0]).then(displayInput).catch(e => status(e.message, 'error'));});uploadZone.addEventListener('dragover', e => { e.preventDefault(); uploadZone.classList.add('drag'); });uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('drag'));uploadZone.addEventListener('drop', e => {e.preventDefault();uploadZone.classList.remove('drag');if (e.dataTransfer.files[0]) loadImage(e.dataTransfer.files[0]).then(displayInput).catch(e => status(e.message, 'error'));});setMode('encrypt');})();</script></body></html>
六、在小程序上复刻图片加密算法
上面的图片加解密在电脑浏览器运行十分顺利,但是当我在Trae中让它把这个html转成微信小程序代码时出现了很多问题。然后就是一下午无休止的调试以及让Trae改代码(微信小程序开发者工具还不支持涉及到的canvas操作,必须真机调试)。
最后最终定位给出的结论是“微信小程序Canvas API 的 putImageData 和 getImageData之间存在差异”

后面就采取了AI给出的建议:建议只用位置置乱方案,放弃XOR像素值加密,且图片保存为png格式确保一定的质量。这也是为什么最后在小程序上图片解密会天然失真,感觉有噪点、不清晰的原因(电脑浏览器上都是正常的)。

上面测试压缩的像素图片可能解密后看起来跟原图差不多,实际照片加密后解密会有一层噪点。因为审核及部分小程序功能问题这个功能预计会在6月10号前上线,大家可以先关注下小程序或公众号,方便后面更新后第一时间体验新功能哦~

发表评论