This page is a presentation of Steganosaurus. This explains its principle.
The document starts from a very simple explanation and gets harder when you go deeper.
Steganosaurus is a simple tool that hides/encodes data (a secret message) in an image.
Basically, you provide an image, and a message, and you get back an image that looks identically than the first one (eyes can't see the difference, but a very small difference exists). Now the secret message is embedded in the image. Magic!
But how can we hide a message into an image ?
This tool uses Least Significant Bit method to store data into pixels.
First, let's understand what is an image.
We are talking about bitmap images, not vector graphics. Bitmap images are images that are represented by a rectangular grid of pixels (array of pixels).
An image is then a combination of pixels.
Okay. Now, do you remember painting lessons, when you were young?
We can get all colors by a specific combination of RED, GREEN and BLUE, the additive primary colors. This is known as the RGB color model.
Note: The intersections of the three primary colors are YELLOW, CYAN and MAGENTA.
There are two types of color mixing: Additive and Subtractive. In both cases, there are three primary colors, three secondary colors (colors made from 2 of the three primary colors in equal amounts), and one tertiary color made from all three primary colors. - Color mixing on Wikipedia
In our case, RGB is an additive color model, when we add all primary colors together, we get WHITE. When painting in real life, it is a subtractive color model, mixing all colors yields BLACK.
In an image, each pixel is saved as a combination of three components: Red, Green and Blue value. And each value is set between 0 (0%) and 255 (100%).
Thanks to this, we can represent 256*256*256 = 16777216 (16,8 millions) different colors in each pixel.
As said before, a value (red, green or blue) is set between 0 and 255. In computer science, each value is coded with 8 bits (1 byte/octet). Because with 8 binary bits, we can represent a decimal value from 0 to 255.
The « least significant bit » (LSB) is then the last bit (rightmost bit), the bit with the lowest weight.
10010101(2) = 149(10)
If we change the LSB, we only subtract or add 1 to the initial number. In the figure above, if we change the LSB to 0, we'll get :
10010100(2) = 148(10)
The binary representation is showed to understand the « LSB » principle. Typically, LSB method means that we'll change the last bit of a number to turn it into an even or odd number.
Okay but, why is this useful to hide a message into an image ?
Look at these two pixels :
Your eyes can't see the difference, but the first one has values R(40),G(150),B(250) and the second has values R(41),G(150),B(251).
A message is a group of characters (letters from alphabet for example) that are also represented as numbers, and in computer science, as binary data. This means we can store binary data by using the LSB of each RGB value of each pixel. It will not alter that much the image (the difference won't be visible), and we just have to split our data into single bits and place them in LSB of pixels.
A lot of « translation tables » exist, they are used to translate a number into a printable character and conversely. There is the standard ASCII table and a lot of characters sets (ISO-8859-1, UTF8...) used by different countries and alphabets.
Let's see an example.
I want to store the text "AB" into these pixels:
This is a 1x10 px sized image with white pixels.
Now we have used the ASCII table and found the following information:
This is very simple, we just have to store "01000001 01000010" in the image. Each bit can be saved in the LSB of the RGB values.
As you can see, the RGB values will just vary between 254 and 255, so the difference will be invisible.
If we have an image with a size of 50x50 px, it contains 50*50=2500 pixels, and can then store 2500*3=7500 bits (because each pixel has 3 available LSB). It corresponds to 7500/8=937 bytes and can then store 937 characters.
But, this is theory.
We'll see that Steganosaurus uses 16 bits to encode each character (UTF8), because some people may want to save a message with accents (éàî..) or special characters (€$£¥..).
Furthermore, we need something to surround our message. In the other case, how can we know when the message ends (because we will certainly not use all the allowed space) ? For this purpose, Steganosaurus adds a header and a footer to the message before encoding it into the image.
As Steganosaurus is a web-program and runs in the web browser, it needs to use a simple and wide supported image format to work properly.
Let's compare the mainly used image formats on internet.
(Joint Photographic Experts Group)
|Large images (up to 65535*65535)
Good compression (lightweight files)
|Bad quality (compression alters pixels)|
|Perfect quality (no compression)
Up to 16,8 millions of colors (24 bits)
|Not used on internet
Very heavy files (no compression)
(Graphics Interchange Format)
|256 colors maximum
No alpha channel for transparency
(Portable Network Graphics)
|Perfect quality (lossless compression)
Transparency (alpha channel)
Up to 281474 billions of colors (48 bits)
Steganosaurus acts on PNG images, because this is the format that best fits our needs (best quality).
For example it is not possible to use JPEG format on Steganosaurus, because when exporting the final "transformed image", some pixels could have a slightly different value of RGB, due to the compression, and even if we export with a quality of 100%.
A QUALITY SETTING OF 100 DOES NOT DEGRADE A JPEG IMAGE AT ALL: FALSE
Saving an image to JPEG format always introduces some loss in quality, although the loss at a quality setting of 100 is barely detectable by the average naked eye. - JPEG Myths and Facts
And the winner is... PNG !
We've seen before that a pixel is coded with 24 bits : 8 bits for Red value, 8 bits for Green value and 8 bits for Blue. The PNG format introduces a 4th value : the alpha channel. Each pixel uses then 32 bits and we define RGBA values.
PNG is able to save images in different formats : from 1 bit to 48 bits. But here we're talking about 32 bits images.
The alpha value is a special field that sets the opacity of the pixel. It is like the transparency of GIF images for example, but this time a pixel is not fully transparent or fully opaque, it has a percentage of opacity (from 0 to 255)... This property is often used on internet and web pages, as this allows us to display beautiful icons that fit on every background.
In this image, we have some transparent pixels (gray mosaic), and also some pixels that are « half transparent » (we can see the gray mosaic behind them). Let's see an example of a pixel defined with RGBA.
A(50): A(100): A(150): A(255):
An idea could then be to store 4 bits per pixel, as we have 4 channels in each pixel. We could by this way achieve to store 1 octet in just 2 pixels!
This method could permit to store 33% more data in the image.
I've done tests, but there is a problem when exporting the final PNG: pixels with an alpha value different than 255 will get altered values in RGB fields. Some people seem to have to same issue (thread on StackOverflow).
It is not reliable to change the alpha value on the browser (but it could be a good idea if Steganosaurus was a program written in Python or C). So finally, when saving the data into pixels, we'll just skip the alpha channel :
The table above is the true representation of what Steganosaurus does.
Now something should catch your attention.
Okay, we don't change the value of the alpha channel when we encode the message in the pixels. But what if the user wants to use an image that contains non-opaque pixels ? He can submit an image with any value of opacity because we accept the PNG format, but we'll have the same issue as before when exporting the image.
My choice has been to put a white background behind the image, anyway, even if the image does contain only opaque pixels. By this way, all the pixels get an alpha value of 255 and we don't have further problems.
Because we need to detect that there is a hidden message in the image, and to know the exact length of the hidden message when time's come to extract it, it is necessary to "surround" the message by specific data. RSTG is the name I've given to the format of data encoded into the image.
Steganosaurus' algorithm adds a signature to the message, a header, and a footer. If all of these parts are well encoded, then the message can be retrieved.
Signature (8 bytes)
Like files' signature, the purpose of this is to detect the presence of a hidden message when reading the first bytes.
Header (4 bytes)
The header contains useful information about the message.
The first 2 bytes contain the message length (value vary from 0 to 65535) in term of characters (not bytes, because each character uses 2 bytes).
The 3rd byte is a « settings byte ». For now, it's only used to store one information : is the message encrypted or is it plaintext ? Only the leftmost bit is used, that's why the value is 0 or 128. But we could imagine other settings like : are characters coded on 1 byte or 2 bytes ? (with that we could reduce the character set and allow user to store a longer text). Or : is the data a hidden message or a file ? (yes, we could hide a complete file)
Message data (0→131070 bytes)
Then comes the message (encrypted or not). Each character uses 2 bytes (UTF8).
Footer (2 bytes)
Footer's only used to check the validity of the RSTG format, to ensure we have a correct structure.
Let's take an example.
We still want to store the secret message "AB" (as plaintext) in an image. Remember this :
Below is the full binary data that will be stored into the image :
11111111 00100001 00100011 01010010 01010011 01010100 01000111 00100011 00000000 00000010 00000000 00100011 01000001 01000010 00100011 00000011
Note that it is also possible to save a message in an image that already contains one. The new message will erase the old one.
As only the LSB change, you can save as many successive messages as you want in the image, you'll theoretically not lose more quality.
Be careful! When saving a new message into an image that already contains a message : if the new message is shorter than the old one, the end of the old message will still be encoded in certain pixels, and will remain readable if it was not encrypted.
Steganosaurus allows you to choose a key to encrypt your message.
This is a symetric encryption mechanism and a very basic method that is used for now.
The message is simply XORED by the key, char by char, and there is no minimum length for the key. This method is then unsecure. The key could be retrieved with a bruteforce attack, helped by social engineering and/or dictionary lookup, or by a known-plaintext attack.
Example : I want to encrypt my message "Message very very very secret" with my key "53cr3t-K3y".
Message very very very secret
Now I want to get back the original message.
Message very very very secret
This is not secure because a small change in the key produces just a small change in the cipher text. However, the aim is to provide a simple way to encrypt data for users, not to force them to use a 256 bits fully-random key.
This tool is designed to provide you the optimal privacy experience. That means, when using Steganosaurus, your browser won't download external scripts (that could intercept your message if compromised), there is no upload of your file(s) and no other server communication. Furthermore, there is no tracker on the page and nothing is saved in browser storage (except logs preferences).
When using this tool, once all the resources are loaded, you can close your internet connection and everything will stay in local.
A program in Python or C, or even Node.js would be more effective, but this is not the immediate objective.
Reading the image
First we need to use the File API to load the file in the browser and get its content. A FileReader() object is necessary and I use its readAsArrayBuffer(file) method. We then get a bytes representation of the file.
Then, Steganosaurus uses the HTML5 Canvas element to load the image (draw the image on the canvas), set a white background (to help opacity management), and get the pixels. Why using canvas ? Because it provides a powerful method to directly get an array of RGBA values of all pixels of the canvas (ctx.getImageData(x,y,width,height)).
The script reads the LSB of each pixel while their values match the signature, header, and footer of RSTG format. If everything matches, it extracts the message, and displays it or asks for a decryption key. If not, the user can hide a message in the image.
Writing the message in the image
First the message and the key are cleaned. If a character in the message has a char code higher than 65535, then it is replaced by the "replacement character" (char code 65533) (�). In the key, the character is just removed. Why this cleaning process? Because we need all the characters to be encoded in 16 bits to fit our model.
Then, the message and the key are converted into arrays of 16bits integers (Uint16Array). And the message is xored by the key.
After the binary data is saved in the pixels, the new pixels (pixels that now contain RSTG data in their LSB) are loaded into the canvas with ctx.putImageData(imageData, 0, 0) and the final image is exported by using the canvas.toDataURL('image/png') method.
Feel free to read the source code.↑ Top ↑