图片加密探索:从凯撒密码到像素置乱

最近一直在更新我的工具小程序,想要增加一个图片加密功能,就是把一张图片进行加密后还是一张图片,只是内容完全看不出来任何有用的信息,只有正确输入解密密码后才能查看图片。为了不要太多增加加密后图片的大小,第一个想到的就是使用凯撒密码算法来实现图片加密(当然是我提想法,AI负责代码实现),在与DeepSeek多轮沟通后最终确认了小程序上图片加密的实现。

一、最初的想法:凯撒密码的像素版本

凯撒密码是最古老的加密方式之一——通过将字母按固定位移来加密文本。把这个思想应用到图片上,我很自然地提出了这个图片加密方案:把图片的像素进行位移轮转

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

二、分块凯撒:从像素到块

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

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

三、双重加密:位置 + 颜色值

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

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

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

四、Arnold 猫映射的尝试

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

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

真的想放弃了,深深怀疑是DeepSeek 能力不行还是我提示词不对?

五、最终方案:纯位置置乱

中间又沟通了几轮,包括使用AES-GCM + 图像编码方案,还是达不到预期验收效果,这里放出最后的方案(中间的N过程已忽略,太累人了)。

  1. 只改变像素位置,不改变像素值——这样即使像素值有微小偏移也不影响解密
  2. 使用 Fisher-Yates 洗牌算法——基于密码生成确定性的置乱映射
  3. 多轮置乱——增强混淆效果,每轮使用不同的密码种子
  4. 完全可逆——解密时使用相同的密码种子生成逆映射

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

该版本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操作,必须真机调试)。

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

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

染卷

本站文章也会定期同步到微信公众号,可以关注下避免错过~

建站 11年 271天
微信公众号

发表评论