使用浏览器中的JavaScript解密图像

10
我有一个基于Web的应用程序,需要在发送到服务器之前对图像进行加密,以及当用户提供正确密钥后从服务器加载到浏览器后进行解密。
我的第一种方法是使用AES加密图像像素并保留图像头。我必须将加密后的图像保存为无损格式,例如png格式。损失压缩格式如jpg会改变AES加密位,使其无法解密。
现在,加密图像可以在浏览器中加载,呈现完全混乱的外观。这里我有JavaScript代码通过Image.canvas.getContext("2d").getImageData()读取RGB像素的图像数据,获取来自用户的密钥,使用AES解密像素,重绘画布,并向用户展示解密后的图像。
这种方法起作用,但存在两个主要问题。第一个问题是将完全混乱的图像以无损格式保存需要大量字节,接近每个像素3个字节。第二个问题是在浏览器中解密大型图像需要很长时间。因此,采用了第二种方法,即加密图像头而非实际像素。但我没有找到任何方法在JavaScript中读取图像头以便进行解密。Canvas只提供已经解压缩的像素数据。实际上,浏览器显示具有更改后头部的图像是无效的。
如何改进第一种方法或使第二种方法成为可能,或提供其他方法的任何建议都将不胜感激。对于冗长的帖子,表示抱歉。

需要在将图像发送到服务器之前对其进行加密。不知道如何在发送之前获取图像数据? - oberhamsi
2
“主机证明托管”是您要寻找的术语。 - Acorn
3个回答

8

你的启发激励我尝试了一下。我写了一篇博客,你可以在这里找到一个演示

我使用Crypto-JS进行AES和Rabbit加密和解密。

首先,我从ImageData对象中获取CanvasPixelArray。

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

像素数组每个像素有四个字节,表示RGBA,但Crypto-JS加密的是字符串,而不是数组。一开始我使用.join()和.split(",")从数组转换为字符串和反向操作。这种方法很慢,而且字符串比必要的长多了,实际上是四倍长。为了节省更多空间,我决定放弃alpha通道。
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;
}

那个字符串是我要加密的内容。在阅读了字符串性能分析之后,我坚持使用+=。
var encrypted = Crypto.Rabbit.encrypt(imageString, password);

我使用了一张小的160x120像素的图片。每个像素有四个字节,这就需要76800字节。尽管我去掉了alpha通道,但加密后的图像仍占用124680字节,比原始图像大1.62倍。使用.join()后大小为384736字节,比原始图像大5倍。造成它仍比原始图像大的一个原因是Crypto-JS返回一个Base64编码的字符串,这增加了大约37%的大小。
在我将其写回画布之前,我必须再次将其转换为数组。
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;
}

解密很简单。

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

在Firefox、Google Chrome、WebKit3.1(Android 2.2)、iOS 4.1以及最近发布的Opera版本中进行了测试。

alt text


3
保存图片的原始数据时,请对其进行加密和Base64编码(除非使用Java小程序,否则只能在支持HTML5文件API的Web浏览器上执行此操作)。下载图像时,请对其进行解码、解密,并为浏览器创建一个数据URI以供使用(或再次使用Java小程序来显示图像)。
但是,您无法消除用户信任服务器的需求,因为服务器可以向客户端发送任何JavaScript代码,而客户端在解密后可以将图像的副本发送给任何人。这是一些人对加密电子邮件服务Hushmail的担忧——政府可能会强迫该公司提供恶意Java小程序。这不是不可能的情况;电信公司Etisalat曾试图通过远程安装间谍软件来拦截黑莓通讯(http://news.bbc.co.uk/2/hi/technology/8161190.stm)。
如果您的网站是公众使用的,则无法控制用户的软件配置,因此他们的计算机甚至可能已经感染了间谍软件。

谢谢。这是一个有趣的方法。有哪些JavaScript API可以让我从服务器下载文件,读取整个文件,操作它并创建数据URI以显示为图像?如果我能做到这一点,我就可以加密图像头而不是图像数据,这将解决我在大型加密文件和缓慢解密方面遇到的问题。到目前为止,我还没有找到一种将下载的文件保存在磁盘上或内存中的方法。 - timeon
同意您对信任的评论。这可能会减少对服务器管理员安全处理服务器上的图像技术能力的信任需求。但这不是他的意图。我在这里可以使用任何输入。这是加密图像的主要动机。是否有我们可以使用的认证服务?我们的名字很小,不知名,但也许我们可以支付一些大公司来认证我们的过程和代码? - timeon
以下链接详细介绍了如何从服务器下载文件并通过数据URL将其显示为图像。http://ajaxref.com/ch4/datauri.html - timeon

0
我想做类似的事情:服务器上有一个加密的gif文件,我想在javascript中下载、解密并显示它。我已经成功地实现了这个功能,存储在服务器上的文件与原始文件大小相同,只多了几个字节(可能多达32个字节)。这是执行文件AES加密并生成VB.Net中的calendar.gif.enc代码。
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Dim AES As New System.Security.Cryptography.RijndaelManaged
        Dim encryption_key As String = "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"
        AES.Key = HexStringToBytes(encryption_key)
        Dim iv_string As String = "000102030405060708090A0B0C0D0E0F"
        'System.IO.File.ReadAllBytes("calendar.gif")
        'Dim test_string As String = "6bc1bee22e409f96e93d7e117393172a"
        AES.Mode = Security.Cryptography.CipherMode.CBC
        AES.IV = HexStringToBytes(iv_string)
        Dim Encrypter As System.Security.Cryptography.ICryptoTransform = AES.CreateEncryptor
        Dim b() As Byte = System.IO.File.ReadAllBytes("calendar.gif")
        System.IO.File.WriteAllBytes("calendar.gif.enc", (Encrypter.TransformFinalBlock(System.IO.File.ReadAllBytes("calendar.gif"), 0, b.Length)))
    End Sub

这是下载 calendar.gif.enc 作为二进制文件、解密并制作图像的 JavaScript 代码:
    function wordArrayToBase64(wordArray) {
      var words = wordArray.words;
      var sigBytes = wordArray.sigBytes;

      // Convert
      var output = "";
      var chr = [];
      for(var i = 0; i < sigBytes; i++) {
        chr.push((words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff);
        if(chr.length == 3) {
          var enc = [
            (chr[0] & 0xff) >> 2,
            ((chr[0] & 3) << 4) | ((chr[1] & 0xff) >> 4),
            ((chr[1] & 15) << 2) | ((chr[2] & 0xff) >> 6),
            chr[2] & 63
          ];
          for(var j = 0; j < 4; j++) {
            output += Base64._keyStr.charAt(enc[j]);
          }
          chr = [];
        }
      }
      if(chr.length == 1) {
        chr.push(0,0);
        var enc = [
          (chr[0] & 0xff) >> 2,
          ((chr[0] & 3) << 4) | ((chr[1] & 0xff) >> 4),
          ((chr[1] & 15) << 2) | ((chr[2] & 0xff) >> 6),
          chr[2] & 63
        ];
        enc[2] = enc[3] = 64;
        for(var j = 0; j < 4; j++) {
          output += Base64._keyStr.charAt(enc[j]);
        }
      } else if(chr.length == 2) {
        chr.push(0);
        var enc = [
          (chr[0] & 0xff) >> 2,
          ((chr[0] & 3) << 4) | ((chr[1] & 0xff) >> 4),
          ((chr[1] & 15) << 2) | ((chr[2] & 0xff) >> 6),
          chr[2] & 63
        ];
        enc[3] = 64;
        for(var j = 0; j < 4; j++) {
          output += Base64._keyStr.charAt(enc[j]);
        }
      }
    return(output);
  }
  var xhr = new XMLHttpRequest();
  xhr.overrideMimeType('image/gif; charset=x-user-defined');
  xhr.onreadystatechange = function() {
    if(xhr.readyState == 4) {
      var key = CryptoJS.enc.Hex.parse('603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4');
      var iv  = CryptoJS.enc.Hex.parse('000102030405060708090A0B0C0D0E0F');
      var aesEncryptor = CryptoJS.algo.AES.createDecryptor(key, { iv: iv });
      var words = [];
      for(var i=0; i < (xhr.response.length+3)/4; i++) {
        var newWord = (xhr.response.charCodeAt(i*4+0)&0xff) << 24;
        newWord += (xhr.response.charCodeAt(i*4+1)&0xff) << 16;
        newWord += (xhr.response.charCodeAt(i*4+2)&0xff) << 8;
        newWord += (xhr.response.charCodeAt(i*4+3)&0xff) << 0;
        words.push(newWord);
      }            
      var inputWordArray = CryptoJS.lib.WordArray.create(words, xhr.response.length);
      var ciphertext0 = aesEncryptor.process(inputWordArray);
      var ciphertext1 = aesEncryptor.finalize();

      $('body').append('<img src="data:image/gif;base64,' + wordArrayToBase64(ciphertext0.concat(ciphertext1)) + '">');
      $('body').append('<p>' + wordArrayToBase64(ciphertext0.concat(ciphertext1)) + '</p>');
    }
  };

注意事项:

  • 我使用了固定的 IV 和密码。您应该修改代码以在加密期间生成随机的 IV 并将它们作为输出文件的第一个字节添加。JavaScript 也需要进行修改,以提取这些字节。
  • 密码长度应该是固定的:256 位用于 AES-256。如果密码不足 256 字节,一种可能性是在加密和解密中使用 AES 哈希将密码散列为 256 位。
  • 您需要 crypto-js
  • overrideMimeType 在旧版浏览器上可能无法正常工作。您需要此功能,以便二进制数据能够正常下载。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接