前言:最近业务上有接触到图片解密,突然灵机一动,前端能不能单独实现图片加解密呢,于是就有了这篇水文 -。-
先假设是这样的业务场景,前端上传图片,先对其进行加密,然后把处理过的数据(一般是base64)发给后端小哥哥,接着通过链接访问图片的时候,把图片加载完成后进行解密。
对buffer数据进行异或
异或:用于对两个二进制操作数逐位进行比较,相同为0,不同为1,可以看下表:
第一个数的位值 | 第二个数的位值 | 结果 |
---|---|---|
1 | 1 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
0 | 0 | 0 |
看看下面这个例子,有一个数组,我们打算遍历对其异或:
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的能力了,主要用到了getImageData
和 putImageData
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
}
测试效果如下:
小结:用像素操作的好处是,加密后的图片是可以显示的,只是笔者选择的算法只适用于正方形的图片(微笑)
参考: