フロントエンドWeb戦略室

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

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

分割して暗号化する

それでは,読み取ったデータを暗号化していきましょう。ここでは,crypto-jsというライブラリを使いますリスト1)⁠ハッシュ関数や暗号化ライブラリは多数の実装が存在しますが,ストリームの入力に対応していることは重要です注6)⁠

リスト1 crypto-jsを使ったサンプルコード

<script src="http://crypto-js.googlecode.com/svn/tags/3.1.2/build/rollups/aes.js"></script>
<script>
  var key = CryptoJS.enc.Hex.parse('000102030405060708090a0b0c0d0e0f');
  var iv = CryptoJS.enc.Hex.parse('101112131415161718191a1b1c1d1e1f');

  var aesEncryptor = CryptoJS.algo.AES.createEncryptor(key, { iv: iv });

  var ciphertextPart1 = aesEncryptor.process("Message Part 1"); ┓
  var ciphertextPart2 = aesEncryptor.process("Message Part 2"); |
  var ciphertextPart3 = aesEncryptor.process("Message Part 3"); ┛(1)
  var ciphertextPart4 = aesEncryptor.finalize(); (2)

  var aesDecryptor = CryptoJS.algo.AES.createDecryptor(key, { iv: iv });

  var plaintextPart1 = aesDecryptor.process(ciphertextPart1); ┓
  var plaintextPart2 = aesDecryptor.process(ciphertextPart2); |
  var plaintextPart3 = aesDecryptor.process(ciphertextPart3); |
  var plaintextPart4 = aesDecryptor.process(ciphertextPart4); |
  var plaintextPart5 = aesDecryptor.finalize();               ┛(3)
</script>

通常の暗号化が文字列とパスワードを一度に渡し,暗号化された文字列を得るのに対し,分割して暗号化するには,process(リスト1 (1) finalize(リスト1 (2) を使います。リスト1 (1) が,分割して順次暗号化している個所です。分割して入力された文字列をバッファに格納し,ブロック単位で暗号化していきます。入力文字列をブロックの長さで分割していきますので,割り切れなかった部分はバッファに残ってしまっています。リスト1 (2) でfinalizeを呼ぶことで,ブロックから溢れたバッファに残っている部分を暗号化し,出力することができるのです注7)⁠

注6)
crypto-js以外のものを使うときにも,巨大なデータを一度に渡さなくても分割して逐一暗号化してくれる,テストされていて実績のあるライブラリを選ぶのがよいでしょう。
注7)
リスト1(3)は復号を示しています。

暗号化したそばからアップロード

大容量のファイルをサーバに直接アップロードする場合,サーバ側でも受け取ったコンテンツをメモリ上に展開せず,ファイルに直接保存するなどの工夫が必要です。ファイルをブラウザ側でメモリに溜め込まず,分割して逐次処理をしていますので,サーバにも分割したまま次々とアップロードしていきましょう。ファイル名にはランダムに生成した文字列のハッシュ値を使用しています。元の文字列がわからない限り上書きも削除もできません。サンプルはリスト2のとおりです。

リスト2 サーバ側の実装例

# app.psgi
# plackup --port=5000 app.psgi

use strict;
use Plack::Builder;
use Plack::Request;
use Plack::App::Directory;
use Path::Class;
use Digest::MD5 qw(md5_hex);

my $upload_dir = "./files/";

builder {
  mount "/files" => Plack::App::File->new(
    root => $upload_dir);
  mount "/upload" => sub {
    my $env = shift;
    my $req = Plack::Request->new($env);
    if ($req->method eq "POST") {
      my $file = file(
        $upload_dir . md5_hex($req->param("key")));
      my $appender = $file->open('a') or die $!;
      $appender->print($req->raw_body);
      $appender->close;
      }
      [200, [], ["OK"]];
    };
    mount "/delete" => sub {
      my $env = shift;
      my $req = Plack::Request->new($env);
      my $success;
      if ($req->method eq "POST") {
        my $file = file(
          $upload_dir . md5_hex($req->param("key")));
          $success = $file->remove;
      }
      [200, [], [ $success ? "OK" : "NG" ]];
    };
    mount "/" => sub {
      my $env = shift;
      if ($env->{PATH_INFO} eq "/") {
          return Plack::App::File->new(
            file => './static/index.html')->call($env);
      }
      Plack::App::File->new(root => './static/')->call($env);
    };
  };

サーバ側での実装に合わせて形式は自由に決めてもよいのですが,ここではArrayBufferを使ってバイナリデータをそのまま送信します。ファイル名を決定するために必要なパラメータは,URLへのクエリパラメータで指定するようにしました。アップロードクライアント側の処理は,リスト3のとおりです。

以上が暗号化してアップロードするまでの流れです。次は,アップロードされたファイルをダウンロードする際の復号する処理について解説します。

リスト3 アップロードクライアント側の実装例

function StreamUploader(api){
  this.uniq_key = random_str(20);
  this.num = 0;
  this.api = api;
  this.busy = false;
  this.queue = [];
}
StreamUploader.prototype = {
  // TODO: retry
  upload_binary: function(binary){ // typed array
  var self = this;
  var task = function() {
    self.busy = true;
    var xhr = new XMLHttpRequest;
    console.log("upload: " + binary.length + " bytes");
    var param = "num=" + self.num + "&" + "key=" + self.uniq_key;
    self.num++;
      xhr.onload = function(){
        setTimeout(function(){ self.busy = false }, 1000) };
       xhr.open("POST", self.api + "?" + param, true);
       xhr.send(binary.buffer);
  };
  this.queue.push(task);
  this.dequeue();
},
dequeue: function(){
  var self = this;
  if (!this.queue.length) return;
  if (this.busy) {
    setTimeout(function(){
      self.dequeue();
    }, 1000);
  } else {
    var task = this.queue.shift();
    task();
    }
  }
};
<続きの(2)はこちら。>

著者プロフィール

mala(マラ)

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

Twitter:@bulkneets