ククログ

株式会社クリアコード > ククログ > Thunderbirdアドオン「CardBook(連絡先)」でローカルに保存されるデータの暗号化に対応しました

Thunderbirdアドオン「CardBook(連絡先)」でローカルに保存されるデータの暗号化に対応しました

CardBookと企業利用

皆さんはCardDAVという仕様をご存じでしょうか? CardDAVはWebDAVのプロトコルを使ってLDIF形式のアドレス帳をやり取りするという物で、これを用いると「読み書き両方を行えて、内容が複数PC間で同期される」という種類のリモートアドレス帳を汎用の物として実現することができます。CardDAVサーバーとして振る舞える製品にはownCloudやDAViCalなどがあり、読み取り専用に設定したリモートアドレス帳を複数人で共有するという事もできますので、企業利用では重宝する場面がありそうです。

このように便利なCardDAVですが、残念ながらThunderbirdは本体の機能としては対応していません。CardDAVベースのリモートアドレス帳を使うにはアドオンをインストールする必要があります。CardBookは、そのようなCardDAV対応のためのアドオンの一例です。

ところで、企業によっては個人情報の取り扱い方について、「顧客や取引先の個人情報をローカルに保存する際は必ず暗号化する」といったプライバシーポリシーを定めている場合があります。前出のCardBookはリモートアドレス帳のデータをIndexedDBを使用してローカルにキャッシュする設計で、この時のデータは暗号化されないため、そのままでは前述のポリシーに抵触するので採用できないという事になります。

そのような背景から、「CardBookでローカルに保存されるデータを暗号化したい」というご相談を頂き、成果を開発元に還元する前提で先行して作業を進めていたのですが、残念ながら受注には至らず、手元には実現可能性の調査のために行った試験的な実装が残るという結果になりました。しかしせっかく実装した物をそのまま放置しておくのも勿体なかったので、CardBookプロジェクトに還元したところ、標準機能の1つとして取り込まれるに至りました。現在リリース済みのCardBook 33.9以降のバージョンでは、設定画面でチェックボックスをONにすればローカルデータの暗号化が有効になるようになっています。

以下、CardBookのローカルデータベースの暗号化を実現するにあたって行った具体的な内容をご紹介します。

IndexedDBに格納するデータの暗号化と復号

幸い、Thunderbirdの基盤であるGeckoには、暗号化のための汎用APIであるWeb Crypto APIが実装されています。あるのなら使わない理由はありませんので、CardBookでもデータの暗号化はWeb Crypto APIによる共通鍵暗号で行う事にしました。何故公開鍵暗号ではなく共通鍵暗号なのかについては別項で詳しく述べていますので、そちらも併せてご覧下さい。

実装は、まず暗号化・復号を行う専用のモジュールを追加した上で、IndexedDBの読み書きを行うモジュールの書き込み用のデータを用意する箇所に暗号化処理を読み込んだデータを検証する箇所に復号処理を仕掛けることで、他のモジュールに影響を与えず透過的に動作するような組み込み方としました。

この時気をつけなくてはならないポイントとして、暗号化をどのタイミングで行うかという点が挙げられます。以下、暗号化を行っている実際の箇所を抜粋しながら説明します。

元々の設計では、IndexedDBへのデータ書き込みは以下の要領で行われていました。

addCard: unction (aDirPrefName, aCard, aMode) {
  var db = cardbookRepository.cardbookDatabase.db;
  // トランザクション開始
  var transaction = db.transaction(["cards"], "readwrite");
  var store = transaction.objectStore("cards");
  var storedCard = aCard;
  // データの書き込み
  var cursorRequest = store.put(storedCard);

  // 以下、成功時・エラー時の処理
}

ここに暗号化処理を組み込むのですが、Web Crypto APIは暗号化したデータがPromiseで返されるため、値を使うには.then()のコールバック関数で受け取るか、awaitで値の解決を待つ必要があります。コールバック関数を使うスタイルで実装するにはこのメソッドの書き方を大幅に変えなくてはなりませんが、asyncキーワードとawaitを使うと、この同期処理の関数を容易に非同期処理に対応させることができます。

addCard: async function (aDirPrefName, aCard, aMode) { // asyncキーワードを追加
  var db = cardbookRepository.cardbookDatabase.db;
  // トランザクション開始
  var transaction = db.transaction(["cards"], "readwrite");
  var store = transaction.objectStore("cards");
  // 暗号化処理を追加
  var storedCard = cardbookIndexedDB.encryptionEnabled ? (await cardbookEncryptor.encryptCard(aCard)) : aCard;
  // データの書き込み
  var cursorRequest = store.put(storedCard);

  // 以下、成功時・エラー時の処理
}

当初はこの例のように、書き込みを行う直前で暗号化を行うようにしていました。しかし実際に動作させてみると、これではIndexedDBでのデータ書き込みに失敗するという結果になりました。何故でしょうか?

実は、IndexedDBでのデータ書き込みはトランザクション開始から書き込みまでを同期的に(同じイベントループ内で)行う必要があります。この例ではトランザクション開始後にawaitを使ってしまっているせいで、store.put(storedCard)が次のイベントループでの実行となってしまい、そのせいで書き込みに失敗してしまうという訳です。

そのため最終的な実装では、以下の例のようにトランザクション開始前に暗号化を終えておくようにしました。

addCard: async function (aDirPrefName, aCard, aMode) {
  // 暗号化
  var storedCard = cardbookIndexedDB.encryptionEnabled ? (await cardbookEncryptor.encryptCard(aCard)) : aCard;
  var db = cardbookRepository.cardbookDatabase.db;
  // トランザクション開始
  var transaction = db.transaction(["cards"], "readwrite");
  var store = transaction.objectStore("cards");
  // データの書き込み
  var cursorRequest = store.put(storedCard);

  // 以下、成功時・エラー時の処理
}

これなら、トランザクション開始から書き込みまでが同期的に行われるため問題ありません。

復号時には、特にこのような注意は必要ありません。また、元々IndexedDBからのデータ読み取りは結果が非同期で返されるので、CardBookのデータ読み込み処理もその前提で設計されていました。そのため、IndexedDBから返ってきたデータを非同期で復号した上で返却するという処理を挟み込んでも、CardBookのデータ読み込み処理全体としてはインターフェースを変えずに済んだのでした。

パスワード入力を求める方式にしなかった理由

CardBookのローカルデータ暗号化では、暗号化・復号に使う共通鍵は、バックグラウンドで自動生成した物を暗黙的に使い、鍵自体をユーザープロファイル内に保存する形としました。

「暗号化されたデータと鍵を同じ場所に置いておくのでは、暗号化の意味が無いじゃないか」と思うでしょうか? 実際、変更をフィードバックした際にもCardBookプロジェクトの開発者の方からも「パスワード入力を求める方式にした方がいいのではないか?」という質問がありました。Web Crypto APIの機能を使うとユーザーが入力したパスワードから秘密鍵を作る事もできる(Web Crypto APIの解説記事の「パスワードを鍵に変換する」の項をご参照下さい)のに、そうしなかったのは何故でしょうか。

ここで一旦、パスワードの安全な運用という事を考えてみましょう。パスワードを自分で記憶しておきその都度入力するという方式は、一見すると安全なように思えます。しかしながら、運用の仕方によっては却って危険になる場合があります。

  • パスワード入力には、肩越しに入力の様子を覗き見るショルダーハックや、キーの入力を監視するキーロガーなどによってパスワードを盗み取られるリスクがあります。パスワード入力の頻度が多ければ多いほど、このリスクは高まります。

  • 人間の記憶容量には限りがあるため、あまり複雑なパスワードを複数覚えるという事はできません。そのために「同じパスワードを何度も使い回す」「推測が容易なパスワードにする」といった事が行われてしまい、そうなると却って危険な状態となります。

  • 定期的なパスワード変更にも、同様の問題があります

これらの理由から、パスワードの入力は「複雑で憶えにくいパスワードを1つだけ覚える」「それをマスターパスワードとして使い、それ以外はパスワードマネージャに憶えさせる」という運用にするのが比較的安全だというのが現在の定説となっています1

「企業でThunderbirdを使う」というシチュエーションでは、「PCのログオン」「受信メールサーバーの認証」「送信メールサーバーの認証」などでそれぞれパスワードの入力が発生する可能性があります。という事は、ここにさらに「ローカルデータの復号」のためのパスワードが加わるというのは、さすがに実運用を妨げるレベルの煩わしさでしょう。かといって、他の部分ではパスワードを使用していないのにここでだけパスワードの入力を求める、というアンバランスな運用も考えにくいです。そういった事を考慮した結果として、CardBookのローカルデータ暗号化は現在比較的安全とされている運用を想定し、

  • 秘密鍵は、自動生成した物をJWK形式でエクスポートし、「長いパスワード文字列」の一種としてThunderbirdのパスワードマネージャに記憶させる。

  • 秘密鍵の保護が必要な場合は、使用者が任意でThunderbird本体のマスターパスワード機能を有効化する。

というポリシーを採用する事にしたのでした。

まとめ

Thunderbird用アドオンのCardBookに対して行った、ローカルデータの暗号化対応の概要をご紹介しました。

当社では、一般に公開されているFirefox用アドオン・Thunderbird用アドオンをはじめとした様々なフリーソフトウェア・OSSについて機能追加・改造のご依頼を承っております。また、成果をアップストリームに還元しても差し支えがないケースでは、積極的に還元を行うようにしています。自社でフリーソフトウェア・OSSを採用したいが少しだけ要件に合わない、という事でお悩みの場合には、メールフォームからお問い合わせ下さい。

  1. ただし、これはあくまで現時点での話です。技術の進歩や、この分野での研究が進む事などによって、「最もマシ」なやり方は変わっていく可能性があります。