还能这么玩!利用 LSB 图片隐写技术去做错误监控

前言

作为专业的切图仔,肯定对于线上问题并不陌生,现在有很多监控工具例如神策、Sentry、Fundebug等等,我们为啥还需要搞这种花活呢?具体原因如下:

  • 这些监控工具往往报错信息颗粒度很粗,信息杂乱不易区分,过滤策略不够灵活导致很难在报错中快速发现具体问题。
  • 在发布线上时并不会上传打包后的 Sourcemap,这也是不容易定位具体问题的原因。
  • 一个项目同时开发,每天可能会上线多个版本,不能很快的确定哪个版本更新的哪个模块有问题,导致回滚慢。

所以基于以上原因我们需要快速的定位问题,然后快速回滚或者发布 hotfix

作为前端肯定对于图片非常了解,毕竟每天都跟大量的图片打交道。提到图片隐写大家想到的肯定是跟版权相关的事情,比如一些公司内部系统截图会加入加密信息防止泄密,公司图片加入版权信息防止被盗用等。与之对应的是一些后台常用的可见水印也是一种防泄密的手段。但是呢图片隐写还能做一些其他事情,例如加密信息传递(不可细说),错误监控等。本篇主要介绍使用 LSB 隐写技术去做错误监控。

什么是 LSB 图片隐写

图片隐写加密的方式有很多,今天使用的是 LSB 算法,你要非问为啥不用别的,那我只能跟你说我还没学会。

言归正传,LSB 全称最低有效位算法(Least Significant Bit)。指的是利用二进制数中的最低位(第0位)隐藏信息,将一个需要隐藏的信息转为一个二进制信息然后嵌入载体图像的最低有效位,即将载体图像的最低有效层替换为当前的二进制信息,从而实现图片信息隐写。

作为前端都了解 Canvas 都知道前端每个像素都是由 R、G、B、A 四个通道组成,每个通道的颜色值各占8位,LSB 就是通过替换每个颜色最低位的二进制信息来隐藏信息的,这些修改一般人眼是不能看出的,从而达到隐藏的目的。具体实现如下图所示:

有上图我们可以看到哪部分位最低有效位,然后将我们需要隐藏的信息转换成二进制替换最低有效位的二进制信息。

如何实现 LSB 图片隐写

利用 Canvas 实现

  1. 将图片转换为 Canvas
  2. 创建画布并判断大小
  3. 获取信息并隐藏

代码如下

js 复制代码
const importImage = e => {
  const reader = new FileReader();
  reader.onload = function(event) {
    const img = new Image();
    img.onload = function() {
      const canvas = canvasRef.current;
      const ctx = canvas.getContext("2d");
      ctx.canvas.width = img.width;
      ctx.canvas.height = img.height;
      ctx.drawImage(img, 0, 0);
    };
    img.src = event.target.result;
  };
  reader.readAsDataURL(e.target.files[0]);
};

const encode = () => {
  const message = document.getElementById("message").value;
  const canvas = canvasRef.current;
  const ctx = canvas.getContext("2d");

  const pixelCount = ctx.canvas.width * ctx.canvas.height;
  if ((message.length + 1) * 16 > pixelCount * 4 * 0.75) {
    alert("内容太多了,超过了可写入的最大量");
    return;
  }

  const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  encodeMessage(imgData.data, message);
  ctx.putImageData(imgData, 0, 0);
  alert("隐写成功,信息已隐藏到图片中");

  const outputCanvas = document.createElement("canvas");
  outputCanvas.width = canvas.width;
  outputCanvas.height = canvas.height;
  outputCanvas.getContext("2d").drawImage(canvas, 0, 0);
  setOutput(outputCanvas.toDataURL());
};

效果如下图:

如何获取错误信息

  1. 原生js通过window.onerror方式获取
  2. react可以通过错误边界,在父组件中通过 componentDidCatch 获取子组件抛出的错误
  3. 通过 XMLHttpRequest 封装获取请求的错误信息
  4. 通过一些本地存储或者接口获取当前版本号、路由等信息

通过上面方式我们获取到了一些错误信息,然后我们可以通过上面的方式将错误信息写入到图片中,然后通过监控工具截图等方式获取到带有报错信息的图片然后上报。

解析图片

  1. 获取图片
  2. 解析图片内容并展示
js 复制代码
const decode = () => {
  const canvas = canvasRef.current;
  const ctx = canvas.getContext("2d");
  const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
  const message = decodeMessage(imgData.data);
  setDecodedMessage(message);
};

const getNumberFromBits = (bytes, history) => {
  let number = 0;
  let pos = 0;
  while (pos < 16) {
    const loc = getNextLocation(history, bytes.length);
    const bit = getBit(bytes[loc], 0);
    number = setBit(number, pos, bit);
    pos++;
  }
  return number;
};

const getNextLocation = (history, total) => {
  let pos = history.length;
  let loc = Math.abs(pos + 1) % total;
  while (true) {
    if (loc >= total) {
      loc = 0;
    } else if (history.indexOf(loc) >= 0) {
      loc++;
    } else if ((loc + 1) % 4 === 0) {
      loc++;
    } else {
      history.push(loc);
      return loc;
    }
  }
};

const setBit = (number, location, bit) => {
  return (number & ~(1 << location)) | (bit << location);
};

const getMessageBits = message => {
  let messageBits = [];
  for (let i = 0; i < message.length; i++) {
    const code = message.charCodeAt(i);
    messageBits = messageBits.concat(getBitsFromNumber(code));
  }
  return messageBits;
};

const getBitsFromNumber = number => {
  let bits = [];
  for (let i = 0; i < 16; i++) {
    bits.push(getBit(number, i));
  }
  return bits;
};

const getBit = (number, location) => {
  return (number >> location) & 1;
};

const encodeMessage = (colors, message) => {
  let messageBits = getBitsFromNumber(message.length);
  messageBits = messageBits.concat(getMessageBits(message));
  const history = [];
  let pos = 0;
  while (pos < messageBits.length) {
    let loc = getNextLocation(history, colors.length);
    colors[loc] = setBit(colors[loc], 0, messageBits[pos]);
    while ((loc + 1) % 4 !== 0) {
      loc++;
    }
    colors[loc] = 255;
    pos++;
  }
};

const decodeMessage = colors => {
  let history = [];
  const messageSize = getNumberFromBits(colors, history);
  if ((messageSize + 1) * 16 > colors.length * 0.75) {
    return "";
}
const message = [];
  for (let i = 0; i < messageSize; i++) {
    const code = getNumberFromBits(colors, history);
    message.push(String.fromCharCode(code));
  }
  return message.join("");
};

如下图所示:

点击读取内容后获取到图片隐藏信息。通过这些错误信息就可快速定位页面错误、版本然后进行回滚或者 hotfix

完整代码:

js 复制代码
import React, { useState, useRef } from "react";
const Steganography = () => {
    const [output, setOutput] = useState("");
    const [decodedMessage, setDecodedMessage] = useState("");
    const canvasRef = useRef(null);
  
    const importImage = e => {
      const reader = new FileReader();
      reader.onload = function(event) {
        const img = new Image();
        img.onload = function() {
          const canvas = canvasRef.current;
          const ctx = canvas.getContext("2d");
          ctx.canvas.width = img.width;
          ctx.canvas.height = img.height;
          ctx.drawImage(img, 0, 0);
        };
        img.src = event.target.result;
      };
      reader.readAsDataURL(e.target.files[0]);
    };
  
    const encode = () => {
      const message = document.getElementById("message").value;
      const canvas = canvasRef.current;
      const ctx = canvas.getContext("2d");
  
      const pixelCount = ctx.canvas.width * ctx.canvas.height;
      if ((message.length + 1) * 16 > pixelCount * 4 * 0.75) {
        alert("内容太多了,超过了可写入的最大量");
        return;
      }
  
      const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
      encodeMessage(imgData.data, message);
      ctx.putImageData(imgData, 0, 0);
      alert("隐写成功,信息已隐藏到图片中");
  
      const outputCanvas = document.createElement("canvas");
      outputCanvas.width = canvas.width;
      outputCanvas.height = canvas.height;
      outputCanvas.getContext("2d").drawImage(canvas, 0, 0);
      setOutput(outputCanvas.toDataURL());
    };
  
    const decode = () => {
      const canvas = canvasRef.current;
      const ctx = canvas.getContext("2d");
      const imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
      const message = decodeMessage(imgData.data);
      setDecodedMessage(message);
    };
  
    const getNumberFromBits = (bytes, history) => {
      let number = 0;
      let pos = 0;
      while (pos < 16) {
        const loc = getNextLocation(history, bytes.length);
        const bit = getBit(bytes[loc], 0);
        number = setBit(number, pos, bit);
        pos++;
      }
      return number;
    };
  
    const getNextLocation = (history, total) => {
      let pos = history.length;
      let loc = Math.abs(pos + 1) % total;
      while (true) {
        if (loc >= total) {
          loc = 0;
        } else if (history.indexOf(loc) >= 0) {
          loc++;
        } else if ((loc + 1) % 4 === 0) {
          loc++;
        } else {
          history.push(loc);
          return loc;
        }
      }
    };
  
    const setBit = (number, location, bit) => {
      return (number & ~(1 << location)) | (bit << location);
    };
  
    const getMessageBits = message => {
      let messageBits = [];
      for (let i = 0; i < message.length; i++) {
        const code = message.charCodeAt(i);
        messageBits = messageBits.concat(getBitsFromNumber(code));
      }
      return messageBits;
    };
  
    const getBitsFromNumber = number => {
      let bits = [];
      for (let i = 0; i < 16; i++) {
        bits.push(getBit(number, i));
      }
      return bits;
    };
  
    const getBit = (number, location) => {
      return (number >> location) & 1;
    };
  
    const encodeMessage = (colors, message) => {
      let messageBits = getBitsFromNumber(message.length);
      messageBits = messageBits.concat(getMessageBits(message));
      const history = [];
      let pos = 0;
      while (pos < messageBits.length) {
        let loc = getNextLocation(history, colors.length);
        colors[loc] = setBit(colors[loc], 0, messageBits[pos]);
        while ((loc + 1) % 4 !== 0) {
          loc++;
        }
        colors[loc] = 255;
        pos++;
      }
    };
  
    const decodeMessage = colors => {
      let history = [];
      const messageSize = getNumberFromBits(colors, history);
      if ((messageSize + 1) * 16 > colors.length * 0.75) {
        return "";
    }
    const message = [];
    for (let i = 0; i < messageSize; i++) {
      const code = getNumberFromBits(colors, history);
      message.push(String.fromCharCode(code));
    }
    return message.join("");
  };

  return (
    <div>
      <input type="file" onChange={importImage} />
      <br />
      <canvas ref={canvasRef} style={{ width: "600px" }} />
      <br />
      隐写信息:
      <textarea id="message" />
      <br />
      <button onClick={encode} className="submit">
        隐写
      </button>
      <br />
      隐写图片:<img src={output} style={{ width: "600px" }} />
      <br />
      <button onClick={decode}>从隐写图片读取信息</button>
      <br />
      读出的隐写内容:<div>{decodedMessage}</div>
      <br />
    </div>
  );
};

export default Steganography;
  

小结

本篇主要展示了利用 LSB 实现图片隐写错误信息和解析的原理,简述了利用这个方式监控排查过程的整个流程。大家可以自己尝试改造自己的项目封装组件,在关键节点加入对应的监控功能。

相关推荐
吕彬-前端4 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白4 小时前
react hooks--useCallback
前端·react.js·前端框架
恩婧4 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式
程序员小杨v15 小时前
如何使用 React Compiler – 完整指南
前端·react.js
谢尔登6 小时前
Babel
前端·react.js·node.js
卸任6 小时前
使用高阶组件封装路由拦截逻辑
前端·react.js
清汤饺子9 小时前
实践指南之网页转PDF
前端·javascript·react.js
霸气小男9 小时前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
小白小白从不日白9 小时前
react 组件通讯
前端·react.js
小白小白从不日白12 小时前
react hooks--useReducer
前端·javascript·react.js