Evernote と E2EE(エンドツーエンド暗号化)機能の簡単な解説

3 min read読了の目安(約3300字

本稿は、Evernote が行っている E2EE(エンド・ツー・エンド暗号化) について私的に整理する目的で書かれています。

リバースエンジニアリングによるサービス解析ではなく、あくまで公になっているドキュメントや、アプリの見た目の挙動を元に仕様を推測し、参考とするものです。今回紹介した暗号化手法の妥当性や安全性を説明するものではありませんのでご留意下さい。セキュリティハック的な意味合いもありません。

公式ドキュメントによる説明

https://evernote.com/intl/jp/security/tips

上記公式ドキュメントによると、Evernote のデスクトップ版アプリには E2EE が搭載されているとのことです。

Evernote のデスクトップ版アプリ(Mac・Windows)を使用している場合は、ノートに含まれる任意のテキストを暗号化し、暗号解除にパスフレーズを必要にすることで、プライベートな情報をより安全に保護できます。このエンドツーエンド暗号化 (E2EE) 機能を使用すると、暗号化を解除できるのはパスフレーズを知っている本人のみになります。Evernote は、パスフレーズまたは暗号鍵の情報を一切受信しません。したがって、ユーザがパスフレーズを忘れてしまっても、Evernote はデータを復元することができませんので、ご注意ください。
この機能が使用された場合、鍵長 128 ビットの AES 方式 (Advanced Encryption Standard) でテキストを暗号化します。この暗号化キーはお客様のパスフレーズから生成されており、それには固有の salt 値および SHA-256 ハッシュを 50,000 回通した PBKDF2 関数を利用します。このキーは初期化ベクトルと共に、暗号ブロック連鎖 (CBC) モードでデータを暗号化するために使用されます。

Evernote の E2EE 機能

Evernote の E2EE に馴染みがない方向けに簡単に機能を紹介します。

  1. ノート(ドキュメント)全体ではなく、ノート内の特定のテキストを暗号化する機能
  2. 暗号化には、任意のパスフレーズの入力が必要
  3. スマホアプリではできない模様

編集中のコンテクストメニューから暗号化可能。

before after

※ ちなみに Android アプリでは、復号のみできることを確認しました。暗号化はできないようです。今回の仕様の場合、復号できる以上は暗号化もできるはずなので、暗号化ができないのはたんに UX を損ねると判断したか、需要がないと判断したからでしょう。

暗号化の仕様 (推測含む)

暗号化方式および仕様については「この機能が使用された場合、鍵長 128 ビットの...」以下に書かれているとおりですが、箇条書きに編集しなおすと以下のフローで暗号化が行われていると考えられます。

  1. ユーザーが任意のパスフレーズを決定する。以下サービスの動き。
  2. [1] のパスフレーズを 鍵導出関数 PBKDF2 に与え、鍵(Derived Key)を生成する。
  3. [2] で生成した鍵をもちいて、AES-128 の共通鍵を生成する
  4. テキストの暗号化には [3] で生成した共通鍵を利用する。
  5. テキストの復号には、同様に [3] で生成した共通鍵を利用する。

パスフレーズおよび各種鍵をサーバーサイドに送信しないことで、E2EE を達成している(と少なくとも表明している)と考えられます。

考察

Evernote の E2EE のミソは、パスフレーズが同じであれば、鍵導出関数は同じ鍵を生成する(ようにしている)という点です。

E2EE において、暗号鍵をユーザーにどう運搬させるか(あるいはさせないか)は難しい検討点です。Evernote の場合、「暗号化および復号時に、明示的にパスフレーズを入力させる」という点で利便性を犠牲にする代わりに、「パスフレーズを忘れなければ復号が可能(裏側で生成される鍵のことは意識しなくてよい)」という利便性を得ることを選択したのかなと推測します。

アプリの挙動を観察するに、PBKDF2 が鍵を生成した時点でパスフレーズは破棄されているようです。毎回パスフレーズの入力を求められる風なので導出鍵のキャッシュもしていないのかも知れません。

Evernote は自動でクラウドに同期されているため、テキスト入力 -> 平文でクラウドに保存 -> テキストのE2EE という順番になると最後に暗号化する意味が薄れてしまう気もしますが、Evernote としてはユーザーからの要求に答えるためにこのような仕様に落ち着いたのでは、と推測します。ドキュメント全体を自動的に暗号化して保存する仕様も一定の需要はありそうですが、Evernote の得意とする検索性の高さと競合するため、プロダクトの方向性として採用しないという決定が下っていても不思議ではありません。特に明記はされていないようでしたが、暗号化したテキストはEvernote内 インデックスの対象外になると思われます(まぁ当然ですが)。

暗号化するプロセス毎にパスフレーズを変えることもできる(おそらく紛失リスクの観点から非推奨と注意書きが添えられていた)のはユニークな仕様かも知れません。

おまけ : Go で PBKDF2

Go の場合、 "golang.org/x/crypto/pbkdf2"を利用することで PBKDF2 に基づいた鍵導出が可能です。コードの一部ですがこんな感じ。

import (
	"golang.org/x/crypto/pbkdf2"
	"crypto/sha256"
)

func getKey() (key []byte) {
	key = pbkdf2.Key([]byte(passPhrase), []byte(salt), 50000, 16, sha256.New)
	return
}

今回はパスフレーズから毎回同じ鍵を取得したいので、passPhrase 文字列および salt 文字列は変更できません。

この鍵を aes.NewCipher() に渡せば AES 共通鍵を生成可能です。

なお、もしも Evernote の実装を活かしつつ改善するなら、PBKDF2 の代わりに Argon2id を、AES-128 CBC の代わりに AES-256 GCM を採用するといいのかなと思いました。

Argon2id を採用した場合の Go のコードはこんな感じ。

func getKey() (key []byte) {
	key = argon2.IDKey([]byte(passPhrase), []byte(salt), 3, 32*1024, 4, 32)
	return
}

golang.org/x/crypto/argon2 に実装があり升。

ということで、Evernote 編は以上です。他のサービスのE2EE仕様も調べてみるかも知れません。