简介
常用的混淆方式有六种
这些加密本质上是图片像素位置的交换 不涉及lsb隐写
可以理解为有这么一张加密表
原图经过查表的方式找到像素加密后位置
最后生成加密图
用户设定的秘钥是生成加密表的参数
秘钥不同会导致加密表的不同
通常秘钥不设置或者设定为0.666
后续就用伊芙的图作为例子
方块混淆
- 特点
- 图片像素呈块状转移
- 加密后的图片尺寸通常与原图不一致
方块混淆设定了两个参数用于计算后续移动块的大小
通常情况下两个值都设定为32 图片宽高不满足32的倍数的话无法移动
所以通常会缩放图片尺寸以达到满足移动的需求
行像素混淆
- 特点
- 图片像素呈小行星带图分布
行像素混淆只对图片每一行的像素位置打乱
这就导致了总体像素呈现每一行渐进式分布
像素混淆
- 特点
- 图片像素凌乱均匀分布
- 与行+列模式混淆较为相似
对每一个像素的位置都进行随机重排
与行+列不同的是加密用表不一样
但实际效果大差不差
兼容PicEncrypt:行模式
- 特点
- 图片像素呈竖条状左右随机分布
行模式类似于将图片按列剪裁并随机重新拼装
这种分布模式更像是列模式
兼容PicEncrypt:行+列模式
- 特点
- 图片像素凌乱均匀分布
- 与像素混淆较为相似
按照行重排后再按照列重排
与像素分布类似只是加密表不一样
新图片混淆
- 特点
- 图片像素似块状但又凌乱
- 无需设定秘钥
原理是利用一维希尔伯特曲线作为像素加密字典
再由原像素位置加上偏移量计算出加密后像素位置
由于希尔波特曲线有连续性的特点 就导致了相邻位置的像素实际上是有一定联系的
所以很大程度上肉眼能分辨出加密后的图片和原图的颜色量差不多
小番茄混淆使用了lsb图片隐写以应对jepg压缩算法
本文中的实现只还原像素的位置 并未使用lsb隐写
也能做到还原和加密 并且和小番茄混淆可互通
代码实现
// 方块混淆
const sx = 32
const sy = 32
const block = (data, width, height, key, factor) => {
// 入参要求width必须是sx的整数倍 height必须是sy的整数倍
const x = amess(sx, key)
const y = amess(sy, key)
const ssx = width / sx
const ssy = height / sy
const core = ([i, j]) => {
const a = (x[((j / ssy) | 0) % sx] * ssx + i) % width
const b = x[(a / ssx) | 0] * ssx + a % ssx
const c = (y[((b / ssx) | 0) % sy] * ssy + j) % height
const d = y[(c / ssy) | 0] * ssy + c % ssy
const e = i + j * width
const f = b + d * width
return [e, f]
}
return pix({data, width, height, factor}, core)
}
// 行像素混淆
const line = (data, width, height, key, factor) => {
const x = amess(width, key)
const y = amess(height, key)
const core = ([i, j]) => {
const a = (x[j % width] + i) % width
const b = x[a]
const d = j
const e = i + j * width
const f = b + d * width
return [e, f]
}
return pix({data, width, height, factor}, core)
}
// 像素混淆
const pixel = (data, width, height, key, factor) => {
const x = amess(width, key)
const y = amess(height, key)
const core = ([i, j]) => {
const a = (x[j % width] + i) % width
const b = x[a]
const c = (y[b % height] + j) % height
const d = y[c]
const e = i + j * width
const f = b + d * width
return [e, f]
}
return pix({data, width, height, factor}, core)
}
// 兼容PicEncrypt:行模式
const pic1 = (data, width, height, key, factor) => {
const address = logistic(key, 1, width)[0]
const core = ([i, j]) => {
const m = address[i]
const x = i + j * width
const y = m + j * width
return [x, y]
}
return pix({data, width, height, factor}, core)
}
// 兼容PicEncrypt:行+列模式
const pic2 = (data, width, height, key, factor) => {
const col = logistic(key, height, width)
const row = logistic(key, width, height)
const core = ([i, j]) => {
const n = row[i][j]
const m = col[n][i]
const x = i + j * width
const y = m + n * width
return [x, y]
}
return pix({data, width, height, factor}, core)
}
// 新图片混淆
const convert = (data, width, height, _, factor) => {
const size = width * height
// 偏移量
const offset = Math.round(size * (Math.sqrt(5) - 1) / 2)
const curve = gilbert2d(width, height)
const core = ([i, j]) => {
const a = i + j * width
const b = (a + offset) % size
const [c, d] = curve[a]
const [e, f] = curve[b]
const g = c + d * width
const h = e + f * width
return [h, g]
}
return pix({data, width, height, factor}, core)
}
// 加密字典
const md5 = (str) => {
const rotateLeft = (lValue, iShiftBits) => (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits))
const addUnsigned = (lX, lY) => {
const lX4 = (lX & 0x40000000)
const lY4 = (lY & 0x40000000)
const lX8 = (lX & 0x80000000)
const lY8 = (lY & 0x80000000)
const lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF)
if (lX4 & lY4) return (lResult ^ 0x80000000 ^ lX8 ^ lY8)
if (lX4 | lY4) {
if (lResult & 0x40000000) return (lResult ^ 0xC0000000 ^ lX8 ^ lY8)
else return (lResult ^ 0x40000000 ^ lX8 ^ lY8)
} else {
return (lResult ^ lX8 ^ lY8)
}
}
const md5ff = (a, b, c, d, x, s, ac) => {
a = addUnsigned(a, addUnsigned(addUnsigned((b & c) | ((~b) & d), x), ac))
return addUnsigned(rotateLeft(a, s), b)
}
const md5gg = (a, b, c, d, x, s, ac) => {
a = addUnsigned(a, addUnsigned(addUnsigned((b & d) | (c & (~d)), x), ac))
return addUnsigned(rotateLeft(a, s), b)
}
const md5hh = (a, b, c, d, x, s, ac) => {
a = addUnsigned(a, addUnsigned(addUnsigned((b ^ c ^ d), x), ac))
return addUnsigned(rotateLeft(a, s), b)
}
const md5ii = (a, b, c, d, x, s, ac) => {
a = addUnsigned(a, addUnsigned(addUnsigned((c ^ (b | (~d))), x), ac))
return addUnsigned(rotateLeft(a, s), b)
}
const convertToWordArray = (str) => {
const lWordCount = Math.floor((str.length + 64) / 64)
const lMessage = new Array(lWordCount * 16).fill(0)
for (let i = 0; i < str.length; i++) {
const charCode = str.charCodeAt(i)
lMessage[i >> 2] |= (charCode & 0xFF) << ((i % 4) * 8)
}
const lastIndex = str.length
lMessage[lastIndex >> 2] |= 0x80 << ((lastIndex % 4) * 8)
lMessage[lWordCount * 16 - 2] = lastIndex * 8
return lMessage
}
const wordToHex = (values) => {
return Array.from(values)
.map(value => Array(4).fill(value).map((v, i) => [v, i]))
.flat()
.map(([v, i]) => (v >>> (i * 8)) & 0x00FF)
.map(v => v.toString(16))
.map(v => v.padStart(2, '0'))
.join('')
.toLowerCase()
}
const x = convertToWordArray(str)
let a = 0x67452301
let b = 0xEFCDAB89
let c = 0x98BADCFE
let d = 0x10325476
for (let k = 0; k < x.length; k += 16) {
let AA = a
let BB = b
let CC = c
let DD = d
a = md5ff(a, b, c, d, x[k + 0], 7, 0xD76AA478)
d = md5ff(d, a, b, c, x[k + 1], 12, 0xE8C7B756)
c = md5ff(c, d, a, b, x[k + 2], 17, 0x242070DB)
b = md5ff(b, c, d, a, x[k + 3], 22, 0xC1BDCEEE)
a = md5ff(a, b, c, d, x[k + 4], 7, 0xF57C0FAF)
d = md5ff(d, a, b, c, x[k + 5], 12, 0x4787C62A)
c = md5ff(c, d, a, b, x[k + 6], 17, 0xA8304613)
b = md5ff(b, c, d, a, x[k + 7], 22, 0xFD469501)
a = md5ff(a, b, c, d, x[k + 8], 7, 0x698098D8)
d = md5ff(d, a, b, c, x[k + 9], 12, 0x8B44F7AF)
c = md5ff(c, d, a, b, x[k + 10], 17, 0xFFFF5BB1)
b = md5ff(b, c, d, a, x[k + 11], 22, 0x895CD7BE)
a = md5ff(a, b, c, d, x[k + 12], 7, 0x6B901122)
d = md5ff(d, a, b, c, x[k + 13], 12, 0xFD987193)
c = md5ff(c, d, a, b, x[k + 14], 17, 0xA679438E)
b = md5ff(b, c, d, a, x[k + 15], 22, 0x49B40821)
a = md5gg(a, b, c, d, x[k + 1], 5, 0xF61E2562)
d = md5gg(d, a, b, c, x[k + 6], 9, 0xC040B340)
c = md5gg(c, d, a, b, x[k + 11], 14, 0x265E5A51)
b = md5gg(b, c, d, a, x[k + 0], 20, 0xE9B6C7AA)
a = md5gg(a, b, c, d, x[k + 5], 5, 0xD62F105D)
d = md5gg(d, a, b, c, x[k + 10], 9, 0x2441453)
c = md5gg(c, d, a, b, x[k + 15], 14, 0xD8A1E681)
b = md5gg(b, c, d, a, x[k + 4], 20, 0xE7D3FBC8)
a = md5gg(a, b, c, d, x[k + 9], 5, 0x21E1CDE6)
d = md5gg(d, a, b, c, x[k + 14], 9, 0xC33707D6)
c = md5gg(c, d, a, b, x[k + 3], 14, 0xF4D50D87)
b = md5gg(b, c, d, a, x[k + 8], 20, 0x455A14ED)
a = md5gg(a, b, c, d, x[k + 13], 5, 0xA9E3E905)
d = md5gg(d, a, b, c, x[k + 2], 9, 0xFCEFA3F8)
c = md5gg(c, d, a, b, x[k + 7], 14, 0x676F02D9)
b = md5gg(b, c, d, a, x[k + 12], 20, 0x8D2A4C8A)
a = md5hh(a, b, c, d, x[k + 5], 4, 0xFFFA3942)
d = md5hh(d, a, b, c, x[k + 8], 11, 0x8771F681)
c = md5hh(c, d, a, b, x[k + 11], 16, 0x6D9D6122)
b = md5hh(b, c, d, a, x[k + 14], 23, 0xFDE5380C)
a = md5hh(a, b, c, d, x[k + 1], 4, 0xA4BEEA44)
d = md5hh(d, a, b, c, x[k + 4], 11, 0x4BDECFA9)
c = md5hh(c, d, a, b, x[k + 7], 16, 0xF6BB4B60)
b = md5hh(b, c, d, a, x[k + 10], 23, 0xBEBFBC70)
a = md5hh(a, b, c, d, x[k + 13], 4, 0x289B7EC6)
d = md5hh(d, a, b, c, x[k + 0], 11, 0xEAA127FA)
c = md5hh(c, d, a, b, x[k + 3], 16, 0xD4EF3085)
b = md5hh(b, c, d, a, x[k + 6], 23, 0x4881D05)
a = md5hh(a, b, c, d, x[k + 9], 4, 0xD9D4D039)
d = md5hh(d, a, b, c, x[k + 12], 11, 0xE6DB99E5)
c = md5hh(c, d, a, b, x[k + 15], 16, 0x1FA27CF8)
b = md5hh(b, c, d, a, x[k + 2], 23, 0xC4AC5665)
a = md5ii(a, b, c, d, x[k + 0], 6, 0xF4292244)
d = md5ii(d, a, b, c, x[k + 7], 10, 0x432AFF97)
c = md5ii(c, d, a, b, x[k + 14], 15, 0xAB9423A7)
b = md5ii(b, c, d, a, x[k + 5], 21, 0xFC93A039)
a = md5ii(a, b, c, d, x[k + 12], 6, 0x655B59C3)
d = md5ii(d, a, b, c, x[k + 3], 10, 0x8F0CCC92)
c = md5ii(c, d, a, b, x[k + 10], 15, 0xFFEFF47D)
b = md5ii(b, c, d, a, x[k + 1], 21, 0x85845DD1)
a = md5ii(a, b, c, d, x[k + 8], 6, 0x6FA87E4F)
d = md5ii(d, a, b, c, x[k + 15], 10, 0xFE2CE6E0)
c = md5ii(c, d, a, b, x[k + 6], 15, 0xA3014314)
b = md5ii(b, c, d, a, x[k + 13], 21, 0x4E0811A1)
a = md5ii(a, b, c, d, x[k + 4], 6, 0xF7537E82)
d = md5ii(d, a, b, c, x[k + 11], 10, 0xBD3AF235)
c = md5ii(c, d, a, b, x[k + 12], 15, 0x2AD7D2BB)
b = md5ii(b, c, d, a, x[k + 9], 21, 0xEB86D391)
a = addUnsigned(a, AA)
b = addUnsigned(b, BB)
c = addUnsigned(c, CC)
d = addUnsigned(d, DD)
}
return wordToHex([a, b, c, d])
}
const amess = (length, key) => {
return Array.from({length}, (_, i) => i)
.map(i => `${key}${i}`)
.map(i => md5(i))
.map(i => i.substr(0, 7))
.map(i => parseInt(i, 16))
.map((v, i) => v % (i + 1))
.map((v, i) => [v, i])
.reverse()
.reduce((a, [v, i]) => ([a[v], a[i]] = [a[i], a[v]], a), Array.from({length}, (_, i) => i))
}
const logistic = (key, height, width) => {
const perms = Array(height)
let x = key
for (let i = 0; i < height; i++) {
const vals = Array(width)
vals[0] = [x, 0]
for (let j = 1; j < width; j++) {
x = 3.9999999 * x * (1 - x)
vals[j] = [x, j]
}
perms[i] = vals.sort((a, b) => a[0] - b[0]).map(pair => pair[1])
}
return perms
}
// 一阶希尔伯特曲线
const gilbert2d = (width, height) => {
const result = []
const stack = []
if (width >= height) {
stack.push({ x: 0, y: 0, ax: width, ay: 0, bx: 0, by: height })
} else {
stack.push({ x: 0, y: 0, ax: 0, ay: height, bx: width, by: 0 })
}
while (stack.length > 0) {
const { x, y, ax, ay, bx, by } = stack.pop()
const w = Math.abs(ax + ay)
const h = Math.abs(bx + by)
const [dax, day] = [Math.sign(ax), Math.sign(ay)]
const [dbx, dby] = [Math.sign(bx), Math.sign(by)]
if (h === 1) {
let px = x
let py = y
for (let i = 0; i < w; i++) {
result.push([px, py])
px += dax
py += day
}
continue
}
if (w === 1) {
let px = x
let py = y
for (let i = 0; i < h; i++) {
result.push([px, py])
px += dbx
py += dby
}
continue
}
let [ax2, ay2] = [Math.floor(ax / 2), Math.floor(ay / 2)]
let [bx2, by2] = [Math.floor(bx / 2), Math.floor(by / 2)]
const w2 = Math.abs(ax2 + ay2)
const h2 = Math.abs(bx2 + by2)
if (2 * w > 3 * h) {
if ((w2 % 2) && (w > 2)) {
ax2 += dax
ay2 += day
}
stack.push({ x: x + ax2, y: y + ay2, ax: ax - ax2, ay: ay - ay2, bx, by })
stack.push({ x, y, ax: ax2, ay: ay2, bx, by })
} else {
if ((h2 % 2) && (h > 2)) {
bx2 += dbx
by2 += dby
}
stack.push({ x: x + (ax - dax) + (bx2 - dbx), y: y + (ay - day) + (by2 - dby), ax: -bx2, ay: -by2, bx: -(ax - ax2), by: -(ay - ay2) })
stack.push({ x: x + bx2, y: y + by2, ax, ay, bx: bx - bx2, by: by - by2 })
stack.push({ x, y, ax: bx2, ay: by2, bx: ax2, by: ay2 })
}
}
return result
}
// 加密流程
const pix = ({data, width, height, factor}, core) => {
// 反过来就是解密
const ass = factor? (i) => i: ([i, j]) => [j, i]
return Array.from({length:width}, (_, i) => i)
.map(i => Array.from({length: height}, (_, j) => [i, j]))
.flat()
// 核心变换
.map(core)
// 反转变换
.map(ass)
// 实际坐标
.map(([i, j]) => [4 * i, 4 * j])
.reduce((a, [i, j]) => ([a[i], a[i+1], a[i+2], a[i+3]] = [data[j], data[j+1], data[j+2], data[j+3]], a), [])
}
void function(action){
// 元素声明
const model0 = document.createElement('option')
const model1 = document.createElement('option')
const model2 = document.createElement('option')
const model3 = document.createElement('option')
const model4 = document.createElement('option')
const model5 = document.createElement('option')
const model = document.createElement('select')
const modelP = document.createElement('p')
const modelL = document.createElement('label')
const file = document.createElement('input')
const fileP = document.createElement('p')
const fileL = document.createElement('label')
const key = document.createElement('input')
const keyP = document.createElement('p')
const keyL = document.createElement('label')
const group = document.createElement('div')
const enc = document.createElement('button')
const dec = document.createElement('button')
const reset = document.createElement('button')
const clear = document.createElement('button')
const groupL = document.createElement('label')
const groupP = document.createElement('p')
const left = document.createElement('div')
const workD = document.createElement('details')
const workS = document.createElement('summary')
const work = document.createElement('canvas')
const workF = document.createElement('fieldset')
const workL = document.createElement('legend')
const originD = document.createElement('details')
const originS = document.createElement('summary')
const origin = document.createElement('canvas')
const originF = document.createElement('fieldset')
const originL = document.createElement('legend')
const right = document.createElement('div')
const container = document.createElement('div')
// 元素结构
model.append(model0, model1, model2, model3, model4, model5)
modelL.append(modelP, model)
fileL.append(fileP, file)
keyL.append(keyP, key)
group.append(enc, dec, reset, clear)
groupL.append(groupP, group)
left.append(modelL, fileL, keyL, groupL)
workF.append(workL, work)
originF.append(originL, origin)
workD.append(workS, workF)
originD.append(originS, originF)
right.append(originD, workD)
container.append(left, right)
// 公共区域
const content = '? x ?'
const pre = () => {
// 方块混淆需要预处理
if(model.value !== '0') {
return
}
const {width: w, height: h} = work
if(w % sx === 0 || h % sy === 0){
return
}
alert(`方块混淆必须设置宽为${sx}的整数倍、高为${sy}的整数倍`)
const width = w - (w % sx) + sx
const height = h - (h % sy) + sy
return writeToWork([origin, width, height])
}
const cipher = factor => {
enc.disabled = dec.disabled = reset.disabled = clear.disabled = true
const {width: w, height: h} = work
const data = wCtx.getImageData(0, 0, w, h).data
const res = action[model.value](data, w, h, key.value, factor)
wCtx.reset()
wCtx.putImageData(new ImageData(new Uint8ClampedArray(res), w, h), 0, 0)
enc.disabled = dec.disabled = reset.disabled = clear.disabled = false
}
const wCtx = work.getContext('2d', { willReadFrequently: true })
const oCtx = origin.getContext('2d', { willReadFrequently: true })
const writeToOrigin = image => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = ({target: {result: src}}) => resolve(src)
reader.readAsDataURL(image)
})
.then(src => new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(img)
img.src = src
}))
.then(img => {
origin.width = img.width
origin.height = img.height
oCtx.reset()
oCtx.drawImage(img, 0, 0, origin.width, origin.height)
return [origin, origin.width, origin.height]
})
}
const writeToWork = ([org, width, height]) => {
work.width = width
work.height = height
wCtx.reset()
wCtx.drawImage(org, 0, 0, work.width, work.height)
workL.textContent = `${work.width} x ${work.height}`
workD.open = true
originD.open = false
}
// 元素属性
origin.onpaste = work.onpaste = ({clipboardData: {items}}) => {
if (!items || items.length === 0) return alert('无法从剪贴板项获取图片文件。')
const image = Array.from(items).find(({type, kind}) => kind === 'file' && type.startsWith('image/'))
if (!image) return alert('无法从剪贴板项获取图片文件。')
const blob = image.getAsFile()
if (!blob) return alert('无法从剪贴板项获取图片文件。')
writeToOrigin(blob).then(writeToWork)
}
file.onchange = ({target: {files: [image]}}) => {
if(image){
writeToOrigin(image).then(writeToWork)
} else {
alert('选择本地图片')
}
}
model.onchange = ({target: {value}}) => {
switch (parseInt(value)) {
case 0:
case 1:
case 2: key.disabled = false, key.value = ''; break
case 3:
case 4: key.disabled = false, key.value = '0.666'; break
case 5: key.value = '无需秘钥', key.disabled = true
}
}
enc.onclick = () => (pre(), cipher(true))
dec.onclick = () => (pre(), cipher(false))
reset.onclick = () => writeToWork([origin, origin.width, origin.height])
clear.onclick = () => {
work.width = 300
work.height = 150
workL.textContent = content
wCtx.reset()
}
workL.textContent = content
originL.textContent = '点击框内直接粘贴'
model0.selected = true
model0.textContent = '方块混淆'
model1.textContent = '行像素混淆'
model2.textContent = '像素混淆'
model3.textContent = '兼容PicEncrypt:行模式'
model4.textContent = '兼容PicEncrypt:行+列模式'
model5.textContent = '小番茄混淆'
model0.value = 0
model1.value = 1
model2.value = 2
model3.value = 3
model4.value = 4
model5.value = 5
modelP.textContent = '模式:'
groupP.textContent = '操作:'
fileP.textContent = '原图:'
keyP.textContent = '秘钥:'
file.accept = 'image/*'
file.type = 'file'
key.title = '输入秘钥'
key.placeholder = '输入秘钥'
originS.textContent = '原图'
workS.textContent = '画布'
workD.open = false
originD.open = true
container.style = 'display: flex; gap: 10px;'
left.style = 'flex-basis: 200px;'
right.style = 'flex-basis: auto;'
origin.tabIndex = work.tabIndex = '0'
enc.textContent = '加密'
dec.textContent = '解密'
reset.textContent = '重置'
clear.textContent = '清空'
// 生效
document.body.append(container)
}([block, line, pixel, pic1, pic2, convert])