Steganography: How to store data in Images

Yesterday I came across an interesting question on stackexchange. The idea was to store data in an avatar image, so you could easily send the avatar, which would display you and store your game data. 


How can we achieve this?

There are many ways, but I would like to introduce a fairly easy to implement solution. We use the PNG image datatype, where every pixel contains of 4 values: alpha red green blue.


ARGB from least significant order bit on


So for our method we will use the alpha channel and will take the loss of a possible transparency.

Implementation details


  • The stored data is limited by the number of pixels we have. We will use 1 pixel for one number, so we have space for width(image) * height(image) number of characters.
  • The alpha value of each pixel that is not used will be set to 255 (non-transparent)
  • We limit our data input to lowercase letters, numbers and blanks. However you might easily adapt the algorithm for other inputs.
  • We will subtract a number between 0 .. 37 from the alpha channel
  • The encoding is done like that:
    A blank is represented by 0
    A number is represented by it's successor, such as 0 will be 1, .. , 5 will be 6, ..., 9 will be 10
    A letter is represented by 11 .. 37, so 'a' will be 11, ... , 'z' will be 37.
 I wrote the code for Java SE, since Android implementation would be even easier here. 


KEEP IN MIND: modifying the image, by applying filters or cropping, scaling ... might destroy or corrupt data!

Example

Let's say we want to store the given gamedata in the users avatar.
User:    bluewhile
Score:  99452
Date:    2014-04-01 12:04:33


We wrap it up in one string: bluewhile 999452 20140401 120433

When decoding, we can use String.split(" "); to get 4 values: name, score, date, time.


Original
Encoded

First let's look at turning a given String into a int array:



public int[] makeCode(String s) {
  if (s == null || !s.matches("[A-Za-z0-9 ]+")) // alphanumeric and blank
   return null;
  String work = s.toLowerCase().trim() + " ";
  int[] array = new int[work.length()];

  for (int i = 0; i < work.length(); i++) {
   array[i] = work.charAt(i);
   if (array[i] == 32) { // blank -> 0
    array[i] = 0;
   } else if (array[i] <= 57) { // number -> 1..9
    array[i] -= 47;
   } else { // letter -> 10..36
    array[i] -= 86;
   }
  }
  return array;
 }

It should be straight forward what happens here. Initially we check if all chars are valid, then the array is populated. An additional blank is added to the String, but don't worry it will be deleted later by trim().


Next up: Encoding



public void encodeAndSave(String s, File input, File output) {
  System.out.println("Encoding: "+s);
  try {
   BufferedImage cache = ImageIO.read(input);
   BufferedImage b = new BufferedImage(cache.getWidth(),
     cache.getHeight(), BufferedImage.TYPE_INT_ARGB);
   b.getGraphics().drawImage(cache, 0, 0, null);

   int i = 0;
   int[] data = makeCode(s);

   System.out.println("Code: " + Arrays.toString(data));

   for (int y = 0; y < b.getHeight(); y++)
    for (int x = 0; x < b.getWidth(); x++) {
     Color c = new Color(b.getRGB(x, y));
     int alpha = (255 - 37) + (i < data.length ? data[i] : 37);
     int col = (alpha << 24) | (c.getRed() << 16) | (c.getGreen() << 8) | c.getBlue();
     b.setRGB(x, y, col);
     i++;
    }
   ImageIO.write(b, "png", output);
  } catch (Exception e) {
   e.printStackTrace();
  }
 }


At the beginning we copy the image over to make sure it has an alpha channel, then we iterate through all pixels and write our encoded array from left to right, from top to bottom. Pixels that are off our data are set to 255. Also keep in mind that we can't use JPG because it has no alpha channel.


Decoding

public String decode(File input) {
  try {
   BufferedImage b = ImageIO.read(input);
   String s = "";
   for (int y = 0; y < b.getHeight(); y++) {
    for (int x = 0; x < b.getWidth(); x++) {
     int alpha = (b.getRGB(x, y) >>> 24);
     if (alpha != 255 && alpha != 0) {
      int n = (37 - (255 - alpha));
      if (n == 0) {
       s += " ";
      } else if (n <= 10) {
       s += n - 1;
      } else {
       s += ((char) (n + 86));
      }
     }
    }
   }
   return s.trim();
  } catch (Exception e) {
   e.printStackTrace();
  }
  return null;
 }

Decoding is like Encoding, just reversed ;-) 

Calling methods should look like:

encodeAndSave("bluewhile 999452 20140401120433", new File(
    "/Users/paul/Downloads/avatar.png"), new File(
    "/Users/paul/Downloads/avatar2.png"));

System.out.println(decode(new File(
    "/Users/paul/Downloads/avatar2.png")));