试一下图片加解密

2,654 阅读4分钟

前言:最近业务上有接触到图片解密,突然灵机一动,前端能不能单独实现图片加解密呢,于是就有了这篇水文 -。-

先假设是这样的业务场景,前端上传图片,先对其进行加密,然后把处理过的数据(一般是base64)发给后端小哥哥,接着通过链接访问图片的时候,把图片加载完成后进行解密。

对buffer数据进行异或

异或:用于对两个二进制操作数逐位进行比较,相同为0,不同为1,可以看下表:

第一个数的位值第二个数的位值结果
110
101
011
000

看看下面这个例子,有一个数组,我们打算遍历对其异或:

let arr = [1,2,3]
let prev = arr[0];
let cur;
for (let i = 1; i < arr.length; i++) {
  cur = arr[i] ^ prev // 当前跟上一个异或,注意上一个的值是未处理过的值
  prev = arr[i]
  arr[i] = cur
}
console.log(arr) //[1,3,1]

// 让我们再遍历异或一次
for (let i = 1; i < arr.length; i++) {
  arr[i] = arr[i] ^ arr[i-1]
  
}
console.log(arr) //[1,2,3] 这里跟原来的一模一样,是不是很神奇

看了上面的例子,突然就有了下面的加解密思路了(偷笑)

加密思路:把要加密的图片转成 buffer,然后用 Uint8Array 来存储 ,接着遍历进行异或,最后转成base64上传。

//伪代码
const handleFileChange = (e)=>{
    const file = e.currentTarget.files[0]
    const reader = new FileReader()
    reader.readAsArrayBuffer(file) // 获取图片的buffer数据
    reader.onload = async (e) => {
    	let buffer = e.target.result
        buffer = new Uint8Array(buffer)
        
        // 加密
        buffer = enCode(buffer)
        // 转成base64
        let base64 = toBase64(buffer) 
        ...
    }
}

// 遍历进行异或
const enCode = (buf) => {
    let prev = buf[0];
    let cur;
    for (let i = 1; i < buf.byteLength; i++) {
      cur = buf[i] ^ prev
      prev = buf[i]
      buf[i] = cur
    }
    return buf
}

const toBase64 = (buf) => {
    let binary = '';
    let len = buf.byteLength;

    for (var i = 0; i < len; i++) {
      binary += String.fromCharCode(buf[i]);
    }

    return window.btoa(binary);
}

解密思路:首先获取到图片资源,转成buffer格式数据,再进行遍历异或,最后转成base64。

/**
* 获取图片资源可以通过下面两种方式:
* 1. 通过发送get请求,拿到图片的buffer数据:
*  比如: fetch(src,{}).then(res=>{return res.arrayBuffer()})
* 2. 通过img.onload,然后配合canvas绘制图片并转成base64,接着把base64转成buffer格式
*/

// 如果你获取到的是base64格式,可以用下面的方法转成buffer
const base64ToBuffer =(base64String)=> {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
      .replace(/\-/g, '+')
      .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8ClampedArray(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
  }
  
 //解密,直接两两异或
 const deCode = (buf) => {
    for (let i = 1; i < buf.byteLength; i++) {
      buf[i] = buf[i] ^ buf[i - 1]
    }
    return buf
 }

小结:用异或进行图片加解密的思路,看起来也很简单,不复杂,但是有个缺点,就是加密后的图片是查看不了的

对图片像素进行操作

上面的方法是对buffer数据进行操作的,那有没有办法对像素进行操作呢,答案是肯定的,虽然没有直接拿来用的api,但是有间接的方法,这里要借助canvas的能力了,主要用到了getImageDataputImageData

getImageData: 这个方法会返回一个ImageData对象,它代表了画布区域的对象数据

putImageData: 对场景进行像素数据的写入。 具体看这里

这里放一张(300*300)图片的ImageData.data数据

360000/300/300 = 4,在这里可以看出,Uint8ClampedArray数组中,每4位组成一个像素(其中前三位代表rgb)。知道这个规则就简单了,可以用一个二维数组来保存像素点。


const toPx = (buf)=> {
    let rs = []
    for (let i = 0; i < buf.length; i += 4) {
      // 每4个组成一个像素
      rs.push([buf[i], buf[i + 1], buf[i + 2], buf[i + 3]])
    }
    return rs
  }

搞定了像素,接下来就是找到加密的算法了,笔者找到了一个比较简单的算法Arnold置乱

Arnold置乱又称为猫脸置乱,据说是因为Arnold首先对猫脸图像应用了这个算法。置乱的含义是置换和打乱,也就是将原始的图片按照我们设计的规则,进行顺序打乱的操作。 变换方式:

其中x和y表示坐标,new表示变换以后的坐标,ori表示原始的坐标(original缩写),a和b是两个可选的参数,mod为求余数操作,N是图像的长或者宽,这里只考虑长度和宽度相等的图像,上式表示的就是“图像的坐标变换”。

对应的解密变换:

代码实现如下:


/**
   * 该加解密方法适用于正方形的图片
   * @param base64 
   * @param decode 是否解密
   */
  drawToCanvas(base64, decode) {
    return new Promise((resolve, reject) => {
      var canvas = document.getElementById('canvas');
      var ctx = canvas.getContext('2d');
      var img = new Image()
      img.src = base64
      img.onload = () => {
       // 这里为了方便固定了宽高
        canvas.width = 300
        canvas.height = 300
        ctx.drawImage(img, 0, 0);
        let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        let data = imageData.data;
        let pxArr = this.toPx(data) // 转成像素
        let xyArr = this.toXY(pxArr, canvas.width, canvas.height) //转成xy坐标
        let xyArr2 = this.toXY(pxArr, canvas.width, canvas.height)
        // 核心,在这里调用加密或者解密方法对像素数组进行置乱
        let yxArr = decode ? this.decodeXY(xyArr, xyArr2, canvas.width, canvas.height) : this.changeXY(xyArr, xyArr2, canvas.width, canvas.height)
        // 对多维数组进行扁平化
        const flatten = (arr) => arr.reduce(
          (acc, val) => acc.concat(Array.isArray(val) ? flatten(val) : val), []
        )
        const data2 = flatten(yxArr)
        for (let i = 0; i < data.length; i++) {
          data[i] = data2[i]
        }
        // data = this.enCode2(data)
        console.log('data', data)
        // console.log('pxArr', pxArr)
        // console.log('xyArr', xyArr)
        // console.log('yxArr', yxArr)
        // console.log('data2', data2)
        ctx.putImageData(imageData, 0, 0); // 把修改后的数据重新画到canvas中
        // console.log('base64:', canvas.toDataURL())
        resolve(canvas.toDataURL())
      }
    })
  }

  toPx(buf) {
    let rs = []
    for (let i = 0; i < buf.length; i += 4) {
      // 每4个组成一个像素
      rs.push([buf[i], buf[i + 1], buf[i + 2], buf[i + 3]])
    }
    return rs
  }
  toXY(arr, row, col) {
    let rs: Array<any> = []
    for (let i = 0; i < col; i++) {
      let rowArr = []
      for (let j = 0; j < row; j++) {
        rowArr.push(arr[i * col + j])
      }
      rs.push(rowArr)
    }
    return rs
  }
  // 加密
  changeXY(arr, source, row, col) {
    let a = 1, b = 7, time = 3; // a,b 参数可以自己调试,time代表打乱的次数
    while (time > 0) {
      time--
      for (let i = 0; i < col; i++) {
        for (let j = 0; j < row; j++) {
          let x = (1 * i + b * j) % col
          let y = (a * i + (a * b + 1) * j) % row
          // console.log(x, y)
          arr[x][y] = source[i][j]
        }
      }
    }
    return arr
  }
  // 解密
  decodeXY(arr, source, row, col) {
    // console.log(arr, source)
    let a = 1, b = 7, time = 3;
    while (time > 0) {
      time--
      for (let i = 0; i < col; i++) {
        for (let j = 0; j < row; j++) {
          let x = ((a * b + 1) * i + (-b) * j) % col
          let y = ((-a) * i + j) % row
          x = x >= 0 ? x : col + x
          y = y >= 0 ? y : row + y
          arr[x][y] = source[i][j]
        }
      }
    }
    return arr
  }

测试效果如下:

小结:用像素操作的好处是,加密后的图片是可以显示的,只是笔者选择的算法只适用于正方形的图片(微笑)

参考:

  1. JS &、|、^和~(逻辑位运算符)
  2. 像素操作
  3. 用python设计图像加密技术之Arnold算法