フロントエンドWeb戦略室

最終回 クライアントサイドでの暗号化とバイナリデータの扱い(2)

この記事を読むのに必要な時間:およそ 5.5 分

<(1)はこちらから。>

ダウンロードして復号までの流れ

ファイルのダウンロード時にはlocation.hashからパラメータを受け取ります。ダウンロード用リンクのlocation.hashは,暗号化済みのデータのパス,復号のためのパスワード,オリジナルのファイル名をカンマで連結したものです。

location.hashの値は,JavaScriptで明示的に取得し送信を行わない限り,サーバに送られることはありません。暗号化済みデータのパスはダウンロードの際,サーバに送られることになりますが,パスワードとオリジナルのファイル名はサーバに送られずに,ブラウザ内でのみ使用します。つまりサービスの運営者からわかるのは「どのファイルがリクエストされたのか」という情報だけで,そのファイルの内容やファイル名はわからないことになります。

ファイルをダウンロードする

XMLHttpRequestを使って暗号化済みのファイルをダウンロードします。responseTypeを指定することで,ファイルをバイナリデータとして受け取ることが可能です。

XMLHttpRequestで従来よりよく使われるresponseTextは,標準では受信したデータを UTF-8のテキストとして解釈しようとしますリスト4)⁠たとえば,

  • あいうえお

とUTF-8で書かれたファイルを受信した場合,ファイルは15bytesになりますリスト5)⁠

リスト4 textとして受信する場合

req = new XMLHttpRequest;
req.open("GET", "test.txt", false);
req.send();
req.responseText.length; // 5 文字

リスト5 バイナリデータとして受信する場合

req = new XMLHttpRequest;
req.responseType = "arraybuffer";
req.open("GET", "test.txt", false);
req.send();
req.response.byteLength; // 15bytes

それに対して,req.responseText.lengthはリスト4のように5文字を示します。文字数とバイト数が一致しないと,crypto-jsなどのライブラリにて,1byte単位で取り出すことができません。一般的に,暗号化されると8bytes単位で処理されますが,先の例だと「う」の途中で切れてしまう,という問題が発生してしまうのです。

今までXMLHttpRequestを使ってバイナリファイルを扱うには,overrideMimeTypeを使ってcharset = x-user-definedという文字コードを指定する方法がよく知られていました注8)⁠英数字のみを扱うならば文字数=バイト数になりますが,リスト4,リスト5のようにマルチバイト文字を含むデータは,テキストとしてデコードされると,文字数とバイト数が一致しなくなります。

特殊な文字コード指定を使うことで,受信したデータが1byte単位でJavaScriptのstringに格納されるのです注9)⁠古いブラウザを動作対象とするのであれば覚えておいても損はないですが,最新のブラウザを動作対象にするのであれば,何はともあれArray Bufferを使うのがよいでしょう。

注8)
XMLHttpRequestを使わない方法もあります。古いブラウザなど,バイナリデータの受信に支障がある場合,サーバ側でバイナリデータをBase64エンコードして取り扱うのです。しかし,この方法ではファイルサイズが大きくなってしまいます。
注9)
実際にはもう少し複雑な手順が必要ですが,詳細は省きます。

ファイルのダウンロードと復号

単純にXMLHttpRequestで巨大なファイルをダウンロードする場合,やはりメモリに貯めこんでしまいますので,数MBであればともかくとして,GBを超えるファイルはとても現実的には扱えないという状況になります。

この問題はサーバ側でファイルを分割してダウンロードすることで解決できます。問題は,分割して受信したファイルをその都度復号し,復号した結果を結合しなければならないことです。

crypto-jsでは受信したデータを順次復号していくことが可能ですが,その結果をJavaScriptの変数に格納するのであれば,分割して受信したところで最終的なファイルのサイズ分だけメモリを消費することになってしまいます。ローカルで巨大なファイルを生成するには,⁠サーバからデータを受信して,ファイルに追記し破棄する」という繰り返し処理が必要です。この問題を解決するためには,JavaScriptから「ファイルに追記していく」操作が必要なのですが,原稿執筆時点(2013年1月)でこれが可能なのはGoogle Chromeだけです。MegaがGoogle Chromeを推奨しているのは,こういった事情のためでしょう。

ほかのブラウザでは動かないことになってしまうので,分割ダウンロードと結合部分についてはサンプルでは作りませんでしたが,FileSystem APIがサポートされている場合はFileSystem APIを使うようにしてもよいでしょうリスト6)⁠次項ではFileSystem APIを詳しく見ていきます。

リスト6 ダウンロード処理

function download(path, password, filename){
  req = new XMLHttpRequest;
  req.open("GET", "./files/" + path, true);
  req.responseType = "arraybuffer";
  req.onload = function(){
    var buffer = req.response;
    decode_arraybuffer(buffer, password, filename);
  };
  req.send("");
}

function objecturlready(url, filename){
  var el;
  if (/(gif|jpg|png)$/i.test(filename)) {
    el = TB("img", {title: filename, alt: filename, src: url});
  } else {
    el = TB("a", {href: url, download: filename}, "Click here to save file");
  }
  document.getElementById("result").appendChild(el);
}

function decode_arraybuffer(ab, password, filename) {
  var dec = new StreamDecryptor(password);
  dec.decorder = null;
  var wordarray = CryptoJS.lib.WordArray.create( ab );
  dec.process(wordarray);
  dec.process();

  var isImage = /(gif|jpg|png)$/i.test(filename);
  var suffix = filename.match(/([^.]*?)$/);
  suffix = suffix ? suffix[1].toLowerCase() : "";
  var mimetype = isImage ? { type: "image/"+suffix}
  : { type: "application/octet-stream"};
  if (window.requestFileSystem) {
    var file = new TempFile();
    file.onready = function(){
      file.write( new Blob(dec.result) );
      setTimeout(function(){
        objecturlready(file.toURL(), filename);
      }, 100);
    };
  } else {
    // Safari does not support create blob from typed array
    var isBuggyBrowser = (new Blob([new Uint8Array()]).size > 0 ) ? true : false;
    var blob = isBuggyBrowser
        ? new Blob(dec.result.map(function(v){ return v.buffer }), mimetype)
        : new Blob(dec.result, mimetype);
    var objectURL = (
        window.URL || window.webkitURL || dataURLsim
      ).createObjectURL(blob);
    setTimeout(function(){
      objecturlready(objectURL, filename);
    }, 0);
  }
}

著者プロフィール

mala(マラ)

NHN Japan所属。livedoor Readerの開発で知られる。JavaScriptを使ったUI,非同期処理,Webアプリケーションセキュリティなどに携わる。

Twitter:@bulkneets

コメント

コメントの記入