I am participating in the Mid-Autumn Festival Creative Submission contest, please see: Mid-Autumn Festival Creative Submission Contest for details

preface

Hello everyone ~ I am rongding, soon the Mid-Autumn Festival ~ first here I wish you a happy Mid-Autumn Festival, family reunion, happiness and well-being ~

We all know that there are many methods of copyright protection. This article mainly tells us how to hide your copyright information in text and pictures

This article participated in the Mid-Autumn Festival activities, so the examples in the article mainly around the theme of the Mid-Autumn Festival to explain, everyone like to support ~ thank you ~ 🥰

After reading this article, you will learn 👇

  • Text steganography: through some method can actually read and modify the hidden message in the string “happy Mid-Autumn Festival “⇄” [hidden data] happy autumn “, click here to experience

  • Steganography: Adding hidden text to an image (and writing files inside it!!) Click here to experience

Text steganography

The first thing we need to know is what Steganography is.

Steganography: Hides information in multiple media, such as videos, hard disks, and images. The hidden information is embedded into the media in a special way without compromising the expression of the original information. Designed to protect information that needs to be hidden from others.

Of course, information concealment technology must be more than steganography, about: 1) steganography, 2) digital watermarking, 3) covert channel, 4) valve channel, 5) anonymous channel…

Realize the principle of

It is possible to represent an invisible string by zero-width characters by converting each character in the string to a representation of only 1 and 0, and then representing 0 and 1 by zero-width characters

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

As the name suggests: special characters with a byte width of 0. 🙄 this explains… Then I go?

😀 zero-width character: a non-printable Unicode character that is not visible in the browser environment, but does exist and will be used to obtain the length of the string to indicate some control function.

There are six types of zero-width characters:

  • Zero-width space U+200B: Newline separation for longer words
  • Zero width no-break space U+FEFF: Used to prevent newline separation at a particular position
  • Zero-width joiner U+200D: used in Arabic and Hindi languages to create a joiner between characters that cannot be joined
  • Zero-width non-joiner U+200C: used in Arabic, German, and Hindi languages to prevent the hyphen effect between characters that would be joined
  • Left-to-right mark U+200E: Used in multilingual text with mixed text orientations to specify that typesetting text is written left-to-right
  • Right-to-left mark U+200F: Used to specify that typesetted text is written right-to-left in multilingual text with mixed text orientations

Given the fact that Unicode has a so-called Surrogate Pair, our core methods for dealing with strings are codePointAt and fromCodePoint

Surrogate Pair: A utF-16 encoding method used to extend characters. It uses four bytes (two UTF-16 encoding) to represent a character.

Unicode code points range from 0 to 1,114,111. The leading 128 Unicode encoding units are the same as the ASCII character encoding. 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 “ji “) is 0x20BB7, while the utF-16 code is 0xD842 0xDFB7 (55362 57271 in decimal), which requires four bytes of storage. JavaScript does not handle these 4-byte characters correctly, misinterpreting the string length as 2, “𠮷”.length // 2, and charAt() cannot read the entire character. CharCodeAt () only returns the first and last two bytes, respectively. ES6 provides a codePointAt() method that correctly handles 4-byte stored characters and returns a character code point.

// The simplest way to make a character consist of two bytes or four bytes.
"我".codePointAt(0) > 0xFFFF;//false
"A".codePointAt(0) > 0xFFFF;//false
"3".codePointAt(0) > 0xFFFF;//false

"𠮷".codePointAt(0) > 0xFFFF;//true
Copy the code
console.log(String.fromCodePoint(0x20bb7)); / / "𠮷"
// Or decimal
console.log(String.fromCodePoint(134071)); / / "𠮷"
// Multiple arguments can also be used
console.log(String.fromCodePoint(25105.29233.0x4f60)); // "I love you"
Copy the code

The string.fromCodePoint method is a new feature added to ES6, which provides the String.fromCodePoint() method, which can recognize characters larger than 0xFFFF, to compensate for the string.fromCharcode () method. Some older browsers may not support it yet. You can guarantee browser support by using this polyfill code

Of course, you can use it if you don’t have to deal with complicated characterscharCodeAtandfromCharCodeTwo methods to process characters

All right, we’re done. Let’s go

Say first encryption

We pass the text through codePointAt() Method to obtain the Unicode code point value for each character, then convert them to base 2, each character is separated by a space, then we use the zero-width character ​ to represent 1, the zero-width character ‌ to represent 0, and the zero-width character ‍ to represent a space, so that we have a completely zero-width character invisible The string

More don’t say, less don’t Lao ~ first look at the code!

  // String to zero-width string
  function encodeStr(val = "Hidden words.") {
      return (
          val
              .split("")
              .map((char) = > char.codePointAt(0).toString(2))
              .join("")
              .split("")
              .map((binaryNum) = > {
                  if (binaryNum === "1") {
                      return ""; // zero space ​
                  } else if (binaryNum === "0") {
                      return "‌"; // zero width non-hyphen ‌
                  } else {
                      return "‍"; // space -> zero width hyphen ‍
                  }
              })
              .join("‎")); }console.log("str:",encodeStr(),"length:",encodeStr().length)// You can copy this section to the console to perform the following

Copy the code

In the beginning, the return is a zero width character written in double quotes, but it’s probably not easy to see it in your browser. It’s not visible anyway, and it would be even more awkward if the platform filtered it

Want to be invscodeoratomTo see if there is any in the codeNull wide character, you candownloadHighlight Bad CharsPlugins to highlight zero-width characters (as shown above)

In order to make it easier for you to distinguish the empty string from the zero-width character, I have changed the code here.


// String to zero-width string
encodeStr() {
    this.cipherText = this.text.split("");
    // Inserts encrypted text at a random position in the string
    this.cipherText.splice(
        parseInt(Math.random() * (this.text.length + 1)),
        0.// Encrypted text
        this.hiddenText
            .split("")
            //[' rong ', 'ding']
            .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); // zero space ​
                } else if (binaryNum === "0") {
                    return String.fromCharCode(8204); // zero width non-hyphen ‌
                } else {
                    return String.fromCharCode(8205); // space -> zero width hyphen ‍}})// Generate a new array [", ", '‌'......] by processing all the above array elements. Each element is a zero-width character representing 0 and 1 and
            .join(String.fromCharCode(8206))
        // Use the left to right character ‎ To concatenate the above arrays into a zero-wide string =>" scientistloger ‌ scientist‌"
    );
    this.cipherText = this.cipherText.join("");
    console.log(this.cipherText, "cipherText");
}
Copy the code

The random mixing of zero-width string is mainly this pseudo-code

let str = "qwe12345789".split("");
// Inserts encrypted text at a random position in the string
str.splice(parseInt(Math.random()*(str.length+1)),0."Encrypted text").join("")
Copy the code

Encrypted text sent through trim or over the network is fine

"Happy Mid-Autumn festival ‎ ‎ ‌ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‍ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‎ ‌ ‎ ‎ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‍ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‎ ‌ ‎ ‍ ‎ ‎ ‎ ‎ ‌ ‎ ‎ ‌ ‎ ‎ ‍ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‎ ‎".trim().length/ / 114
Copy the code

Now that we know how to encrypt, how do we decrypt a string

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

"Thumb up encourage ~ 😀 ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‎ ‌ ‎ ‌ ‎ ‎ ‌ ‎ ‌ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‍ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‎ ‎ ‎ ‌ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‎ ‎ ‍ ‎ ‎ ‌ ‎ ‌ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‎ ‍ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‎ ‎ ‌ ‎ ‎ ‎ ‎ ‎ ‍ ‎ ‎ ‎ ‌ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‎ ‎ ‎ ‎ ‌ ‎ ‎ ‍ ‎ ‎ ‎ ‌ ‎ ‎ ‎ ‎ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌ ‎ ‌".replace(/[^\u200b-\u200f\uFEFF\u202a-\u202e]/g."");
Copy the code

The decryption process is the reverse operation of encryption. We first extract the zero-width string from the text, and use 1 to represent the zero-width character ​, 0 to represent the zero-width character ‌, and space to represent the zero-width character ‍. In this way, we can get a string separated by space composed of 1 and 0 After the represented String has been converted to decimal, it can be converted back to visible text using the string.fromCharcode method

// Zero width character to string
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("‎") // Instead of an empty string, it is ‎
        .map((char) = > {
            if (char === "" /* Is not an empty string, is ​ * /) {
                return "1";
            } else if (char === "‌" /* Is not an empty string, is ‌ * /) {
                return "0";
            } else {
                / * & # 8205; , replace */ with a space
                return "";
            }
        })
        .join("")
        / / array
        .split("")
        Returns a string based on the ordinal value in the specified Unicode encoding.
        .map((binaryNum) = > String.fromCharCode(parseInt(binaryNum, 2)))
        .join("");
    console.log(text + hiddenText);
},
Copy the code

Demonstrate a wave

The core implementation method is above, now we will demonstrate, before you look at the picture, you can first copy this small paragraph of text anywhere to print (F12 The console to see the most convenient) console. The log (” happy Mid-Autumn festival 123456 ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ The length must not be 10😀

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

Application scenarios

  • Data crawl prevention inserts zero-width characters into text, interfering with keyword matching. Data with zero-width characters obtained by crawlers will affect their analysis, but not the user’s reading data.
  • To hide text information, you can insert a user-defined zero-width character into a text. After the text is copied, invisible information is carried by the user.
  • Escape the filter of sensitive words

    You swear in the game,Your mama *There’s no way it’s gonna go out, but you didXXXXX you mom yyyyy *Different 😀, this thing or study rise very interesting.
  • Embedding hidden code goes on and on…

If you embed the personal information of the buyer in electronic books, PDFS or some legitimate music works, how many people dare to send it to others directly?

Like the following kind of bright watermark, with and without no difference, it is easy to be deducted by others

But also by others to change the watermark, hit the advertising, TSK TSK… If a large amount of steganography content is embedded in an e-book, it can play a very good role in the dissemination of piracy (major publishers, e-books can be made 😀 to buy e-books have the buyer’s contact information and personal information).

In projects such as toG, there is a lot of sensitive content in the form of open watermarks with personal information, and steganography, which can be passed on by someone else (steganography is really hard to detect, at least for non-technical people).

Prevent insertion of zero – width characters

How to insert and extract characters with zero width? When will nuggets not allow you to insert characters with zero width characters? When will they use re for all text

To prevent the text uploaded by users from containing zero-width characters, we usually replace it once

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

Image steganalysis

What is picture steganography? LSB steganography is very practical, simple algorithm, can store a large amount of information, not to mention CTF competition regular (originated in 1996 DEFCON global hackers conference)

Realize the principle of

There are many ways to steganography images, but I will use the simplest one here (LSB: Binary lowest, not “X” old color 😅) making a presentation, of course, also because my food 😥 math is not good, otherwise I can also use Fourier transform by processing the color of the picture (image is essentially a variety of color wave superposition of the concrete explanation of the concept can see nguyen other teachers this article) to do, so handsome, gone with the wind ~ harm!

Lowest binary

What is the least significant binary? It’s the last bit of binary

Ok, more don’t say less don’t chatter ~ we look at the picture to talk ~

The lowest value of each binary can represent a 1bit of data, one pixel requires RGBA,4 values total 4 * 8=32 bits so at least 8 pixels (32 values) are required to represent the lowest value of a pixel RGBA (2560000/4 = here) 640000 pixels can be used to store RGBA values of 2560000/32 = 80000 pixels)

That is, the lowest binary of every 32 values can be used to represent the RGBA value of a standard pixel (for ease of understanding, see 👇 below).

Get the data you need to hide

In this step, we can hide the image, hide the text, or hand draw something on the canvas to convert the image we have drawn or loaded on the main canvas into a URL. By creating a temporary small canvas, the URL generated by the main canvas is reduced to the temporary small canvas by drawImage method

(Why do they do that here? Or because the bottom of the 640,000 pixel value of the main canvas mentioned above can only store the RGBA value of 80,000 pixels)

// Draw the information on the canvas to a smaller canvas and save it
saveHiddenImageData() {
    const tempCanvas = document.createElement("canvas");
    const tempCtx = tempCanvas.getContext("2d");
    // The length and width of the smaller canvas = the pixels of the larger canvas /8 and then square
    // Because you need a minimum of eight pixels to represent the RGBA value of a pixel of a small canvas
    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 = () = > {
        // Draw the image to a temporary small canvas
        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: "Saved successfully! Please select the target image ~"});this.canvas.clear();
    };
},
Copy the code

And get the target map data

We need to pre-process all the color values in order to get the data from the target graph (which you want to steganograph into)

This step is very important, as you can see in my image above, when the canvas is loaded with the target image, we can read all the pixel values of the page through the getImageData method. I have zeroed the lowest binary value of all the pixels, that is, I have processed all the color values to be even/non-odd or even (0).

We store the data we want to save by manipulating the lowest level of each value, and your changes to the lowest level are invisible to the naked eye

// Get canvas pixel data
getCanvasData() {
    this.targetData = this.ctx.getImageData(0.0.this.canvas.width, this.canvas.height);
    // Digitizing to non-odd
    function evenNum(num) {
        num = num > 254 ? num - 1 : num;
        num = num % 2= =1 ? num - 1 : num;
        return num;
    }
    // Save a binary numeric representation
    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;
},
Copy the code

Write hidden data

Now that we have the hidden data and the target image data, all we need to do is write the 318,096 color values into the lowest binary of the target image’s 2,560, 000 color values to achieve steganography

Again, note that by doing this at the lowest level, we have a limited amount of data that can be hidden in the image, i.e., the total number of pixels in the image / 8 = the number of pixels that can be hidden in the image. Here we are using an 800 × 800 canvas with 640,000 pixels, of which 640,000/8 = can be hidden 80000 pixels so the data we are hiding can only be drawn to math.floor (math.sqrt ((this.canvas.width * this.canvas.height) / 8)) which is 282 wide and height canvas (79,524) Pixels,318,096 color values), the number can only be rounded down, otherwise it will overflow, resulting in the loss of hidden data!

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

A small change in RGB component value is undiscernable by the naked eye and does not affect the image recognition. You can’t tell the difference between this plus one


More don’t say less don’t Lao ~ first look at the code

// Store steganographic resource image data in the lowest binary of the target image
drawHiddenData() {
    // Put the binary of the hidden data into an array
    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: "Encryption successful!"}); },Copy the code

As you can see, one image has been hidden in another image (we have a moon cake image inscribed into the moon image).

Parse the encrypted image

Once the image is encrypted, the next thing we need to do is parse the encrypted image

First of all, we select the local image and render it on the canvas. After obtaining the pixel data of the canvas through getImageData, we build a binary storage representation. Later, we will take out the image hidden in the target image through its lowest level

GetCanvasData () {this.targetData = this.ctx.getimageData (0, 0, this.canvas.width, this.canvas.height); / / save a binary value this. TargetData. BinaryList = Array. The from (this) targetData) data, (color, index) => { color = color.toString(2).padStart(8, "0").split(""); return color; }); console.log(this.targetData); this.loading = false; },Copy the code

Extract hidden color values from images

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

It is important to note that: The length of the first argument to putImageData must be a multiple of 4 of the product of two edges 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, because I have 800 * 800, so 2560000 values, you can write the variable canvas.width * canvas.height *4

// Parse the image
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");
    // The length of putImageData's data must be a multiple of 4 of the product of two edges
    ctx.putImageData(
        new ImageData(
            decryptImageData,
            Math.floor(Math.sqrt(2560000 / 8 / 4)),
            Math.floor(Math.sqrt(2560000 / 8 / 4))),0.0
    );
},
Copy the code

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

Wow, isn’t that interesting?

Note also: LSB steganographic images can only be stored as PNG or BMP images, and lossy compression (such as JPEG) can not be used, otherwise steganographic data will be lost!

** Do not use steganography to do illegal and criminal bad things!! ** because it is able to prevent (some wall) monitoring, such as you put the bad child map hidden in the good child map inside, I go too bad 😢

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

If you’re interested, check out StegaStamp: Invisible Hyperlinks in Physical Photographs from the University of California, Berkeley. Their project home page is here

The above is the content of steganography related to text and pictures. Of course, the medium of steganography is not limited to this. There are many kinds of steganography, as long as computers can express everything digitally, it can be used to do steganography, such as audio steganography, and video steganography

The last

Steganography is a deep, widely used knowledge, here is very general, the right as a piece of advice. Steganography of images and text is only the simplest part, but if you are interested, there is a book called Data Hiding Techniques Unmasked. If you need a partner, you can add me, and I’ll send it to you! The examples are already on my Github site, but you can also check them out here

I am rongding, very happy to be here with you to become stronger! Programming for happiness together! 😉

If you love front-end technology too! Welcome to my small circle ~ there are big guys, take you fly! 🦄 click → here