Skip to main content
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Sumit Verma and Jarrod Stiles and Marc Esher
Ben Nadel at CFUNITED 2009 (Lansdowne, VA) with: Sumit Verma Jarrod Stiles Marc Esher

Embedding Secret Messages In An Image Using ColdFusion

By on
Tags:

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:

Embedding Secret Messages In ColdFusion Images Using The Underlying Pixel Data.

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

33 Comments

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.

33 Comments

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?

33 Comments

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.

15,640 Comments

@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 :)

33 Comments

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.

15,640 Comments

@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 :)

15,640 Comments

@David,

Getting some of those finger configurations to work kind of strains my hand. Nine, for example, feels crazy awkward.

33 Comments

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 :)

17 Comments

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... (:

15,640 Comments

@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".

33 Comments

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

15,640 Comments

@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).

1 Comments

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)

15,640 Comments

@Tommy,

I am not really sure what you just said :) Sorry, a bit above my head. I remember matrices, though, from finite math.

290 Comments

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.)

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel