17
头图

Preface

Hello everyone~ I am Rongding, and the Mid-Autumn Festival is coming soon~ I wish you all a happy Mid-Autumn Festival, family reunion, happiness and health~

The reason why there will be an issue of copyright protection this time is because a group of friends said that his article was stolen some time ago, so out of curiosity about copyright protection methods, there is this article~

There are many ways to protect copyright:
This article mainly talks about how to hide your copyright information in text and pictures (the next article will give you a detailed discussion about the watermark elements that cannot be removed)

This article participated in the Mid-Autumn Festival activities, so the examples in the article mainly focus on the theme of the Mid-Autumn Festival. Everyone likes and supports~ Thank you~ 🥰

reading this article, you will learn

Text steganography

First of all, we need to understand what is steganography (Steganography)

Steganography: hiding information in a variety of carriers, such as videos, hard disks and images, and embedding the information that needs to be hidden into the carrier in a special way without compromising the original expression of the carrier’s information. It is designed to protect the information that needs to be hidden from being recognized by others.

Of course, information concealment technology is definitely more than steganography. There are probably: 1) steganography, 2) digital watermarking, 3) covert channel, 4) sub-valve channel, 5) anonymous channel...

Realization principle

By converting each character in the string to only 1 and 0 representation, and then using zero-width characters to represent 0 and 1, you can use zero-width characters to represent a string of invisible strings

First of all, let us understand what is a zero-width character?

As the name implies: it is a special character with a byte width of 0. 🙄 This explanation...then I go?

😀 Zero-width character: It is a non-printable Unicode character that is not visible in browsers and other environments, but it does exist. It will also occupy a position when obtaining the length of the string, indicating a certain control function character.

zero-width characters mainly include the following six types:

  • Zero-width space U+200B: used for line breaks for longer words
  • Zero width no-break space U+FEFF: Used to prevent line breaks at specific positions
  • Zero-width joiner (zero-width joiner) U+200D: Used in Arabic and Indian languages ​​to produce a ligature effect between characters that do not ligature
  • Zero-width non-joiner (zero-width non-joiner) U+200C: Used in Arabic, German, Hindi and other scripts to prevent ligatures between characters that can be ligated
  • Left-to-right mark (left-to-right mark) U+200E: Used in multilingual texts with mixed text directions, specifying that the typesetting text writing direction is left to right
  • Right-to-left mark (right-to-left mark) U+200F: Used in multilingual texts with mixed text directions, specifying that the typesetting text writing direction is right to left

Considering the so-called Surrogate Pair in Unicode, the core methods for processing strings here are codePointAt and fromCodePoint

Surrogate Pair : It is the encoding method used for extended characters in UTF-16. It uses four bytes (two UTF-16 encoding) to represent a character.

Unicode code points range from 0 to 1,114,111. The first 128 Unicode code units are the same as ASCII character codes.
If the specified index is less than 0 or greater than the length of the string, charCodeAt returns NaN

For example: the code point of the Chinese character "𠮷" (not "吉") is 0x20BB7, and the UTF-16 encoding is 0xD842 0xDFB7 (55362 57271 in decimal), which requires 4 bytes to store.
For this 4-byte character, JavaScript cannot handle it correctly, and the string length will be misjudged as 2, "𠮷".length // 2 And the charAt() method cannot read the entire character, the charCodeAt() method can only Return the values ​​of the first two bytes and the last two bytes respectively.
ES6 provides the codePointAt() method, which can correctly process the characters stored in 4 bytes and return the code point of a character.

//一个字符由两个字节还是由四个字节组成的最简单方法。
"我".codePointAt(0) > 0xFFFF;//false
"A".codePointAt(0) > 0xFFFF;//false
"3".codePointAt(0) > 0xFFFF;//false

"𠮷".codePointAt(0) > 0xFFFF;//true
console.log(String.fromCodePoint(0x20bb7)); // "𠮷"
//或者十进制
console.log(String.fromCodePoint(134071)); // "𠮷"
//也可以多个参数
console.log(String.fromCodePoint(25105,29233,0x4f60)); // "我爱你"

String.fromCodePoint method is a newly added feature of ES6. ES6 provides the String.fromCodePoint() method, which can recognize characters larger than 0xFFFF, which makes up for the shortcomings of the String.fromCharCode() method. Some old browsers may not yet support it. You can use this polyfill code to ensure browser support

Of course, if you don’t need to deal with complex characters, you can also use the charCodeAt and fromCharCode methods to process the characters

Alright, the paving is complete, let's dry~

first talk about encryption

We use the codePointAt() method to get the Unicode code point value of each character, and then convert them to binary, and each character is separated by spaces, and then we use zero-width characters &#8203. Represents 1 , uses zero-width character &#8204 to represent 0 , uses zero-width character &#8205 to represent spaces invisible string that is completely represented by zero-width characters

more, don’t talk about less~ Look at the code first!

  // 字符串转零宽字符串
  function encodeStr(val = "隐藏的文字") {
      return (
          val
              .split("")
              .map((char) => char.codePointAt(0).toString(2))
              .join(" ")
              .split("")
              .map((binaryNum) => {
                  if (binaryNum === "1") {
                      return "​"; // 零宽空格符​
                  } else if (binaryNum === "0") {
                      return "‌"; // 零宽不连字符‌
                  } else {
                      return "‍"; //空格 -> 零宽连字符‍
                  }
              })
              .join("‎")
      );
  }
  console.log("str:",encodeStr(),"length:",encodeStr().length)//大家可以把这段copy到控制台执行以下看看

At the beginning of this piece, return are all zero-width characters written directly in double quotation marks, but it is not easy to see if you copy it to the browser. Anyway, it is invisible. If the platform filters it for me, it will be more embarrassment

want vscode or atom see whether there code zero-width characters, download Highlight Bad Chars plugins for zero-width character is highlighted (shown above)

In order to make it easier for everyone to distinguish between empty strings and zero-width characters, I changed the code here (here I am done with vue, and all my examples will be in the warehouse . Of course, it is also on the experience address , Everyone is welcome to give pointers~)


// 字符串转零宽字符串
encodeStr() {
    this.cipherText = this.text.split("");
    //在字符串中的随机一个位置插入加密文本
    this.cipherText.splice(
        parseInt(Math.random() * (this.text.length + 1)),
        0,
        //加密的文本
        this.hiddenText
            .split("")
            //['荣', '顶' ]
            .map((char) => char.codePointAt(0).toString(2))
            // ['1000001101100011','1001100001110110']
            .join(" ")
            //"1000001101100011 1001100001110110"
            .split("")
            /* [ '1', '0', '0', '0',  '0', '0', '1', '1', '0', '1', '1', '0', '0',  '0', '1', '1', ' ',
                '1', '0', '0', '1', '1',  '0', '0', '0', '0', '1', '1', '1', '0', '1',  '1', '0'] */
            .map((binaryNum) => {
                if (binaryNum === "1") {
                    return String.fromCharCode(8203); // 零宽空格符​
                } else if (binaryNum === "0") {
                    return String.fromCharCode(8204); // 零宽不连字符‌
                } else {
                    return String.fromCharCode(8205); //空格 -> 零宽连字符‍
                }
            })
            //对上面所有的数组元素进行处理,生成一个新的数组['​', '​', '‌'......]其中每一个元素都是零宽字符,分别代表0和1以及
            .join(String.fromCharCode(8206))
        // 用左至右符‎来把上面的数组相连成一个零宽字符串=>"‎​‎‌‎‌"
    );
    this.cipherText = this.cipherText.join("");
    console.log(this.cipherText, "cipherText");
}

Among them, the random mixing of zero-width strings is mainly this pseudo code

let str = "qwe12345789".split("");
//在字符串中的随机一个位置插入加密文本
str.splice(parseInt(Math.random()*(str.length+1)),0,"加密文本").join("")

The encrypted text can be sent through trim or over the network.

"中秋节快乐​‎​‎‌‎​‎​‎‌‎‌‎‌‎‌‎​‎​‎​‎‌‎‌‎‌‎‍‎​‎​‎‌‎‌‎​‎‌‎​‎​‎​‎​‎‌‎‌‎‌‎‌‎‌‎‍‎​‎‌‎‌‎‌‎‌‎​‎‌‎‍‎​‎​‎​‎‌‎​‎‌‎​‎‍‎​‎​‎‌‎‌‎​‎​‎​".trim().length//114

everyone knows how to encrypt, let's look at the following how to decrypt a string

Decryption first needs to understand how to extract zero-width characters

"点赞鼓励~😀​‎​‎​‎‌‎‌‎​‎‌‎‌‎​‎‌‎‌‎​‎‌‎‌‎‌‎‍‎​‎​‎‌‎‌‎​‎​‎​‎‌‎​‎‌‎‌‎‌‎‌‎​‎​‎‍‎​‎‌‎‌‎​‎​‎​‎​‎​‎​‎​‎‌‎‌‎‌‎‌‎​‎‍‎​‎​‎‌‎‌‎‌‎‌‎‌‎‌‎​‎​‎‌‎​‎​‎​‎​‎‍‎​‎​‎‌‎​‎​‎‌‎‌‎‌‎‌‎‌‎​‎​‎​‎​‎‌‎​‎‍‎​‎​‎‌‎​‎​‎​‎​‎‌‎‌‎‌‎‌‎‌‎‌‎‌‎‌‎‌".replace(/[^\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

The decryption process is the reverse operation of encryption. We first extract the zero-width character string in the text, use 1 to represent zero-width character &#8203, use 0 to represent zero-width character &#8204, and use space to represent zero-width character &#8205, so you can get a space-separated string consisting of 1 and 0. After we convert the string represented by each 1/0 to decimal, String.fromCharCode can convert it back to be visible through the 0614161c49f16a method You can see the text~

// 零宽字符转字符串
decodeStr() {
    if (!this.tempText) {
        this.decodeText = "";
        return;
    }
    let text = this.tempText.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
    let hiddenText = this.tempText.replace(/[^\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");
    console.log(text, "text");
    console.log(hiddenText, "hiddenText");
    this.decodeText = hiddenText
        .split("‎") //不是空字符串,是 ‎
        .map((char) => {
            if (char === "​" /* 不是空字符串,是​ */) {
                return "1";
            } else if (char === "‌" /*  不是空字符串,是‌ */) {
                return "0";
            } else {
                /* 是‍时,用空格替换 */
                return " ";
            }
        })
        .join("")
        //转数组
        .split(" ")
        //根据指定的 Unicode 编码中的序号值来返回一个字符串。
        .map((binaryNum) => String.fromCharCode(parseInt(binaryNum, 2)))
        .join("");
    console.log(text + hiddenText);
},

Demo a wave

core implementation method of 1614161c49f204 is the above. Now let’s demonstrate. Before you look at the picture, you can first copy this small text to any place and print 1614161c49f206 (the most convenient for F12 console) console.log("Happy Mid-Autumn Festival 123456 - ‎‌‎‌‎‌‎​‎​‎‍‎​‎‌‎‌‎​‎​‎​‎​‎​‎​‎​‎‌‎‌‎‌‎‌‎​‎‍‎​‎​‎‌ ‎‌‎‌‎‌‎‌‎‌‎​‎​‎‌‎​‎​‎​‎​‎​‎‍‎​‎​‎‌‎​‎​‎‌‎‌‎‌‎‌‎‌‎​‎​ The length must not be 10 😀

I have uploaded the demo here to my github, you can click here experience it~

Application scenario

  • Data anti-climbing
    Insert zero-width characters into the text to interfere with keyword matching. The data with zero-width characters obtained by the crawler will affect their analysis, but will not affect the user's reading data.
  • Hidden text information
    Insert a custom combination of zero-width characters into the text, and the user will carry invisible information after copying, achieving the effect of hiding the text information.
  • Escape the filtering of sensitive words
    You curse in the game, your mother* must not be sent out, but you send your xxxxx mother yyyyy* is different 😀, this thing is still very interesting to study.
  • There are a lot of embedding hidden codes and so on...

Imagine embedding the purchaser's personal information in an e-book PDF or some genuine film and television music works, then how many people dare to pass it directly to others?

like the following watermark, there is no difference between adding and not adding it, it is easy to be deducted by others

and the watermark was replaced by someone else, advertised, tut tut...If a large amount of steganographic content is embedded in an e-book, it can actually play a very good role in accountability for the spread of piracy. (Major publishers, e-books can be made 😀 Buy e-books with the contact information and personal information of the

Such as, toG project, there are a lot of sensitive content are all clear watermark of personal information, personal information and steganography, came to know who the pass (at least for people who do not understand the technology, the steganographic is really hard to find)

Prevent the implantation of zero-width characters

Having said so much about how to embed and extract zero-width characters, like Nuggets someday will not allow you to add this stuff in it, they will use regularization for all the text and it will be gone.

Prevents zero-width characters in the text uploaded by users. Generally, we can do a replacement first.

str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

Picture steganography

What is image steganography?
lsb steganography is very practical, the algorithm is simple, and the amount of information that can be stored is also large, not to mention the frequent visitors in the CTF competition (originated from the 1996 DEFCON Global Hacking Conference)

Realization principle

There are many ways to steganography of pictures, I only use the simplest method (LSB: the lowest bit of the binary, not "old X" ) 1614161c49f52b for demonstration, of course, it is also because of my cooking 😥 The math is not good, or I will You can use the Fourier transform to process the color wave image (the image is essentially the superposition of various color waves a specific explanation of the concept of 1614161c49f52d, you can see the article Yifeng 1614161c49f530), so handsome and floating ~ Harm!

Least significant bit of binary

What is the lowest bit of binary? It is last bit of binary,

Okay, don’t talk about more, don’t talk about less~ Let’s look at pictures and talk together~

The lowest bit of each binary value can represent a 1-bit data, a pixel needs RGBA, 4 values ​​total 4 * 8 = 32 bits, so at least eight pixels (32 values) can be represented by the lowest bit The RGBA value of a pixel is (2560000 / 4 = 640000 pixels here can be used to store the RGBA value of

That is: the lowest binary bit of every 32 values ​​can be used to represent the RGBA value of a standard pixel (for ease of understanding, as shown in the figure below 👇)

Get the data that needs to be hidden

In this step, we can hide the picture, hide the text, or hand-paint something on the canvas.
Convert the picture we have drawn or loaded on the main canvas into a URL, and by creating a temporary small canvas, the URL generated by the main canvas is drawn on the temporary small canvas drawImage

pixel value of the main canvas mentioned above can only store the RGBA value of 80,000 pixels) 1614161c49f6aa

//将画布上的信息绘制到小画布上保存起来
saveHiddenImageData() {
    const tempCanvas = document.createElement("canvas");
    const tempCtx = tempCanvas.getContext("2d");
    //小画布的长宽=大画布的像素/8后再开平方
    //因为需要八个像素的最低位才可以表示一个小画布的像素的RGBA值
    tempCanvas.width = Math.floor(Math.sqrt((this.canvas.width * this.canvas.height) / 8));
    tempCanvas.height = Math.floor(Math.sqrt((this.canvas.width * this.canvas.height) / 8));
    var image = new Image();
    image.src = this.canvas.toDataURL("image/png");
    image.onload = () => {
        //绘制图像到临时的小画布
        tempCtx.drawImage(image, 0, 0, tempCanvas.width, tempCanvas.height);
        this.hiddenData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
        this.hiddenData.binaryList = Array.from(this.hiddenData.data, (color) => {
            color = color.toString(2).padStart(8, "0").split("");
            return color;
        });
        console.log(this.hiddenData, "hiddenData");
        this.$message({
            type: "success",
            message: "保存成功!请选择目标图片~",
        });
        this.canvas.clear();
    };
},

Take the data of the target map again

When taking the data of the target image (the target image you want to hide the hidden data into), we need pre-process all the color values ​​of

This step is very important.As you can see, in the picture above, when the target image is loaded on the canvas, we can read all the pixel values ​​of the page getImageData All color values ​​are processed as even/not odd and not even (0)

We operate a minimum value of each bit is used to store data to be saved, naked eye can not see your changes to the lowest level of

//获取画布像素数据
getCanvasData() {
    this.targetData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
    //将数字化为非奇数
    function evenNum(num) {
        num = num > 254 ? num - 1 : num;
        num = num % 2 == 1 ? num - 1 : num;
        return num;
    }
    //存一个二进制的数值表示
    this.targetData.binaryList = Array.from(this.targetData.data, (color, index) => {
        this.targetData.data[index] = evenNum(this.targetData.data[index]);
        color = evenNum(color).toString(2).padStart(8, "0").split("");
        return color;
    });
    console.log(this.targetData);
    this.loading = false;
},

Write hidden data

At this point, we have got the hidden data and the data of the target image. The next thing we need to do is to write these 318,096 color values ​​into the lowest binary bit of the color values ​​of the target image, 1614161c49f7e1. Realize the steganography of pictures

again needs to pay attention to : if we do it through the lowest bit, the data we picture is limited, that is, the overall pixels of the 1614161c49f807 picture / 8 = the pixels that can be hidden in the picture (a)
Here we use the 800*800 canvas, there 640000 pixels, which can be hidden 640000/8 = 80000 pixels
So our hidden data can only be drawn to Math.floor(Math.sqrt((this.canvas.width * this.canvas.height) / 8)) which is 282 wide and high canvas (79,524 pixels, 318,096 color values). The number calculated here can only be rounded down, otherwise it will overflow and cause loss of hidden The data!

The operation here is like we hide the head poem, we hide the data at the end

Small changes in RGB component values ​​are indistinguishable by the naked eye and do not affect the recognition of the picture. You can't see the difference of this +1


Don’t talk about more, don’t talk about less~Look at the code first

//将隐写的资源图片数据存到目标图片的二进制最低位中
drawHiddenData() {
    //将隐藏的数据的二进制全部放到一个数组里面
    let bigHiddenList = [];
    for (let i = 0; i < this.hiddenData.binaryList.length; i++) {
        bigHiddenList.push(...this.hiddenData.binaryList[i]);
    }
    console.log(bigHiddenList, "bigHiddenList");
    this.targetData.binaryList.forEach((item, index) => {
        bigHiddenList[index] && (item[7] = bigHiddenList[index]);
    });
    this.canvas.clear();
    this.targetData.data.forEach((item, index) => {
        this.targetData.data[index] = parseInt(
            this.targetData.binaryList[index].join(""),
            2
        );
    });

    const tempCanvas = document.createElement("canvas");
    tempCanvas.width = 800;
    tempCanvas.height = 800;
    let ctx = tempCanvas.getContext("2d");
    ctx.putImageData(this.targetData, 0, 0);
    fabric.Image.fromURL(tempCanvas.toDataURL(), (i) => {
        this.canvas.clear();
        this.canvas.add(i);
        this.canvas.renderAll();
    });
    this.$message({
        type: "success",
        message: "加密成功!",
    });
},

As you can see, one picture has been hidden in another picture here, (we have implicitly written a moon pie chart into the moon picture here)

Parse the encrypted picture

After completing the image encryption, what we need to do next is to parse the encrypted image

First, after we select the local picture, we render it to the canvas. After getImageData , we establish a binary storage representation. Later, we will take out the picture hidden in the target image through its lowest bit.

//获取画布像素数据
    getCanvasData() {
        this.targetData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
        //存一个二进制的数值表示
        this.targetData.binaryList = Array.from(this.targetData.data, (color, index) => {
            color = color.toString(2).padStart(8, "0").split("");
            return color;
        });
        console.log(this.targetData);
        this.loading = false;
    },

Extract the hidden color value from the picture

Here we mainly extract the lowest bit of the binary color value of the main canvas, assemble it into a pixel value array of the hidden picture, and finally draw the hidden picture putImageData

is very important to note here: The putImageData of the first parameter of data , must be a multiple of 4 of the product of the two sides otherwise an error will be reported
So, when we take pixels here, we need to take Math.pow(Math.floor(Math.sqrt(2560000 / 32)), 2) * 4 like this. Since I am 800 * 800 here, it is 2560000 values. You can directly write the variable canvas.width * canvas.height *4

//解析图片
decryptImage() {
    const c = document.getElementById("decryptCanvas");
    const ctx = c.getContext("2d");

    let decryptImageData = [];

    for (let i = 0; i < this.targetData.binaryList.length; i += 8) {
        let tempColorData = [];
        for (let j = 0; j < 8; j++) {
            tempColorData.push(this.targetData.binaryList[i + j][7]);
        }
        decryptImageData.length < Math.pow(Math.floor(Math.sqrt(2560000 / 32)), 2) * 4 &&
            decryptImageData.push([...tempColorData]);
    }
    decryptImageData = Uint8ClampedArray.from(decryptImageData, (z) => {
        z = parseInt(z.join(""), 2);
        return z;
    });
    console.log(decryptImageData, "decryptImageData");
    //需要注意的是putImageData的data的长度必须为两个边的乘积的4的倍数
    ctx.putImageData(
        new ImageData(
            decryptImageData,
            Math.floor(Math.sqrt(2560000 / 8 / 4)),
            Math.floor(Math.sqrt(2560000 / 8 / 4))
        ),
        0,
        0
    );
},

After this step, we can see that we can extract the hidden pictures from the main canvas!!!

wow~ is it very interesting?

Also need to pay attention to : Steganographic images in LSB mode can only be stored in PNG or BMP image format, and lossy compression (such as JPEG) cannot be used, otherwise the steganographic data will be lost!

Don't use steganography to do bad things about illegal crimes!!! because it can prevent (a wall) monitoring, for example, if you hide the bad boy picture in the good boy picture, I'm going too bad 😢

But as a programmer, you can use it to confess to your beloved baby, which is a romantic way~

Interested friends can also take a look at this paper StegaStamp: Invisible Hyperlinks in Physical Photographs (StegaStamp: Invisible Hyperlinks in Physical Photographs) , the author is from the University of California, Berkeley. Their project homepage is here

The above is the steganographic related content of text and pictures. Of course, the steganographic medium cannot be limited to this. There are many others. As long as the computer can express everything with numbers, it can be used for steganography. , Such as audio steganography , and video steganography

finally

Steganography is a very deep and widely applied science, and it's very general here, and it's a good idea. The steganography of pictures and text is just the easiest part. Those who are interested can read a book called "Data Hiding Techniques Revealed". Friends who need it can also add me, I will send it to you!
The example in the article has been placed in my github , of course, you can also experience it here

I am Rongding, I am very happy to be strong here with you! Together for happy programming! 😉

If you also love front-end related technology very much! Welcome to my little secret circle~ There are big guys inside, take you to fly! 🦄 scan 👇~


荣顶
570 声望2.5k 粉丝

​公​众​号​​‎​‎‌‎‌‎‌‎​: 前端超人