Embedding Secret Messages In An Image Using ColdFusion
After pixelating a ColdFusion image using the underlying BufferedImage Java object, I wanted to see what other kinds of fun I could have playing with RGB values. One thing I thought would be kind of cool would be to embed character data in the image data. Each character can be translated into a numeric ASCII value; as such, I figured I could use that numeric value equivalent to fudge the RGB value of a given pixel.
My first approach to this problem was to embed a single character in each pixel, distributing the ASCII value across the three color channels - Red, Green, and Blue. The problem with this approach, though, is that I wanted to support up to the ASCII value 255 (the standard set), which can drastically shift the RGB value of a given pixel (not to mention it was very hard to figure out how much of that ASCII value to put in each color channel).
My second approach - the one I finally went with - was to take the ASCII concept one step further and convert each ASCII value to its bit value equivalent (ASCII in base2). Then, rather than trying to stuff a single character into a single pixel, I would only stuff a single bit into a single pixel. This meant I needed 8 pixels to store a single character (8 bits to a character); but, a single bit could easily be stored in a single pixel without any visual distortion.
As an example, let's say I have the letter, "H". The ASCII value of this is 72. 72 in base2 (bit version) is "01001000". Breaking that bit string into individual numbers, I then needed 8 pixels, each of which would be altered by one, or left alone (if given bit is zero).
Once the pixels of a given image are altered based on the given character data, there is no way to extract the character data from the mutated picture alone. Both the person embedding the text and the person extracting the text would need to have a copy of the original image. Then, and only then, can the embedded character data be extracted by comparing the two images (the original and the mutated), creating bit strings based on the difference. These bit strings could then be converted back into ASCII and, subsequently, back into printable characters.
Before we look at the ColdFusion component that does this, ImageMessage.cfc, let's take a look at a demo to see how it might be used:
<!--- Create an instance of the image message component. --->
<cfset embedder = createObject( "component", "ImageMessage" ).init() />
<!---
Read in the image that will be used as our key - this
is the shared image that each user would have to have
to extract hidden messages.
--->
<cfimage
name="keyImage"
action="read"
source="./athlete.jpg"
/>
<!--- Create a TOP SECRET message to embed. --->
<cfsavecontent variable="message">
Katie,
I want to throw a surprise birthday party for Sarah, but
it is essential that she does not find out about it! Not
only is it her birthday, but I think I am going to propose
this year.
Speaking of proposal, I know nothing about picking out a
ring. Do you think we could meet this weekend and you can
help me out? We can head up to the diamond district to
see what's good.
Also, think of some great places to take her out to dinner.
I'm just a programmer - I don't know anything about that
kind of stuff - my idea of a good time is a Law and Order
rerun and take-out Chinese.
Thanks!
You're the best!
</cfsavecontent>
<!---
Embed the top secret message in a new image, which will
be a dupliate of the Key image.
--->
<cfset messageImage = embedder.embedMessage(
keyImage,
trim( message )
) />
<!---
Now that we have our images, let's output them to see if
there are any striking visual differences.
--->
<cfoutput>
<div style="width: 500px">
<h3>
KEY Image
</h3>
<p>
<!--- Key image. --->
<cfimage
action="writetobrowser"
source="#keyImage#"
/>
</p>
<h3>
EMBEDDED MESSAGE Image
</h3>
<p>
<!--- Message image. --->
<cfimage
action="writetobrowser"
source="#messageImage#"
/>
</p>
<!---
Now, let's extract the message from our message image
and output it to the screen.
--->
<h3>
EXTRACTED Message (from Message Image)
</h3>
<p>
<!---
Notice that when I extract the message, I need
the original KEY image as well as the image that
contains the message.
--->
#replace(
embedder.extractMessage(
keyImage,
messageImage
),
chr( 13 ),
"<br />",
"all"
)#
</p>
</div>
</cfoutput>
As you can see in the above code, we are embedding text in an image, then outputting the two images (the original and the mutated) to see if there are any visual differences. Then, using the original image, we extract the message from the mutated image and display the message on the screen. And, in fact, when we run the above code, we get the following page output:
As you can see, the two images look exactly alike; and, without any visual distortion, the embedded message was easily extracted.
Ok, now that you see how this works, here is the ColdFusion component that provided the embedding and extracting algorithms:
ImageMessage.cfc
<cfcomponent
output="false"
hint="I provide functionality to embed and extract messages within a given image based on a shared key image.">
<cffunction
name="init"
access="public"
returntype="any"
output="false"
hint="I return an intialized component.">
<!--- Return this object reference. --->
<cfreturn this />
</cffunction>
<cffunction
name="copyImage"
access="public"
returntype="any"
output="false"
hint="I duplicate the given image.">
<!--- Define arguments. --->
<cfargument
name="image"
type="any"
required="true"
hint="I am the image being duplicated."
/>
<!--- Copy the image and return it. --->
<cfreturn imageCopy(
arguments.image,
0,
0,
imageGetWidth( arguments.image ),
imageGetHeight( arguments.image )
) />
</cffunction>
<cffunction
name="embedMessage"
access="public"
returntype="any"
output="false"
hint="I embed the given message in a duplicate of the give shared image key and return the message image.">
<!--- Define arguments. --->
<cfargument
name="keyImage"
type="any"
required="true"
hint="I am the image key (the shared image) that will be used to create the message image."
/>
<cfargument
name="message"
type="string"
required="true"
hint="I am the message being embedded in the image key (a duplicate of the image key)."
/>
<!--- Define the local scope. --->
<cfset var local = {} />
<!---
Duplicate the image key (since we don't want to alter
the key itself but rather work based off of it).
--->
<cfset local.image = this.copyImage( arguments.keyImage ) />
<!--- Get the width and height of the image. --->
<cfset local.imageWidth = imageGetWidth( local.image ) />
<cfset local.imageHeight = imageGetHeight( local.image ) />
<!---
Check to see if this image is large enough to hold the
given message. Each character will require 8 pixels -
we are going to store one ASCII BIT per pixel (we need
8 bits to hold one character).
--->
<cfif (
(len( arguments.message ) * 8) gt
(local.imageWidth * local.imageHeight)
)>
<!--- Throw message length error. --->
<cfthrow
type="MessageLength"
message="The message you are trying to embed is too long for this image."
detail="The message you are trying to embed is of length [#len( arguments.message )#] and the given image key can only accept messages of max length [#fix( local.imageWidth * local.imageHeight / 8 )#]."
/>
</cfif>
<!---
Get the underlying buffered image. This will give
use access to all of the underlying pixel data for our
"message" image.
--->
<cfset local.bufferedImage = imageGetBufferedImage(
local.image
) />
<!---
Figure out the number of pixel rows that will be
required to embed the message.
--->
<cfset local.requiredRows = ceiling(
len( arguments.message ) * 8 / local.imageWidth
) />
<!---
Get enough RGB pixels to embed the message based on
the given rows.
--->
<cfset local.pixelBuffer = local.bufferedImage.getRGB(
javaCast( "int", 0 ),
javaCast( "int", 0 ),
javaCast( "int", local.imageWidth ),
javaCast( "int", local.requiredRows ),
javaCast( "null", "" ),
javaCast( "int", 0 ),
javaCast( "int", local.imageWidth )
) />
<!---
Now that we have our pixel buffer, we need to create
our own into which we can store the "message" pixels.
NOTE: We can convert this back to a Java array when
we overwrite the pixel buffer.
--->
<cfset local.messagePixelBuffer = [] />
<!---
Resize to be the size of the original pixel buffer -
this will be faster to resize upfront than to later
set values by appending them as needed.
--->
<cfset arrayResize(
local.messagePixelBuffer,
arrayLen( local.pixelBuffer )
) />
<!---
Now, let's split the message up into an array so
that we can easily access each of the characters.
--->
<cfset local.characters = reMatch(
"[\w\W]",
arguments.message
) />
<!---
We are going to loop over the characters to embed
them in the pixel colors. We will need 8 pixles (one
per bit) to embed a single ascii character. Therefore
we need to keep a seperate index for the pixel buffer
than we do for the characters.
--->
<cfset local.pixelIndex = 1 />
<!--- Loop over the characters. --->
<cfloop
index="local.character"
array="#local.characters#">
<!--- Convert the character to it's ASCII value. --->
<cfset local.characterAscii = asc( local.character ) />
<!---
Make sure the ASCII value is not greater than 255.
If it is, we are going to replace it with the "?"
mark to indicate that this is beyond the
capabilities of the embedding.
--->
<cfif (local.characterAscii gt 255)>
<!--- Overwrite with "?" ascii. --->
<cfset local.characterAscii = 63 />
</cfif>
<!--- Convert the charater to a array of bits. --->
<cfset local.characaterBits = reMatch(
".",
formatBaseN( local.characterAscii, 2 )
) />
<!--- Make sure the bit array is 8 bits. --->
<cfloop condition="arrayLen( local.characaterBits ) lt 8">
<!--- Prepend a zero. --->
<cfset arrayPrepend(
local.characaterBits,
"0"
) />
</cfloop>
<!---
Loop over the array bit array and use each bit to
update the pixel color.
--->
<cfloop
index="local.characterBit"
array="#local.characaterBits#">
<!---
Check to see if the current pixel value is
positive or negative. To be on the safe side,
we are goint to move towards zero.
--->
<cfif local.pixelBuffer[ local.pixelIndex ]>
<!--- Positive value, so subtract. --->
<cfset local.messagePixelBuffer[ local.pixelIndex ] = (local.pixelBuffer[ local.pixelIndex ] - local.characterBit) />
<cfelse>
<!--- Negative value, so add. --->
<cfset local.messagePixelBuffer[ local.pixelIndex ] = (local.pixelBuffer[ local.pixelIndex ] + local.characterBit) />
</cfif>
<!--- Increment the pixel index. --->
<cfset local.pixelIndex++ />
</cfloop>
</cfloop>
<!---
Now that we have copied over the characters, copy
over the rest of the pixels (which aren't being used
to embed character data).
--->
<cfloop
index="local.pixelIndex"
from="#local.pixelIndex#"
to="#arrayLen( local.pixelBuffer )#"
step="1">
<!--- Copy existing color. --->
<cfset local.messagePixelBuffer[ local.pixelIndex ] = local.pixelBuffer[ local.pixelIndex ] />
</cfloop>
<!---
Now, let's write the pixel buffer BACK into the
message image.
--->
<cfset local.bufferedImage.setRGB(
javaCast( "int", 0 ),
javaCast( "int", 0 ),
javaCast( "int", local.imageWidth ),
javaCast( "int", local.requiredRows ),
javaCast( "int[]", local.messagePixelBuffer ),
javaCast( "int", 0 ),
javaCast( "int", local.imageWidth )
) />
<!--- Return the message image. --->
<cfreturn local.image />
</cffunction>
<cffunction
name="extractMessage"
access="public"
returntype="string"
output="false"
hint="I extract an embedded message from the given image based on the shared key image.">
<!--- Define arguments. --->
<cfargument
name="keyImage"
type="any"
required="true"
hint="I am the image key (the shared image) that will be used to extract the embedded message."
/>
<cfargument
name="messageImage"
type="any"
required="true"
hint="I am the image with the embedded image."
/>
<!--- Define the local scope. --->
<cfset var local = {} />
<!--- Get the key image dimentions. --->
<cfset local.keyImageWidth = imageGetWidth( arguments.keyImage ) />
<cfset local.keyImageHeight = imageGetHeight( arguments.keyImage ) />
<!--- Get the message image dimentions. --->
<cfset local.messageImageWidth = imageGetWidth( arguments.messageImage ) />
<cfset local.messageImageHeight = imageGetHeight( arguments.messageImage ) />
<!---
Check to make sure the images have the same dimensions.
If not, then correct key has not been supplied (not to
mention we might get OutOfBounds errors when trying to
extract the message).
--->
<cfif !(
(local.keyImageWidth eq local.messageImageWidth) &&
(local.keyImageHeight eq local.messageImageHeight)
)>
<cfthrow
type="IncorrectKeyImage"
message="The key image you provided does not have the same dimensions as your message image."
/>
</cfif>
<!---
Get the pixels from the key image. Because we don't
know how large the message is, we'll just get all of
the pixels.
--->
<cfset local.keyPixels = imageGetBufferedImage(
arguments.keyImage
)
.getRGB(
javaCast( "int", 0 ),
javaCast( "int", 0 ),
javaCast( "int", local.keyImageWidth ),
javaCast( "int", local.keyImageHeight ),
javaCast( "null", "" ),
javaCast( "int", 0 ),
javaCast( "int", local.keyImageWidth )
)
/>
<!---
Get the pixels from the message image. Because we
don't know how loarge the message is, we'll just get
all of the pixels.
--->
<cfset local.messagePixels = imageGetBufferedImage(
arguments.messageImage
)
.getRGB(
javaCast( "int", 0 ),
javaCast( "int", 0 ),
javaCast( "int", local.messageImageWidth ),
javaCast( "int", local.messageImageHeight ),
javaCast( "null", "" ),
javaCast( "int", 0 ),
javaCast( "int", local.messageImageWidth )
)
/>
<!---
Create a message character array. As we find each
character, we will append it to this array (to later
be turned into a full string).
--->
<cfset local.characters = [] />
<!---
Create a bit array to hold the differnce between
each pixel.
--->
<cfset local.characterBits = [] />
<!---
Loop over the pixel buffer to start adding pixel
differences to the bit array.
--->
<cfloop
index="local.pixelIndex"
from="1"
to="#arrayLen( local.messagePixels )#"
step="1">
<!---
Append the difference the character bits. Be sure
to use the absolute values since the alpha channel
can give us odds values.
--->
<cfset arrayAppend(
local.characterBits,
abs(
abs( local.keyPixels[ local.pixelIndex ] ) -
abs( local.messagePixels[ local.pixelIndex ] )
)
) />
<!---
Check to see if our bit array is of lenth 8. If
it is, then we have collected an entire character
which we can then convert to CHR and append to the
message array.
--->
<cfif (arrayLen( local.characterBits ) eq 8)>
<!--- Convert to ASCII value. --->
<cfset local.characterAscii = inputBaseN(
arrayToList( local.characterBits, "" ),
2
) />
<!---
Check to see if the ASCII value is zero. This
would result if there was no difference
between the two pixels; this will signal the
end of the string.
--->
<cfif !local.characterAscii>
<!--- We are done. Break out of loop! --->
<cfbreak />
<!--- ------------------------------- --->
<!--- ------------------------------- --->
</cfif>
<!--- ASSERT: We are still matching. --->
<!---
Append character to building message character
array. When we do this, we have to convert the
ASCII value to a printable character.
--->
<cfset arrayAppend(
local.characters,
chr( local.characterAscii )
) />
<!---
Reset the bit array (so that we can start
building the next character).
--->
<cfset local.characterBits = [] />
</cfif>
</cfloop>
<!---
Join the character array as a single string and
return it.
--->
<cfreturn arrayToList( local.characters, "" ) />
</cffunction>
</cfcomponent>
I know that if you are already relying on shared, private keys, there are far more efficient ways to send encrypted messages (such as with straight up text values); but, I just thought this was a lot of fun. Plus, you could have a large set of shared keys that would have to be matched up visually, rather than just using the same key for each value. And of course, an image is much less conspicuous than a chunk of garbled text.
Anyway, I just thought this was a really fun way to end the week.
Want to use code from this post? Check out the license.
Reader Comments
You know what I'm going to say... ;)
@Raymond,
Oh snap - I didn't even think of it for this. But yeah, I guess that makes sense. This weekend :)
Steganography rules! :)
Quite frankly you could encrypt the message with almost any encryption algorithm, then embed the encrypted data in the image(if the result isn't too large), which would give you the security of the encryption used with the inconspicuousness of an image. If the message data bit length is too long you could always encode the data 1 bit per channel which would be 3 bits per pixel, 4 bits per pixel if the format supports alpha channel.
Very creative and _very_ cool Ben!
Wait a second ... you are proposing to someone?! :)
I had to try it, so I modified your code to encode 1 bit per color channel(3 bits per pixel) and to encrypt the data before embedding.
I am using bit shifting and XOR to embed and extract the data bits.
I did have an error with your original code and the image I borrowed of you and Laura Arguello:
"
Invalid argument for function InputBaseN.
The argument 1 of InputBaseN which is now 000016777215101 must be a valid number in base 2.
"
.
16777215=11111111 11111111 11111111=white
I think it might be an integer rollover issue
http://www.drm31415.com/embed.txt
http://www.drm31415.com/ImageMessage.txt
Is it just me or does the image with embedded data seem lighter in both examples?
Now for even more fun!
I updated yet again to instead of embedding ASCII text embed another picture
http://www.drm31415.com/embed2.txt
http://www.drm31415.com/ImageMessage2.txt
I have a pair of images, see if you can find whats embedded
http://www.drm31415.com/laura_arguello.jpg < Key
http://www.drm31415.com/secret.png < Hidden Image
The embedded is lead by an embedded 32bit int storing the width in the higher 16 bits and the height in the lower 16 bits.
Because a pixel is stored as an 32bit int with 3 channels( I don't believe the upper 8bits are usable because RGB doesn't use it so (get/set)RGB() may not retrieve/set it ) to store a 80x80 image it takes a image with ceiling(((80*80)+1)*32/3)=68278 pixels to store it which is just over 10 times the size, the +1 is to store the dimensions
This technique could be adjusted to store virtually any type of binary data, as long as you had enough room in the key image or used multiple result images and split the embedded data.
The storage could be doubled by using 3(00000011 in binary) as MessageBitMask and Shifting in 2's but it would increase the visual impact in the result.
The embedded image could also be encrypted before embedding like the 1st example.
PS: I can take down the laura_arguello.jpg whenever you want, just wanted to use a recognizable image.
@David,
That is very cool! I see you hit my Gravatar thumbnail :)
Using the bit manipulation is definitely clever (using one bit per channel), but it certainly ups the complexity of the algorithm a lot! I had a bit of trouble following it (bit manipulation is one thing I don't visualize very well), but I ran it and it works well!
Very cool experimentation :)
@Erika,
Nah, just needed a top-secret message to demo ;)
I could have commented it better, yours simply adds the the lowest bit which is presumable the lowest bit in the blue channel.
I pluck out the bits out of the int/ascii by shifting it so the bit I want is the rightmost/lowest order then Bitwise anding it with 1(binary 00000001) so only the rightmost bit is reflected in the result then I store it in an int with the lower 3 bytes corresponding to the 3 color channels.
I use an int to store the red bit, shift it 8 bits to the left, store the green bit, shift it 8 bits to the left, store the blue bit, store int in array, clear int to 0, start over, the resulting int is all zeros except for the lowest bit in the final 3 bytes.
Then it Xors the ints together with the corresponding pixel ints.
Bitwise XOR is cool because:
If IntA is a Key Int and IntB is a Data Int and IntC is the XOR result of IntA and IntB then the XOR result of IntA and IntC is IntB, so with the Key and Result it is easy to retrieve the embedded data.
@David,
Bit manipulation is something that I understand at the conceptual level, but it's just hard for me to think that way since I rarely use bits. In fact, when I first started the approach to store each letter inside a single pixel, I got to playing with the BitSHLN() and BitSHRN() methods. But, that said, it definitely trips me up to think in bits :)
I think I may have an affinity for other numbering systems, I have liked bit based operations for a while. If you want to see something trippy you can learn how to count to 1023 on your fingers using binary: http://en.wikipedia.org/wiki/Finger_binary
@David,
Getting some of those finger configurations to work kind of strains my hand. Nine, for example, feels crazy awkward.
20(10100) seems the most difficult to me, btw avoid 5(00101) or 640(1010000000) or even worse 645.
Today is a binary date btw, 01/10/10, yes I am using a 2-digit year, sue me :)
Ben, As I was sitting here reviewing your code I found myself wondering how to do something similar. I imagined doing something along these lines: As you type each letter it is simply replaced with a single pixel of color. So if you type the letter 'a' it would map to a blue pixel (or perhaps a green 2x2 pixel.) The output wouldn't necessarily make sense but it could turn out pretty with a few paragraphs... (:
@David,
Nothing wrong with a 2-year date, when done for a cool purpose ;)
@Kristopher,
It's funny you mention that because I used to wonder what "music" would look like as an image, much in the same way - it's all just "data".
I modified the code to make 'pictures' from binary data then re-extract it, this was a learning experience because to rebuild the bytearray I had to extract the separate bytes and convert them to signed byte values to make java happy. It can be VERY memory intensive depending on what you use so be careful, It can't use files more than approx 16MB(just under).
I noticed on the few mp3s I used the resulting pics had stripes.
http://www.drm31415.com/embed3.txt
http://www.drm31415.com/ImageMessage3.txt
@David,
What kind of image does this create? Just something with completely random looking colors?
There's got to be a easier way to work with bits. I'm not sure what I don't like about it yet, but just there's got to be a nicer "wrapper" for this kind of functionality.
I'm gonna put by bit-thinking cap on (which is really small at this time).
Speckly images, although some interesting patterns developed in the mp3 ones, may try on a small mpg or avi to see if it is the same.
I used to work at the UCF Vision lab... My job was to convert C++ and MatLab code into OpenGL/GLSL code so it could be processed on the GPU (Graphical processing unit). The only way to store the information was in matrices (which were image textures). This is very useful stuff... to someone
(The algorithms we ran on the GPU were 1000x faster than when ran on CPU)
@Tommy,
I am not really sure what you just said :) Sorry, a bit above my head. I remember matrices, though, from finite math.
can anyone tell me how can i insert data in image bits in matlab,plz help me!
I saw this piece of news online. This news is about how the alleged Russian spies hid messages in images in a way that is practically the same as what you described in your blog entry (except that I am not sure if they used ColdFusion to do it though. :)
http://blogs.discovermagazine.com/80beats/2010/07/01/how-the-russian-spies-hid-secret-messages-in-public-online-pictures/
Save Anna Chapman! Save Anna Chapman! Save Anna Chapman!
What a babe! It would be such a shame for her to spend the next 20 years in prison. That punishes **US** not the Russians!!
(Like I had a shot.)