Introduction
There are many ways to protect sensitive data. Firewalls and network security may block a network peer. File permissions may block another system user. If someone were to get a hold of the data, encryption may help thwart attempts to make use of it. There is another less known method called Steganography: the process of hiding data inside non-secret information, with the hopes that no one will detect it’s there to begin with.
In order to prevent detection of the hidden data, one must avoid altering the overall structure of the carrier data, so that the alterations do not stand out. Oftentimes this would mean selecting a carrier that is much larger than the secret data and then scattering little pieces of the secret data throughout.
Let’s do an experiment, trying to hide the text “This is a secret password: OneTwoLeetPass” inside an image file.
The Payload: Message and Header
Before we can hide our message, we need to convert it into a simple stream of
bits. We also need a reliable way to know when the message ends. A common
mistake is to wait for a “null” character (00000000), but our secret data
could naturally contain nulls!
A much more robust method is to create a header that tells us the message length. We’ll first write the length of our message (a 32-bit integer) as a stream of 32 bits, and then follow it with the bits from the message itself.
Our total payload will be 32 bits (for the length) + (message length * 8) bits.
string message = "This is a secret password: OneTwoLeetPass";
uint32_t msg_len = message.length();
// Total bits = 32 for the length integer + 8 for each character in the message
vector<uint8_t> bits(32 + (msg_len * 8));
uint8_t MASK = 0b10000000;
uint8_t byte, bit;
int bit_idx = 0;
// First, encode the 32-bit length into the first 32 bits of our payload
for (int i = 0; i < 32; i++) {
bits[bit_idx++] = (msg_len >> (31 - i)) & 1;
}
// Next, encode the message characters one by one
for (int i = 0; i < msg_len; i++) {
byte = message.c_str()[i]; // Grab a byte of the next char in message
for (int x = 0; x < 8; x++) { // Loop over each bit in this character's byte
bit = (byte & MASK) >> 7; // Get the most significant bit
bits[bit_idx++] = bit;
byte = byte << 1; // Shift left so the next bit is most significant
}
}
A little ‘bit’ of magic
Let’s break down the character encoding part…
uint8_t MASK = 0b10000000;bit = (byte & MASK) >> 7

Here we create a bitmask so we can grab only the left-most bit of the character.
When we use the bitwise AND operator (&) on the mask and the character, the
resulting byte only has bits activated that were set in both bytes.
byte = byte << 1;
Here we take the current character in the message and we ’left shift’ it over by
one bit. So 00110010 would now be 01100100. Now when we repeat the masking
step, we are actually grabbing the next bit in the sequence.
After this, we have a vector where each element is either a 0 or 1,
representing our entire payload.
Hiding in Plain Sight
Let’s use an image file for our carrier. In this example, we will use a PNG
file. A PNG’s pixel data, when uncompressed, is a stream of bytes representing
each pixel’s color channels: red, green, blue, and sometimes alpha
(transparency). A single solid red pixel might be
11111111 00000000 00000000 11111111 in binary. Looks like a good place to hide
our message!
To hide our message, we will use a process called Least Significant Bit (LSB) encoding. The LSB is the right-most bit in a byte. Changing it only alters the byte’s total value by 1. In a busy image, changing the color value of a pixel by 1 is virtually undetectable to the human eye.
Here’s how we embed our bits. We’ll use the LodePNG library to easily access the image’s pixel data.
vector<unsigned char> encodeBits(vector<unsigned char> orgImage, string message) {
// --- This section is the same as the code block above ---
uint32_t msg_len = message.length();
vector<uint8_t> bits(32 + (msg_len * 8));
uint8_t MASK = 0b10000000;
int bit_idx = 0;
for (int i = 0; i < 32; i++) { bits[bit_idx++] = (msg_len >> (31 - i)) & 1; }
for (int i = 0; i < msg_len; i++) {
uint8_t byte = message.c_str()[i];
for (int x = 0; x < 8; x++) {
bits[bit_idx++] = (byte & MASK) >> 7;
byte = byte << 1;
}
}
// --------------------------------------------------------
vector<unsigned char> image = orgImage; // Create a new image, copy of the original
int current_bit = 0;
// Go through the image bytes and write our payload bits
for (int i = 0; i < image.size() && current_bit < bits.size(); i++) {
// Skip every 4th byte (the alpha channel)
if ((i + 1) % 4 == 0) {
continue;
}
// Clear the least significant bit
image[i] = image[i] & 0b11111110;
// Set the least significant bit using our payload bit
image[i] = image[i] | bits[current_bit];
current_bit++;
}
return image;
}

image[i] = image[i] & 0b11111110;
This line clears the right-most bit. The mask 11111110 ensures all other bits
keep their original value, but the last bit will always become 0.
image[i] = image[i] | bits[current_bit];
Then, we use the bitwise OR operator (|). Since the LSB of image[i] is 0,
the result of the OR operation is determined entirely by our secret bit. If our
secret bit is 1, the LSB becomes 1. If it’s 0, it stays 0. We also skip
every fourth byte to avoid modifying the alpha channel, as changes there can
sometimes be more noticeable.
Getting the Data Back
Now that we’ve hidden the message, let’s write a function to retrieve it. We just have to reverse the process.
- Read the first 32 LSBs to figure out how long the message is.
- Read that many more bits to reconstruct the message.
string decodeBits(vector<unsigned char> image) {
uint32_t msg_len = 0;
uint8_t byte = 0;
string message = "";
vector<uint8_t> bits;
// Extract all the LSBs from the image data (skipping alpha)
for (int i = 0; i < image.size(); i++) {
if ((i + 1) % 4 == 0) continue;
bits.push_back(image[i] & 0b00000001);
}
// 1. Rebuild the 32-bit length header
for (int i = 0; i < 32; i++) {
msg_len = (msg_len << 1) | bits[i];
}
// 2. Rebuild the message characters using the decoded length
for (int i = 32; i < (32 + (msg_len * 8)); i++) {
byte = (byte << 1) | bits[i];
// If we have 8 bits, save the character and reset the byte
if ((i + 1) % 8 == 0) {
message.push_back(byte);
byte = 0;
}
}
return message;
}
Let’s look at how a character is rebuilt: byte = (byte << 1) | bits[i];
We take the byte we’re currently building, shift it left to make room for the
next bit, and then use OR to place the next bit from our bits vector into that
empty spot. Rebuilding the character ‘T’ (01010100) would look like this:
0000000000000001000000100000010100001010000101010010101001010100
Once we have 8 bits, we add the completed character to our message and start the next one.
Closing Thoughts
This was fun! It could be improved further. We could add a “magic number” (a unique sequence of bytes) to the start of our header to quickly identify if an image even contains our hidden data. We could also scatter the data more randomly throughout the image instead of sequentially to make it even harder to detect.
What other kinds of carrier data can you think of to hide things in? Audio files? Plain text documents? The possibilities are fascinating.