Encrypt images in JavaScript
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.
Breaking simple ciphers 7
The last few days I've happened to stumble over a couple of ciphers and I just couldn't help myself from trying to break them.
The Lost Symbol
Dan Brown has a new book coming out and part of the promotion is this cipher text "AOFACFSOA FSZWBEIC EIOA ZOHSFWQWOA OQQSDW". The WQW, QQ and three of the words ending with an A made me believe we could be dealing with a substitution cipher and maybe even a Caesar cipher, the most simple of them all.
My usual tool of choice is Ruby and in this case the splendid Interactive Ruby Shell.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
$ irb >> s="AOFACFSOA FSZWBEIC EIOA ZOHSFWQWOA OQQSDW" => "AOFACFSOA FSZWBEIC EIOA ZOHSFWQWOA OQQSDW" >> def caesar(text,n) >> alphas=('A'..'Z').to_a*2 >> text.tr('A-Z', alphas[n..n+26].join) >> end >> 1.upto(25) do |n| puts "%2d. %s" % [n, caesar(s,n)] end 1. BPGBDGTPB GTAXCFJD FJPB APITGXRXPB PRRTEX 2. CQHCEHUQC HUBYDGKE GKQC BQJUHYSYQC QSSUFY 3. DRIDFIVRD IVCZEHLF HLRD CRKVIZTZRD RTTVGZ 4. ESJEGJWSE JWDAFIMG IMSE DSLWJAUASE SUUWHA 5. FTKFHKXTF KXEBGJNH JNTF ETMXKBVBTF TVVXIB 6. GULGILYUG LYFCHKOI KOUG FUNYLCWCUG UWWYJC 7. HVMHJMZVH MZGDILPJ LPVH GVOZMDXDVH VXXZKD 8. IWNIKNAWI NAHEJMQK MQWI HWPANEYEWI WYYALE 9. JXOJLOBXJ OBIFKNRL NRXJ IXQBOFZFXJ XZZBMF 10. KYPKMPCYK PCJGLOSM OSYK JYRCPGAGYK YAACNG 11. LZQLNQDZL QDKHMPTN PTZL KZSDQHBHZL ZBBDOH 12. MARMOREAM RELINQUO QUAM LATERICIAM ACCEPI 13. NBSNPSFBN SFMJORVP RVBN MBUFSJDJBN BDDFQJ 14. OCTOQTGCO TGNKPSWQ SWCO NCVGTKEKCO CEEGRK 15. PDUPRUHDP UHOLQTXR TXDP ODWHULFLDP DFFHSL 16. QEVQSVIEQ VIPMRUYS UYEQ PEXIVMGMEQ EGGITM 17. RFWRTWJFR WJQNSVZT VZFR QFYJWNHNFR FHHJUN 18. SGXSUXKGS XKROTWAU WAGS RGZKXOIOGS GIIKVO 19. THYTVYLHT YLSPUXBV XBHT SHALYPJPHT HJJLWP 20. UIZUWZMIU ZMTQVYCW YCIU TIBMZQKQIU IKKMXQ 21. VJAVXANJV ANURWZDX ZDJV UJCNARLRJV JLLNYR 22. WKBWYBOKW BOVSXAEY AEKW VKDOBSMSKW KMMOZS 23. XLCXZCPLX CPWTYBFZ BFLX WLEPCTNTLX LNNPAT 24. YMDYADQMY DQXUZCGA CGMY XMFQDUOUMY MOOQBU 25. ZNEZBERNZ ERYVADHB DHNZ YNGREVPVNZ NPPRCV |
Take a closer look at row 12.
MARMOREAM RELINQUO QUAM LATERICIAM ACCEPI
I found Rome a city of bricks and left it a city of marble. - Google tells me it's Augustus.
The code is not the most clear I've written but if you read Ruby in your sleep you can skip this part.
('A'..'Z') is a range in Ruby. Another, maybe more obvious, example of a range is (0..7).
.to_a could be read as to_array and unsurprisingly it converts a range to an array. (0..7).to_a will create [0, 1, 2, 3, 4, 5, 6, 7]
The operator * for arrays appends n copies of the array. Thus [0,1,2]*2 will create [0,1,2,0,1,2].
String#tr works the same way as the Unix command tr, it translates the characters in the string according to the from and to parameters.
At last .join converts the array to a string.
The recruiting agency
A government agency responsible for signals intelligence is hiring. Among the qualifications they are looking for is the ability to break a certain cipher. I will not publish their cipher here but instead one of my own, constructed in the same way as theirs.
"VGhpcyBpcyBleGNsdXNpdmUgZm9yIHlvdSwgb3I/IGMNR0d LCkZPXgpTRV8K\nTENEQ1lCCkhfXgpoT1NFRElPCkNZCksKSE9eXk 9YCklYU1peRU1YS1pCT1gK\nXkJLRAp5SUJET0NPWAQ="At first glance it looked like Base64 and the ending "=" made it even more likely.
1 2 3 4 5 |
$ irb >> require 'base64' >> cipher = "VGhpcyBpcyBleGNsdXNpdmUgZm9yIHlvdSwgb3I/IGMNR0dLCkZPXgpTRV8K\nTENEQ1lCCkhfXgpoT1NFRElPCkNZCksKSE9eXk9YCklYU1peRU1YS1pCT1gK\nXkJLRAp5SUJET0NPWAQ=" >> decoded=Base64.decode64(cipher) => "This is exclusive for you, or? c\rGGK\nFO^\nSE\nLCDCYB\nH^\nhOSEDIO\nCY\nK\nHO^^OX\nIXSZ^EMXKZBOX\n^BKD\nyIBDOCOX\004" |
So it's Base64 but to no surprise it didn't end there. The "This is exclusive for you, or?" hinted at XOR so I tried XORing the text with 0-255.
1 2 3 4 5 6 7 8 |
>> code=decoded[31..decoded.length].split(//) >> File.open('xor.txt','w') { |file| ?> 0.upto(255) {|n| ?> file.write(n.to_s + " ") >> code.each {|c| file.write( (c[0]^n).chr ) } >> file.write("\n\n") >> } >> } |
A quick look in the file told me that XORing with 42 was the solution.
Now you know how to break two of the most simple cipher methods. Use the knowledge wisely. :)
Blowfish in the URL
Sometimes you do not want to show the database id for a row in the URL. The reason could be that you do not want someone to be able to scan through all the data.
One solution is to use GUID's but they have drawbacks and one of them is that they add a considerable length to the URL. The shortest URL-safe representation of a GUID I've seen is 22 characters but usually they are 36 characters.
Depending on how your id's are implemented a much shorter way could be to simply to encrypt them.
Here's a Ruby-example that Blowfish encrypts, Base64 encodes and URL-encodes an integer value. You can get crypt as a gem:
gem install crypt
1 2 3 4 5 6 7 8 9 10 11 |
require 'rubygems' require 'crypt/blowfish' require 'Base64' blowfish = Crypt::Blowfish.new("A key up to 56 bytes long") plainId=123456 encrypted = blowfish.encrypt_block(plainId.to_s.ljust(8)) idForURL = URI.escape((Base64.encode64(encrypted).strip)) decryptedId = blowfish.decrypt_block( Base64.decode64( URI.unescape(idForURL))). strip.to_i |
The .ljust(8) is because Blowfish is a 64-bit block cipher and the Ruby-implementation does not pad the data itself.
The id in the URL in this case would be c2PSXWgky40=. Its 12 characters long (11 if you skip the equal sign) and that's 10 or 24 characters shorter than a GUID. Also there is zero percent chance of collusion and if you want to you can even decrypt it.
This is not a super safe implementation but if you start your id's at a random and not too low number you are making it a bit harder for someone to crack the 56-bit key. Actually a truly random and at least 64-bit big number would be a better choice as it would have no connection to the true id at all. You would have to check for uniqueness before storing those in the database though.
The Zodiac Killer Cipher 5
The Zodiac Killer was a serial killer in the late sixties and maybe early seventies. He sent a number of letters to the press, including four ciphers or cryptograms and only one of them has been solved. The killer's identity remains unknown.
Chris McCarthy has a nice page about the cipher and he also has an ASCII version of the cipher.
Here's a small Ruby hack that calculates the character frequency using the ASCII version of the cipher. Feel free to use it if you like to have a go at cracking it!
EDIT: At this page you can have a go at cracking it real-time. I am not convinced it's really a homophonic substition cipher since the frequency analysis shows that the 340 does not have a flat frequency distribution.
It would be nice to know what cryptographic literature was available for the public in northern California in the late sixties.
