图片加密探索:从凯撒密码到像素置乱
最近一直在更新我的工具小程序,想要增加一个图片加密功能,就是把一张图片进行加密后还是一张图片,只是内容完全看不出来任何有用的信息,只有正确输入解密密码后才能查看图片。为了不要太多增加加密后图片的大小,第一个想到的就是使用凯撒密码算法来实现图片加密(当然是我提想法,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. XOR
data = 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号前上线,大家可以先关注下小程序或公众号,方便后面更新后第一时间体验新功能哦~

发表评论