Encrypt images in JavaScript

Posted by Jonas Elfström Thu, 14 Oct 2010 21:40:00 GMT

In browsers that supports the HTML5 canvas it's possible to read the raw image data. I figured that this makes it possible to encrypt images on the client. Why would one ever want to encrypt images client-side? One use case could be to send images to a server that only those who know the password can decrypt (host-proof hosting).

I used Crypto-JS to encrypt with AES and Rabbit.

First I get the CanvasPixelArray from the ImageData object.

1
2
3
4
var ctx = document.getElementById('leif')
                  .getContext('2d');
var imgd = ctx.getImageData(0,0,width,height);
var pixelArray = imgd.data;

The pixel array has four bytes for each pixel as RGBA but Crypto-JS encrypts a string, not an array. At first I used .join() and .split(",") to get from array to string and back. It was slow and the string got much longer than it had to be. Actually four times longer. To save even more space I decided to discard the alpha channel.

1
2
3
4
5
6
7
8
9
10
function canvasArrToString(a) {
  var s="";
  // Removes alpha to save space.
  for (var i=0; i<pix.length; i+=4) {
    s+=(String.fromCharCode(pix[i])
        + String.fromCharCode(pix[i+1])
        + String.fromCharCode(pix[i+2]));
  }
  return s;
}

That string is what I then encrypt. I sticked to `+=` after reading String Performance an Analysis.

var encrypted = Crypto.Rabbit.encrypt(imageString, password);

I used a small 160x120 pixels image. With four bytes for each pixels that gives 76800 bytes. Even though I stripped the alpha channel the encrypted image still takes up 124680 bytes, 1.62 times bigger. Using `.join()` it was 384736 bytes, 5 times bigger. One cause for it still being larger than the original image is that Crypto-JS returns a Base64 encoded string and that adds something like 37%.

Before I could write it back to the canvas I had to convert it to an array again.

1
2
3
4
5
6
7
8
9
10
function canvasStringToArr(s) {
  var arr=[];
  for (var i=0; i<s.length; i+=3) {
    for (var j=0; j<3; j++) {
      arr.push(s.substring(i+j,i+j+1).charCodeAt());
    }
    arr.push(255); // Hardcodes alpha to 255.
  }
  return arr;
}

Decryption is simple.

1
2
3
4
var arr=canvasStringToArr(
              Crypto.Rabbit.decrypt(encryptedString, password));
imgd.data=arr;
ctx.putImageData(imgd,0,0);

Tested in Firefox, Google Chrome, WebKit3.1 (Android 2.2), iOS 4.1, and a very recent release of Opera.

Browser / Action in milliseconds Enc. Rabbit Dec. Rabbit Enc. AES Dec. AES
Google Chrome 6.0.472.62 C2D@1.86GHz 136 130 236 222
Opera 10.63 P4HT@3GHz 246 252 438 437
Google Chrome 6.0.472.63 P4HT@3GHz 280 648 303 547
Firefox 3.6.10 Phenom II X4 945@3GHz 494 321 1876 1745
Firefox 3.6.10 i5@3,5GHz 366 193 1639 1410
Firefox 3.6.10 C2D@1.86GHz 760 367 2417 1983
Firefox 3.6.10 P4HT@3GHz 880 440 4000 3500
Nokia N900 Chrome 1764 1975 2509 2508
WebKit 3.1 (HTC Desire) 2000 2200 3300 3400
iPhone 3GS iOS 4.1 2130 2071 7198 7131
N900 Firefox Mobile 3411 3508 19308 19466
N900 native (MicroB) 4681 4300 24560 20973
X10 Mini Pro, Android 1.6 7464 7747 timeout timeout

 

A demo can be found here.

EDIT 2010-10-17

Warning I've noticed that the encrypted strings aren't compatible between browsers. At least not in between Google Chrome and Firefox. I don't know why.

I also tried to add deflate/inflate and that compresses my test image to a third of the raw size and in Google Chrome it also halved the execution time. In Firefox Rabbit got about 50% slower and AES about 50% faster with deflate/inflate.

Here's a demo of this.

EDIT 2010-10-21

Added a preprocessing filter of the raw image data inspired by PNG type 1 sub filter. Presented my idea to Fredrik and he returned a formula. Thanks!
The result wasn't what I had hoped for, a measly 6% smaller. I also tried to save the image as real PNG and that one is 20480 bytes. 20480*1.37=28057 bytes (Base64 overhead) and my homemade format is 38336 bytes. Not a fantastic result but not that horrible either.

Here's a demo of this.

Posted in Cryptography, JavaScript | no comments

Comments

Comments are closed