图片混淆

7,816 阅读13分钟

简介

常用的混淆方式有六种
这些加密本质上是图片像素位置的交换 不涉及lsb隐写
可以理解为有这么一张加密表
原图经过查表的方式找到像素加密后位置
最后生成加密图
用户设定的秘钥是生成加密表的参数
秘钥不同会导致加密表的不同
通常秘钥不设置或者设定为0.666
后续就用伊芙的图作为例子

伊芙.jpg

方块混淆

  • 特点
    • 图片像素呈块状转移
    • 加密后的图片尺寸通常与原图不一致

方块.png

方块混淆设定了两个参数用于计算后续移动块的大小
通常情况下两个值都设定为32 图片宽高不满足32的倍数的话无法移动
所以通常会缩放图片尺寸以达到满足移动的需求

行像素混淆

  • 特点
    • 图片像素呈小行星带图分布

行像素.png

行像素混淆只对图片每一行的像素位置打乱
这就导致了总体像素呈现每一行渐进式分布

像素混淆

  • 特点
    • 图片像素凌乱均匀分布
    • 与行+列模式混淆较为相似

像素.png

对每一个像素的位置都进行随机重排
与行+列不同的是加密用表不一样
但实际效果大差不差

兼容PicEncrypt:行模式

  • 特点
    • 图片像素呈竖条状左右随机分布

行模式.png

行模式类似于将图片按列剪裁并随机重新拼装
这种分布模式更像是列模式

兼容PicEncrypt:行+列模式

  • 特点
    • 图片像素凌乱均匀分布
    • 与像素混淆较为相似

行+列.png

按照行重排后再按照列重排
与像素分布类似只是加密表不一样

新图片混淆

  • 特点
    • 图片像素似块状但又凌乱
    • 无需设定秘钥

新图片混淆.png

原理是利用一维希尔伯特曲线作为像素加密字典
再由原像素位置加上偏移量计算出加密后像素位置
由于希尔波特曲线有连续性的特点 就导致了相邻位置的像素实际上是有一定联系的
所以很大程度上肉眼能分辨出加密后的图片和原图的颜色量差不多
小番茄混淆使用了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])