Cryptomatorの暗号化ファイルを自力で復号してみた
冬休みの自由研究として作ったこのツールを紹介する記事です。
はじめに
皆さん、クラウドストレージ使ってますか?
大切なデータをDropboxなどのクラウドストレージに保存しておけば、手元のストレージが壊れてデータが失われてしまうリスクを大きく軽減することができます。しかしクラウドストレージはクラウドストレージで、事業者側にデータを検閲されてデータやアカウントを消されてしまいアクセスできなくなるという別のリスクが存在します。
このような検閲リスクを回避してクラウドストレージを利用する方法として、データを手元で暗号化してストレージに置くという方法があります。これによって、事業者側がユーザーのデータを見ることが原理的に不可能になるため、検閲のリスクを限りなく小さくすることができますね。ただし、事業者側でデータに対して何の処理もできないということでもあるので、全文検索やサムネイルの生成といった便利な機能が使えない、というトレードオフは存在します。
MEGAやpCloud、icedriveなど、クライアントサイド暗号化を売りにしているサービスも増えてきました。自分もpCloudの暗号化フォルダをDropboxのバックアップとして利用しています。しかし、事業者の提供しているアプリでクライアントサイド暗号化していると主張されても、それが本当か確かめるのは難しいです。やはり、オープンソースのツールで暗号化するのが一番安心できますね。
自分は以前VeraCryptの暗号化ディスクイメージをDropbox上に置く運用をしていました。しかし、暗号化ディスクの巨大なイメージを丸ごとローカルに持ってこないと中身にアクセスできないため、「ファイルは基本的にオンラインのみに置いておき、必要な時に初めてローカルにダウンロードする」というクラウドストレージの運用と相性が悪く、最近Cryptomatorに完全乗り換えをしました。
Cryptomatorとは
オープンソースの暗号化ソフトウェアです。使い方のイメージは公式HPを見てもらうのが一番早いでしょう。
Cryptomatorの暗号化ストレージはVaultと呼ばれます。Vaultの実体はメタデータの書かれたテキストファイルと暗号化されたバイナリファイルの集合です。
.
├── d
│ ├── AM
│ │ └── EEPDUPBBPTCPXKLBNC7ALEHGZBFHTB
│ │ ├── oicNldmCJ-bXX3Vfnk5P-Q1omUk=.c9r
│ │ │ └── dir.c9r
│ │ ├── 5weNNO8T59d8MKsYrZBAI1zK-Lsq01rYFZSL.c9r
│ │ ├── BkstEI_SlImSSPKWtgXOWCy_3CJNB5pAoQ==.c9r
│ │ ├── dirid.c9r
│ │ └── pwwBymkWRpB_NuVts3jke98Kq4JTgweSLg==.c9r
│ └── JN
│ └── WLGNJKLX7IMEE4PKVISNJSHPPAWNSL
│ ├── 1rh_btf-sfOo5PRnbBYUulkkFSQC5A9mEL7Sey6kdQ==.c9r
│ └── dirid.c9r
├── IMPORTANT.rtf
├── masterkey.cryptomator
├── masterkey.cryptomator.AD16228F.bkup
├── vault.cryptomator
└── vault.cryptomator.8217D379.bkup
.c9r
ファイルが暗号化されたバイナリです。Cryptomatorでは暗号化はファイル単位で行われます。
そのため、どのくらいの容量のファイルが何個あるかという情報は外から見えてしまいますが、ファイル単位でローカルにデータを持ってこれるので使い勝手が良いです。
Cryptomatorを利用すると、Vaultに対して仮想ファイルシステム経由でアクセスすることができ、通常のファイルシステムと同様にファイルの読み書きを行うことができます
$ pwd
/Users/hooto/Library/Application Support/Cryptomator/mnt/sample
$ tree
.
├── test
│ └── Cryptomator.txt
├── Hello.txt
└── WELCOME.rtf
Cryptomatorの暗号化アーキテクチャ
そんな中、ある日突然不安に襲われました。
「CryptomatorはOSSだから動作が見えて安心と言いつつ暗号化のアーキテクチャを理解していないのであれば、プロプライエタリなツールを使っているのと結局同じことでは?」
「もし突然Cryptomatorのソースコードが世界から消滅してしまったとして、暗号化されたVaultからデータを救出できるのか?」
そこで、Cryptomatorの暗号化の仕組みがまとまっているSecurity Architectureのページを眺めてみることにしました。読んでみると、Cryptomatorは標準化された暗号技術の組み合わせによって実現されていることが分かります。
であれば、自分で復号化ツールを書くこともできそうです。それによってCryptomatorのアーキテクチャを 「完全理解」 することで、安心して使うことができます。というわけでつくりました。
復号の流れ
公式ドキュメントには暗号化の流れが書かれているので、逆に辿っていけば復号の方法も自ずと分かります。ただし、ドキュメントは一部のパラメータが省略されていたり、そもそも嘘が書いてあったりしたので、結局本家のコードを何度も見に行くことになりました。
CryptomatorはJavaで書かれていますが、Javaを読むのは久しぶりだったので辛かった...
基本的に流れは以下のようになります。ドキュメントに記載がなくて自分が苦しんだ部分は強調して書いています。
なお、自分は暗号に関しては完全な素人なので、変な記述があるかもしれないですがご了承ください。
まずVaultのメタデータを解析してPrimary KeyとMAC Keyを得ます
- Vaultのルートディレクトリにある
vault.cryptomator
を読んで、中身の署名付きJTWを署名を無視してデコードする - JWTヘッダの
kid
を見て、マスターキーファイル(JSON)のパスを調べる - マスターキーファイルからscryptのパラメータとsalt、ラッピングされたPrimary KeyとMAC Keyを読み込む (ただしscryptのパラメータpとdkLenは書かれていない。それぞれ1と32で固定)
- ユーザーパスワードからscryptを用いてKey-Encryption-Key (KEK) を導出
- RFC3394に従い、KEKを用いてPrimary KeyとMAC Keyを求める
- Primary KeyとMAC Keyをconcatしたものをsecretとして、JWTの署名を検証する (ドキュメントにはマスターキーファイルを使って認証する、としか書かれておらず混乱しました。このあたりは暗号に慣れてる人ならすぐ分かるものなんでしょうか...)
暗号化されたファイルはディレクトリごとにまとめて置かれています。
各ディレクトリにユニークなIDが振られており、AES-SIVで暗号化されたIDのSHA1ハッシュをbase32でデコードした30文字からデータの置き場が決まります。
ルートディレクトリのIDは空文字と決まっているので、まずルートディレクトリに対応するハッシュを計算し、そこに置かれている.c9r
ファイルを順次復号していくことになります。
dirIdHash := base32(sha1(aesSiv(dirId, null, encryptionMasterKey, macMasterKey)))
dirPath := vaultRoot + '/d/' + substr(dirIdHash, 0, 2) + '/' + substr(dirIdHash, 2, 30)
サブディレクトリは、ディレクトリIDの書かれたテキストファイルによって表現されます。そのIDからハッシュを計算することで、サブディレクトリの中身を復号するためにどこを見にいけば良いのか分かります。復号結果として親子関係にあるディレクトリであっても、Vaultの中では全て兄弟関係にあるということですね。
さて、肝心のファイルの復号についてです。Cryptomatorでは1ファイルにつき1つの.c9r
ファイルが生成されます。元ファイルの名前と中身がそれぞれ.c9r
ファイルの名前と中身に暗号化されています。
まずファイル名を復号します。ファイル名はAES-SIVによって暗号化されたあとBase64URLでエンコードされています。暗号化時に親ディレクトリのIDをAssociated Dataとして使うので、基本的にルートから順番に辿らないと復号できません。[1]
ファイルはそれぞれ異なるContent Keyを使って暗号化されています。Content KeyはPrimary Keyを使ってAES-GCMによって暗号化され、68バイトのヘッダに書かれています。
Content Keyが復号できたら、あとはこれを使ってファイルをチャンクごとに復号していきます。暗号化は追加認証データ(ADD)つきのAES-GCMです。各チャンクの大きさは暗号文32KiB、Nonceの12バイト、tagの16バイトを合わせた1024*32+12+16
バイトになります。
ADDとしては、BigEndianのチャンク番号とヘッダのNonce(ファイルの先頭12バイト)をつなげたものを使用します。 (チャンク番号について、ドキュメントには32bit big endian integerと書かれているのですが、実装を読みに行ったら64bitでした。ここが最大のハマりポイントだった...)
このほかに、暗号化の結果ファイル名が長くなりすぎた場合にいい感じにするための処理などがあったりするのですが、そのあたりはドキュメントのほうを参照してください。
おわりに
Q: 結局AES関連の実装はライブラリを使っているのだから、完全理解したとは言えないのでは?
A: はい...
-
これだとファイルの破損に弱くなってしまうので、実は各ディレクトリに
dirid.c9r
というファイルが置かれていて、これを復号してIDを得ることもできるようになっています。 ↩︎
Discussion