寫一個微博上傳圖片自動加密解密工具

微博的和諧太厲害了,有時候髮色圖加了反色還是會被和諧,於是我就想寫一個簡單的程序用來自動加密解密圖片

GitHub 庫在這裏:weibo-img-crypto

添加加密解密處理

加密

我的目的是在上傳圖片時自動加密,但是上傳時的處理函數是在閉包裏的,JS 好像沒有辦法訪問閉包裏沒有導出的東西,所以無法動手腳。經過逆向分析,微博上傳圖片時調用了 FileReader.readAsDataURL 將圖片二進制數據轉爲 Base64,然後再上傳,所以可以 hook 這個函數加入加密圖片的處理:

  let canvas = document.createElement('canvas')
  let ctx = canvas.getContext('2d')

  // Hook FileReader.readAsDataURL
  let originalReadAsDataURL = window.FileReader.prototype.readAsDataURL
  window.FileReader.prototype.readAsDataURL = function (file) {
    if (file.type.startsWith('image/') && file.type !== 'image/gif') { // 暫時不支持GIF
      // Hook onloadend
      let originalOnloadend = this.onloadend
      this.onloadend = () => {
        let img = new window.Image()
        img.onload = () => {
          [canvas.width, canvas.height] = [img.width, img.height]
          ctx.drawImage(img, 0, 0)

          // 加密
          let imgData = ctx.getImageData(0, 0, img.width, img.height)
          encrypt(imgData.data)
          ctx.putImageData(imgData, 0, 0)

          // 替換上傳的圖片
          originalOnloadend({target: {result: canvas.toDataURL()}})
        }
        img.src = this.result
      }
    }
    originalReadAsDataURL.call(this, file)
  }

其中 encrypt() 就是加密圖片的函數了,這個之後再介紹

解密

我打算做成在圖片上點擊鼠標右鍵就解密,這個可以直接抄我之前寫的反色圖片的代碼。只是涉及到圖片的跨域,沒什麼好講的

  // 監聽右鍵菜單
  document.addEventListener('contextmenu', event => {
    if (event.target instanceof window.Image) {
      // event.preventDefault() // 爲了右鍵保存圖片這裏先註釋掉了
      let originImg = event.target
      if (!(originImg instanceof window.Image)) {
        return
      }

      // 跨域
      let img = new window.Image()
      img.crossOrigin = 'anonymous'
      img.onerror = () => window.alert('載入圖片失敗,可能是跨域問題?')
      img.onload = () => {
        [canvas.width, canvas.height] = [img.width, img.height]
        ctx.drawImage(img, 0, 0)

        // 解密
        let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height)
        decrypt(imgData.data)
        ctx.putImageData(imgData, 0, 0)
        originImg.src = canvas.toDataURL()
      }

      if (!originImg.src.startsWith('data:')) { // 如果是'data:'開頭說明已經解密過了
        // 防緩存
        img.src = originImg.src + (originImg.src.indexOf('?') === -1 ? '?_t=' : '&_t=') + new Date().getTime()
      }
    }
  })

加密解密算法

我最初試了一下把 RGB 值異或一個數來加密,但是因爲新浪用了 JPEG 有損壓縮,解密後的圖片有嚴重的雜色。後來想了個把 RGB 數據隨機移動到一個新的位置的方法

僞隨機數生成

JS 的僞隨機數不能自己設置隨機種子,所以我把谷歌 V8 引擎的隨機數算法抄過來,改成可以設置隨機種子:

  // 從谷歌V8引擎抄來的 https://github.com/v8/v8/blob/dae6dfe08ba9810abbe7eee81f7c58e999ae8525/src/math.js#L144
  class Random {
    constructor (seed) {
      if (seed === undefined) {
        seed = new Date().getTime()
      }
      this._rngstate = [seed & 0x0000FFFF, seed >>> 16]
    }

    // 返回[0, 1)
    random () {
      let r0 = (Math.imul(18030, this._rngstate[0] & 0xFFFF) + (this._rngstate[0] >>> 16)) | 0
      this._rngstate[0] = r0
      let r1 = (Math.imul(36969, this._rngstate[1] & 0xFFFF) + (this._rngstate[1] >>> 16)) | 0
      this._rngstate[1] = r1
      let x = ((r0 << 16) + (r1 & 0xFFFF)) | 0
      // Division by 0x100000000 through multiplication by reciprocal.
      return (x < 0 ? (x + 0x100000000) : x) * 2.3283064365386962890625e-10
    }

    // 返回[min, max]的整數
    randint (min, max) {
      return Math.floor(min + this.random() * (max - min + 1))
    }
  }

然後我們要把 RGB 數據移動到一個新的位置,這個位置不能和之前的重複,所以寫了一個生成不重複的隨機序列的類。算法是先初始化數組成員爲 0 到 length - 1,每次取隨機數時取索引在 nextMin 到 length - 1 之間的成員,然後把取到的數和 nextMin 的數交換位置,nextMin + 1

  // 生成[0, length)的隨機序列,每次調用next()返回和之前不重複的值,直到[0, length)用完
  class RandomSequence {
    constructor (length, seed) {
      this._rng = new Random(seed)
      this._list = new Array(length)
      for (let i = 0; i < length; i++) {
        this._list[i] = i
      }
      this._nextMin = 0
    }

    next () {
      if (this._nextMin >= this._list.length) {
        this._nextMin = 0
      }
      let index = this._rng.randint(this._nextMin, this._list.length - 1)
      let result = this._list[index]
      this._list[index] = this._list[this._nextMin]
      this._list[this._nextMin] = result
      this._nextMin++
      return result
    }
  }

加密解密

最後是加密解密函數了,算法很簡單,先分配一個 buffer,然後把 data 的 RGB 數據放到 buffer 的新位置,最後把 buffer 複製回 data。注意這裏 data 是每個像素 4 字節(Alpha 通道沒用到),而 buffer 是每個像素 3 字節

這裏用戶可以通過 window.randomSeed 設置隨機種子,只有加密和解密的種子一樣才能解密,默認種子是 114514 (不要問我什麼意思)

  const DEFAULT_SEED = 114514

  function encrypt (data) {
    let nRgbs = data.length / 4 * 3
    let seq = new RandomSequence(nRgbs, window.randomSeed || DEFAULT_SEED)
    let buffer = new Uint8ClampedArray(nRgbs)
    // 每一個RGB值放到新的位置
    for (let i = 0; i < data.length; i += 4) {
      buffer[seq.next()] = data[i]
      buffer[seq.next()] = data[i + 1]
      buffer[seq.next()] = data[i + 2]
    }
    for (let i = 0, j = 0; i < data.length; i += 4, j += 3) {
      data[i] = buffer[j]
      data[i + 1] = buffer[j + 1]
      data[i + 2] = buffer[j + 2]
    }
  }

  function decrypt (data) {
    let nRgbs = data.length / 4 * 3
    let buffer = new Uint8ClampedArray(nRgbs)
    for (let i = 0, j = 0; i < data.length; i += 4, j += 3) {
      buffer[j] = data[i]
      buffer[j + 1] = data[i + 1]
      buffer[j + 2] = data[i + 2]
    }
    let seq = new RandomSequence(nRgbs, window.randomSeed || DEFAULT_SEED)
    // 取新的位置,放回原來的位置
    for (let i = 0; i < data.length; i += 4) {
      data[i] = buffer[seq.next()]
      data[i + 1] = buffer[seq.next()]
      data[i + 2] = buffer[seq.next()]
    }
  }

效果

由於有 JPEG 有損壓縮,解密後的圖片有高頻噪聲,不過可以被人眼自動過濾。理論上如果數據無損,解密後的圖片和原圖一樣

加密後:

加密後

解密後:

解密後

原圖:

原圖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章