Steganography Steganography is a complex subject, please note that although it involves hiding messages, it is not cryptography. The definitions are as follows: Cryptography: “The discipline which embodies principles, means and methods for the transformation of data in order to hide its information content, prevent its undetected modification, or prevent its unauthorized use” Steganography: “A method of hiding a secret message inside of other data.” Essentially the difference is that, while both hide a message, steganography is meant to make the message invisible, while cryptography changes the message’s form, by means of replacement and/or algorithm. This code is written in Java, and the following topics will need to be understood, before properly understanding how this method works: Bytes: individually as integers and as arrays Bit Operations: Logical AND (&), OR(|) and how they work Images: BufferedImage specifically ImageIO: how image files are opened and saved Graphics2D: accessing user space image properties Raster: specifically WritableRaster allows access to the buffer DataBufferByte: Buffer used with BufferedImage *These are the major topics needed to understand Steganography, but there are others used and assumed to be understood, as this topic is not meant for those inexperienced with the Java language. Bytes: Bytes are the elementary data source of most applications, and many programmers will [i]never[/i] use them in any source code, but that is beside the point. A byte is made of bits, 1s and 0s, 8 of them to be exact. And the 8 0s and 1s have a decimal value, it is simply a case of transforming the binary (base 2) into decimal (base 10). Value by position: 128 64 32 16 8 4 2 1 (and all positions with a 1 are added together) Examples: 00000000 = 0 00000010 = 2 00000111 = 7 00001011 = 11 And so on… A byte can be transformed from an int in java by simple casting: [il]Byte b = (byte)7;[/il] Most classes in java have a method for returning the byte[] of an object, either as a section of the object or the entire object. String Class Example: String w = “William”; Byte[] b = w.getBytes(); Where b[0] will now contain the ascii value for ‘W’ 87 if printed. Though it is good to remember that although it appears as an int, when displayed, it is in fact a byte, which is stored as 8 bits, in this case: 01010111. Bit Operations: There are simple operations which most computer users have either heard of, or even used: AND: The AND(&) bit operator, will AND 2 bytes together. The same rules apply as when using true and false values, where 1 = true, and 0 = false. If both bytes have a 1 in the same position, then the result for that position is a 1, otherwise the result is a 0. Example: 01010111 = 87 01100101 = 101 01000101 = 69 [il]Byte b = 87 & 101; //69: 01000101[/il] OR: The OR(|) bit operator, will OR 2 bytes together. The same rules as with AND where 1 = true, and 0 = false, only when using OR, as long as one of the bits in the position is a 1, then the result is a 1. Only if both bits are 0, is the result a 0. Example: 01010111 = 87 01100101 = 101 01110111 = 119 [il]Byte b = 87 | 101; //119: 01110111[/il] On top of these basic operations, we can also shift bits: Left Shift: An important thing to remember when left shifting bits, is if the first bit is not a 1, a single left shift will essentially double the value. What actually happens, is a 0 is added on the right hand side of the bits, then the far left bit is removed thus leaving a new set of 8 bits. Also, when shifting in Java, a number of positions to shift must also be supplied. If the value is greater than 1, the process is simply repeated that many times each time beginning with the result of the previous shift. Thus any value will become 0 if shifted 8 times. Examples: (single shift) 01010111 = 87 << 1 10101110 = 174 (double shift) 01010111 = 87 << 2 01011100 = 92 Byte b1 = 87 << 1; //174: 10101110 Byte b2 = 87 << 2; //92: 01011100 Right Shift: A right shift is the opposite of a left shift in the sense that a 0 is added to the left side of the bits, and the far right bit is removed, once again leaving a set of 8 bits. Examples: (single shift) 01010111 = 87 >>> 1 00101011 = 43 (double shift) 01010111 = 87 >>> 2 00010101 = 21 byte b1 = 87 >>> 1; //43: 00101011 byte b2 = 87 >>> 2; //21: 00010101 These are the bit and byte operations which are used to effectively create this steganography application, I will provide some more complex examples, breaking down the steps of adding the data to the image, a little later. BufferedImage: A bufferedImage is something to be comfortable with when dealing with images. They are easily used with the newly introduced ImageIO class of Java 1.5.0 as well as containing methods for accessing the raster and buffer of the image, which makes image editing much easier. The basic actions for creating a new image are: BufferedImage img = new BufferedImage(int, int, int); File file = new File(String); BufferedImage img = ImageIO.read(file); ImageIO: A useful class to handle IO operations on images. This class has much to offer, but as far as this program is concerned, the read() and write() methods will be sufficient. Graphics2D: A class which has been around for a long time as far as Java is concerned, and allows access to some of the more in depth aspects of graphics/images. Allows for creating editable areas in a new image or an image which already exists. As well as allowing a way to reach the renderable area of the image. This class also allows for an easy switch from image space to user space, which is necessary when modifying or reading certain bytes of an image. WritableRaster: This by definition is the process of rendering an image pixel by pixel, which comes in handy when you need to access the bytes of an image, that are representing pixels. WritableRaster is a sub-class of Raster itself, which has methods to access the buffer of an image more directly. DataBufferByte: The form of a byte[] buffer for an image. *These topics/classes will be useful to know and have experience with as you attempt to modify this application, or create similar applications of your own. The Program: There are a few specific methods that should be gone over, including the complex bit operations to add the data seamlessly into the image to properly understand the how and why behind this code. User Space: private BufferedImage user_space(BufferedImage image) { BufferedImage new_img = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_3BYTE_BGR); Graphics2D graphics = new_img.createGraphics(); graphics.drawRenderedImage(image, null); graphics.dispose(); return new_img; } -To make the switch into user space (this is the actual term) a new image is created the same size as the original, and a graphics area is created in it. -The original image is then rendered/drawn onto the new image -As an added memory benefit, the resources used by the new image are released That’s it, the new image is now completely in user space, this means that all of the data is created and thus can be modified in Java. There are issues with trying to modify an image directly, the changes are not always applied. It is also advisable to create this user space as a new copy of the original image, thus ensuring there is no resource sharing between the original and user space version which may impede the saving of your changes. Bit Conversion: private byte[] bit_conversion(int i) { byte byte3 = (byte)((i & 0xFF000000) >>> 24); byte byte2 = (byte)((i & 0x00FF0000) >>> 16); byte byte1 = (byte)((i & 0x0000FF00) >>> 8 ); byte byte0 = (byte)((i & 0x000000FF) ); return(new byte[]{byte3,byte2,byte1,byte0}); } I thought it important to explain this operation. This method could just as easily be written as: private byte[] bit_conversion(int i) { return(new byte[]{0,0,0, (byte)(i & 0x000000FF)); } Because a byte holds a max value of 127, all shifts of 8 and higher, will remove all bits and replace them with zeros, but to be proper, to save each set of bits, the implementation is left as calculating each byte. *Note that hex FF = 11111111 in binary this is important, because, if there were more than 8 bits, say 16 and let i = 287: 0000000100011111 = 287 0000000011111111 = 255 or 0x00FF 0000000000011111 = 31 The result has the last 8 bits matching I, but the first 8 bits were all removed to 0s due to being AND with 0s in all positions, but the last 8. The thing to take from this, is we can force a value to 0, by ANDing with 0, and leave a value alone, by ANDing with 1. Encode Text: private byte[] encode_text(byte[] image, byte[] addition, int offset) { if(addition.length + offset > image.length) { throw new IllegalArgumentException("File not long enough!"); } for(int i=0; i=0; --bit, ++offset) { int b = (add >>> bit) & 1; image[offset] = (byte)((image[offset] & 0xFE) | b ); } } } At first this can appear overwhelming, the task of doing it nearly drove me insane, until I had read on countless websites and forums about classic implementations of steganography and how to split up and place the bits. In a byte, the bits have a rank, the left most bit is the most significant and right most, least significant. This gives us the key, if we need to change some data in this image, we want it to be as unobtrusive as possible, or even invisible. Thus we want to apply our changes to the least significant bit of some of the bytes. In this way we change each byte, a maximum of 1 in value. Here is how this code accomplishes that: [il] for(int i=0; i=0; --bit, ++offset)[/il] – loops through the 8 bits of the byte stored in add [il] int b = (add >>> bit) & 1;[/il] – b is assigned the value of the byte add shifted right bit positions AND 1 This may look complicated, but the end result is a loop which systematically assigns b the next single bit value of the byte add, either 0, or 1. This is best seen in a set of examples: We will start with [il] int b = (add >>> bit);[/il] only, Say: add = 87 = 01010111 First loop through, bit = 7: 01010111 = 87 >>> 7 00000000 = 0 Next time, bit = 6: 01010111 = 87 >>> 6 00000001 = 1 Next time, bit = 5: 01010111 = 87 >>> 5 00000010 = 2 Next time, bit = 4: 01010111 = 87 >>> 4 00000101 = 5 … and so on. *Notice how the right bits match the left bits of add, in a growing number based on how many positions we shift add. Now to apply the [il]& 1[/il]: First loop: 00000000 = 0 00000001 = 1 00000000 = 0 = b Next: 00000001 = 1 00000001 = 1 00000001 = 1 = b Next: 00000010 = 2 00000001 = 1 00000000 = 0 = b Next: 00000101 = 5 00000001 = 1 00000001 = 1 = b Note the pattern, b is assigned the value 0 or 1, based on the last bit of the shifted add byte. We accomplish the same as above, by ANDing by 1, which clears all bits to 0, except the last which is left as it was. This means that b’s value represents the bit at position bit in the for loop. [il] image[offset] = (byte)((image[offset] & 0xFE) | b );[/il] This line of code works in a similar way. 0xFE is hex, which represents 11111110 in binary. By reasoning above, this will leave the first 7 bits as is, and clear the least significant bit to 0. Then with the last bit 0, we OR it with b, which is either: 00000000 or 00000001. This will set the last bit to match the value stored in b. As the OR operation with 0s will not change any of the first 7 bits, and thus knowing the last bit is a 0, the value in this position of b, is guaranteed to be placed into this position, whether it be 0 or 1. *The code advances the offset value as the loop continues as well, thus the 8 bits of a single byte of addition are separated across the 8 least significant bits of 8 separate and sequential bytes of the image. **Also it is important that we encode the length first, and do it in a static way, eg. It is saved in 4 bytes, or the first 32 least significant bits. Thus we know how many least significant bits to read after the length to retrieve the entire message. Decode Text: private byte[] decode_text(byte[] image) { int length = 0; int offset = 32; for(int i=0; i<32; ++i) { length = (length << 1) | (image[i] & 1); } byte[] result = new byte[length]; for(int b=0; b