Front end watermark generation scheme

The content of the system was leaked outside the audit time, but the security screenshot of the background audit personnel was not particularly sensitive. Therefore, a watermark is added on the system page, which has a certain prompt function for sensitive operations such as screenshots of auditors.

Web watermark generation solution

Generate watermark through canvas

Canvas compatibility

Here, we use canvas to generate base64 pictures and query the compatibility through CanIUse website. If it is used on the mobile terminal and some management systems, the compatibility problem can be completely ignored.

HTMLCanvasElement. The todataurl method returns a data URI containing the image display. You can use the type parameter to its type, which defaults to PNG format. The resolution of the picture is 96 DPI.

If the height or width of the canvas is 0, the string "data:, will be returned.
If the incoming type is not "image/png", but the returned value starts with "data:image/png", the incoming type is not supported.
Chrome supports the "image/webp" type. Specific reference HTMLCanvasElement.toDataURL

The specific code is as follows:

(function () {
      // Implement watermark with canvas
      function __canvasWM({
        // Use the function default value method of ES6 to set the default value of parameters
        // See for details https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
        container = document.body,
        width = '200px',
        height = '150px',
        textAlign = 'center',
        textBaseline = 'middle',
        font = "20px microsoft yahei",
        fillStyle = 'rgba(184, 184, 184, 0.8)',
        content = 'Please don't pass it on',
        rotate = '30',
        zIndex = 1000
      } = {}) {
        var args = arguments[0];
        var canvas = document.createElement('canvas');

        canvas.setAttribute('width', width);
        canvas.setAttribute('height', height);
        var ctx = canvas.getContext("2d");

        ctx.textAlign = textAlign;
        ctx.textBaseline = textBaseline;
        ctx.font = font;
        ctx.fillStyle = fillStyle;
        ctx.rotate(Math.PI / 180 * rotate);
        ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);

        var base64Url = canvas.toDataURL();
        const watermarkDiv = document.createElement("div");
        watermarkDiv.setAttribute('style', `
          position:absolute;
          top:0;
          left:0;
          width:100%;
          height:100%;
          z-index:${zIndex};
          pointer-events:none;
          background-repeat:repeat;
          background-image:url('${base64Url}')`);

        container.style.position = 'relative';
        container.insertBefore(watermarkDiv, container.firstChild);

        
      });

      window.__canvasWM = __canvasWM;
    })();

    // call
    __canvasWM({
      content: 'TEST'
    })

In order to make this method more general and compatible with different reference methods, we can also add this Code:

 // In order to be compatible with different environments
      if (typeof module != 'undefined' && module.exports) {  //CMD
        module.exports = __canvasWM;
      } else if (typeof define == 'function' && define.amd) { // AMD
        define(function () {
          return __canvasWM;
        });
      } else {
        window.__canvasWM = __canvasWM;
      }

This seems to meet our needs, but there is another problem. Users who know a little about the use of the browser or web page knowledge can use the browser developer tools to dynamically change the properties or structure of the DOM. There are two solutions at this time:

  • Monitor the change of the watermark div, record the innerHTML of the newly generated div, take a new value every few seconds, and regenerate the watermark once it changes. However, this approach may affect performance;
  • Using MutationObserver

MutationObserver provides developers with the ability to respond appropriately when the DOM tree in a certain range changes.
From the compatibility table, we can see that the support of advanced browser and mobile browser is very good.
The rotation observer API is used to monitor DOM changes. The API can be notified of any changes to the DOM, such as the increase or decrease of nodes, the change of attributes, and the change of text content.
Use the MutationObserver constructor to create a new observer instance. The instance has a callback function that accepts two parameters, the first is the variable array and the second is the observer instance. The observe method of the instance of MutationObserver is used to start listening. It accepts two parameters.
The first parameter is the DOM node to be observed. The second parameter is a configuration object that specifies the specific changes to be observed. There are the following types:

attribute describe
childList If you need to observe the child nodes of the target node (add a child node or remove a child node), set to true
attributes If you need to observe the attribute node of the target node (add or delete an attribute, and the attribute value of an attribute has changed), set it to true
characterData If the target node is a characterData node (an abstract interface, which can be a text node, a comment node, and a processing instruction node), you should also observe whether the text content of the node has changed, set it to true
subtree In addition to the target node, if you also need to observe all descendant nodes of the target node (observe the above three node changes on the whole DOM tree contained in the target node), set to true
attributeOldValue On the premise that the attributes attribute has been set to true, if you need to record the attribute value before the changed attribute node (recorded in the oldValue attribute of the MutationRecord object below), set it to true
characterDataOldValue On the premise that the characterData attribute has been set to true, if you need to record the text content before the changed characterData node (recorded in the oldValue attribute of the MutationRecord object below), set it to true
attributeFilter An array of attribute names (no namespace needs to be specified), which will be observed only when the attribute names contained in the array change, and will be ignored when the attributes of other names change

The MutationObserver can only monitor such as attribute changes, adding or deleting child nodes, etc. there is no way for itself to be deleted. It can meet the requirements by monitoring the parent node. Therefore, the code after the final transformation is:

   (function () {
      // Implement watermark with canvas
      function __canvasWM({
        // Use the function default value method of ES6 to set the default value of parameters
        // See for details https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
        container = document.body,
        width = '300px',
        height = '200px',
        textAlign = 'center',
        textBaseline = 'middle',
        font = "20px Microsoft Yahei",
        fillStyle = 'rgba(184, 184, 184, 0.6)',
        content = 'Please don't pass it on',
        rotate = '30',
        zIndex = 1000
      } = {}) {
        const args = arguments[0];
        const canvas = document.createElement('canvas');

        canvas.setAttribute('width', width);
        canvas.setAttribute('height', height);
        const ctx = canvas.getContext("2d");

        ctx.textAlign = textAlign;
        ctx.textBaseline = textBaseline;
        ctx.font = font;
        ctx.fillStyle = fillStyle;
        ctx.rotate(Math.PI / 180 * rotate);
        ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);

        const base64Url = canvas.toDataURL();
        const __wm = document.querySelector('.__wm');

        const watermarkDiv = __wm || document.createElement("div");
        const styleStr = `
          position:absolute;
          top:0;
          left:0;
          width:100%;
          height:100%;
          z-index:${zIndex};
          pointer-events:none;
          background-repeat:repeat;
          background-image:url('${base64Url}')`;

        watermarkDiv.setAttribute('style', styleStr);
        watermarkDiv.classList.add('__wm');

        if (!__wm) {
          container.style.position = 'relative';
          container.insertBefore(watermarkDiv, container.firstChild);
        }
        
        const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
        if (MutationObserver) {
          let mo = new MutationObserver(function () {
            const __wm = document.querySelector('.__wm');
            // Only in__ The wm element is called again only after it changes__ canvasWM
            if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {
              // Avoid constant triggering
              mo.disconnect();
              mo = null;
            __canvasWM(JSON.parse(JSON.stringify(args)));
            }
          });

          mo.observe(container, {
            attributes: true,
            subtree: true,
            childList: true
          })
        }

      }

      if (typeof module != 'undefined' && module.exports) {  //CMD
        module.exports = __canvasWM;
      } else if (typeof define == 'function' && define.amd) { // AMD
        define(function () {
          return __canvasWM;
        });
      } else {
        window.__canvasWM = __canvasWM;
      }
    })();

    // call
    __canvasWM({
      content: 'TEST'
    });

Generate watermark through SVG

Svg: Scalable Vector Graphics (SVG) is a graphic format based on extensible markup language (XML) to describe two-dimensional vector graphics. SVG is an open standard developed by W3C-- Wikipedia

Compared with Canvas, SVG has better browser compatibility. The way of generating watermark using SVG is similar to that of Canvas, except that the generation method of base64Url is replaced by SVG. The details are as follows:

     (function () {
      // Implement watermark with svg
      function __svgWM({
        container = document.body,
        content = 'Please don't pass it on',
        width = '300px',
        height = '200px',
        opacity = '0.2',
        fontSize = '20px',
        zIndex = 1000
      } = {}) {
        const args = arguments[0];
        const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${width}">
  <text x="50%" y="50%" dy="12px"
    text-anchor="middle"
    stroke="#000000"
    stroke-width="1"
    stroke-opacity="${opacity}"
    fill="none"
    transform="rotate(-45, 120 120)"
    style="font-size: ${fontSize};">
    ${content}
  </text>
</svg>`;
        const base64Url = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
        const __wm = document.querySelector('.__wm');

        const watermarkDiv = __wm || document.createElement("div");
     // ...
     // Consistent with canvas
     // ...
    })();

    __svgWM({
      content: 'TEST'
    })

Generate watermark through NodeJS

As a modern front-end developer, node JS also needs to be mastered. We can also generate web page watermarks through NodeJS (for performance reasons, the better way is to use the user's client to generate them). The front end sends a request, the parameters are watermarked, and the background returns the picture content.
Specific implementation (Koa2 environment):

  • Install gm and related environment. See gm document for details
  • ctx.type = 'image/png'; Set response to picture type
  • The process of generating pictures is asynchronous, so you need to wrap a layer of Promise so that you can use async/await to generate CTX Body assignment
const fs = require('fs')
const gm = require('gm');
const imageMagick = gm.subClass({
  imageMagick: true
});


const router = require('koa-router')();

router.get('/wm', async (ctx, next) => {
  const {
    text
  } = ctx.query;

  ctx.type = 'image/png';
  ctx.status = 200;
  ctx.body = await ((() => {
    return new Promise((resolve, reject) => {
      imageMagick(200, 100, "rgba(255,255,255,0)")
        .fontSize(40)
        .drawText(10, 50, text)
        .write(require('path').join(__dirname, `./${text}.png`), function (err) {
          if (err) {
            reject(err);
          } else {
            resolve(fs.readFileSync(require('path').join(__dirname, `./${text}.png`)))
          }
        });
    })
  })());
});

If it is just a simple watermark display, it is recommended to generate it in the browser for better performance

Image watermark generation solution

In addition to adding a watermark to the web page, sometimes we need to add a watermark to the picture. In this way, after the user saves the picture, the watermark source information is brought, which can not only protect the copyright, but also prevent the disclosure of other information of the watermark.
Watermark the picture through canvas to achieve the following:

    (function() {
      function __picWM({
        url = '',
        textAlign = 'center',
        textBaseline = 'middle',
        font = "20px Microsoft Yahei",
        fillStyle = 'rgba(184, 184, 184, 0.8)',
        content = 'Please don't pass it on',
        cb = null,
        textX = 100,
        textY = 30
      } = {}) {
        const img = new Image();
        img.src = url;
        img.crossOrigin = 'anonymous';
        img.onload = function() {
          const canvas = document.createElement('canvas');
          canvas.width = img.width;
          canvas.height = img.height;
          const ctx = canvas.getContext('2d');

          ctx.drawImage(img, 0, 0);
          ctx.textAlign = textAlign;
          ctx.textBaseline = textBaseline;
          ctx.font = font;
          ctx.fillStyle = fillStyle;
          ctx.fillText(content, img.width - textX, img.height - textY);

          const base64Url = canvas.toDataURL();
          cb && cb(base64Url);
        }
      }

        if (typeof module != 'undefined' && module.exports) {  //CMD
        module.exports = __picWM;
      } else if (typeof define == 'function' && define.amd) { // AMD
        define(function () {
          return __picWM;
        });
      } else {
        window.__picWM = __picWM;
      }
      
    })();

    // call
    __picWM({
        url: 'http://localhost:3000/imgs/google.png',
        content: 'TEST',
        cb: (base64Url) => {
          document.querySelector('img').src = base64Url
        },
      });

Watermark images in batches through NodeJS

We can also use gm this library to add watermarks to images

function picWM(path, text) {
  imageMagick(path)
    .drawText(10, 50, text)
    .write(require('path').join(__dirname, `./${text}.png`), function (err) {
      if (err) {
        console.log(err);
      }
    });
}

If you need to batch process pictures, you only need to traverse the relevant files.

If it is just a simple watermark display, it is recommended to generate it in the browser for better performance

expand

Hidden watermark

Some time ago, Ali found the leaker of the moon cake incident with a screenshot. In fact, he used a hidden watermark. In fact, this is not the front-end category to a great extent, but we should also understand. The AlloyTeam team wrote an unspeakable secret - Image Steganography that can also be played at the front end. The "hidden watermark" is added to the image through Canvas. For the image saved by the user, the hidden content can be easily restored, but there is nothing to do with the screenshots or processed photos. However, for the display of some confidential image files, this technology can be used secretly.
Use encrypted watermark content

The watermark generated by the front end can also be generated by others in the same way. There may be "blame on others" (this may be too much concern). We still need to have a safer solution. The watermark content can contain a variety of encoded information, including user name, user ID, time and so on. For example, we just want to save the unique user ID of the user. We need to pass the user ID into the md5 method below to generate the unique ID. The encoded information is irreversible, but it can be traced by traversing all users globally. In this way, the watermark can be prevented from counterfeiting and the information of the real watermark can be traced.

// MD5 encryption library utility
const utils = require('utility')

// Salt MD5
exports.md5 =  function (content) {
  const salt = 'microzz_asd!@#IdSDAS~~';
  return utils.md5(utils.md5(content + salt));
}

summary

We can't be careless about security issues. For some sensitive content, we can use the above watermarking schemes in combination, so as to maximize the role of warning to the browser and reduce the leakage of secrets. Even if the secret is leaked, it is possible to track the leaker.

Posted by Tyco on Wed, 25 May 2022 12:17:07 +0300