Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8900

データをパスワードだけで暗号化し、ファイル化して保存することも可能なnpmパッケージを作った話

$
0
0
いい感じのものが無かったので作りました。 @sounisi5011/encrypted-archive - npm 宣伝も兼ねていますが、どちらかというと暗号技術について詳しい方の意見とか指摘とかが欲しいなという下心が強いです。 特徴 2021年現在、安全な(と考えられている)暗号アルゴリズムを採用しています。具体的にはAES-GCM 256ビットとChaCha20-Poly1305です。 パスワードは最新の鍵導出関数Argon2を使って変換されています。 ファイル化することを考えて、圧縮アルゴリズムにも対応しています。 生成されたデータに復号に必要な全ての情報が含まれているので、復号するときはパスワードだけしか要りません。ファイルに書き込んで保存すれば、KeePassのデータベースファイルのように、時間が経った後に他のデバイスで復号することも可能(なはず)。 IVを乱数ではなくカウンターで生成します。 デカいデータやファイルも暗号化・復号できます(手元にあった47GBのファイルの暗号化と復号に成功しています) 用途 バックアップのためクラウドストレージにアップロードするファイルの暗号化に 公開URLにうpしてアクセスしたいが、第三者には絶対に内容を見られたくない機密データに アプリケーション固有のデータファイルに なぜ作ったか Node.js組み込みのcryptoモジュールには、暗号化を行うためのcreateCipheriv関数が存在します。しかし、「この関数を使えば簡単に暗号化できるか?」というとそう簡単な事はなく、 適切な暗号化アルゴリズムを選ばなければならない ちゃんとしたkey(パスワードに相当しますが、そのまま渡してはいけません)を指定しなければならない iv(Initialization Vector)を指定しなければならない 直感どおりに動かすためには認証タグを暗号化の後に取得し、復号の前に指定しなければならない これらの情報を復号するときのために保持しておかなければならない など、やることが多く、知識なしで扱うのはハードルが高すぎます。 加えて、巨大なデータ(ファイルなど)を変換する場合、Node.jsではStreamオブジェクトを使うことになりますが、これに対応する適切な関数やクラスも用意しなければなりません。 同じような事を考えた先人はいたようで、探したところ以下のパッケージを見つけました。 string-crypto - npm しかしこのパッケージには、 Streamオブジェクトに対応していない IVを乱数で生成している 鍵導出関数にPBKDF2を使用している 鍵導出関数のパラメーターをクラス作成時に指定しなければ(つまり、別の場所で保持しておかなければ)復号できない などの不満点がありました。他にもいろいろ探したものの、目的に合うものは無く、結局自作してしまったというわけです。 生成されるデータ構造 @sounisi5011/encrypted-archiveが「archive」の名を持つのには理由があります。文字通り、そのままアーカイブ・ファイルにできるほどの、後方互換性を維持した全部入りの暗号化データを生成するからです。データ構造に関しては以下のドキュメントに図入りで簡単に書いています。 Structure of the encrypted archive at encrypted-archive-v0.0.2 · sounisi5011/npm-packages ここでその特徴を簡単に箇条書きすると、 データを複数のチャンクに区切り、それぞれを暗号化することで、巨大なデータもメモリに展開せずに暗号化・復号できる 以下の情報を全て含んでいるため、復号時にはパスワードだけが必要 暗号化アルゴリズムの種別 IVのバイナリデータ 認証タグのバイナリデータ 鍵導出関数が生成するべきkeyの長さ keyの生成で用いるsaltのバイナリデータ 鍵導出関数のパラメーター もし行っていれば、圧縮アルゴリズムの種別 これらのデータをunsigned varintとProtocol Buffersを用いてエンコードしているため、将来の拡張にも備え、後方互換性を維持可能(なはず) これを作った動機の一つは、「静的サイトジェネレーターで短縮URLを生成する時に、一度作ったURLの一覧を暗号化したファイルとしてうpしたい」というものでした。暗号化の時にどのようなオプションを指定した場合でも、復号の時に必要なものはパスワードだけです。 暗号化技術に関する話 書きたいことを先に書いてしまったせいで、前提になる情報がまるで出せていませんでしたね。おそらく詳しい人以外は、頭の中が「なぜIVをランダムに生成してはいけないのか?」「鍵導出関数って何?」「keyにパスワードを渡しちゃダメなの?」「認証タグってなんだよ」みたいな疑問でいっぱいだと思います。ここから、私が調べて得た知識の範囲で解説します。 適切な暗号化アルゴリズムの選択と認証タグについて 私が調べた範囲で得た結論から言えば、以下の2種類です。 AES-GCM 256bit ChaCha20-Poly1305 これ以外にも、AES-CTRなどがありますが、それらは認証付き暗号(AEAD)ではありません。 どういうことか?調べ始めた頃の私も勘違いしていたのですが、「暗号化」と「暗号に使ったパスワードが合っているかどうか」は全く別の機能です。 例えば、AES-CTRを用いて、あるデータを暗号文に変換したとします。その暗号文は、一見するとランダムなバイナリデータです。これを、誤ったkeyで復号しようとした場合、なんとエラーが発生すること無く成功します。ただし、「復号」されたデータは、相変わらず意味も読めないバイナリデータです。元の暗号文ではありません。暗号化は数学的な処理の一種であり、ただデータを変換する行為でしかないため、結果が正しいかどうかも、keyが合致するかどうかも無関係なのです。 「keyが合っている」かどうかは、暗号文とは別に、認証タグ(MAC、メッセージ認証コード)というものを追加することで行われます。私もあまりよく分かっていないのですが、これを使うことで、「keyが誤っていた場合に復号が失敗する」直感的な動作になります。また、「暗号文が弄くられた場合にエラーにする」という、大事な機能もあります。 認証タグの生成は別の機能ですが、暗号文の後ろに追加することで、求める暗号化が可能になります。しかし、暗号と認証タグも、合わない種類や不適切な構造で組み合わせると脆弱になる場合があります。うっかり忘れたりするかもしれません。そういう事を防止するため、この2つがひとまとめになったものが、前述の認証付き暗号です。 なお、認証付き暗号を使用する場合は、生成される認証タグが復号のときにも必要になります。よって、認証タグもデータの一部に含めなければなりません。 keyにパスワードを渡してはいけない理由と鍵導出関数 調べても分からなかっため、teratailで質問しました。 セキュリティー - AES-256 GCMに渡すkeyに、パスワードそのものではなく、鍵導出関数(PBKDF2など)で生成したハッシュ値を指定する理由は?|teratail その結果得られた解答が非常に納得できたため、@sounisi5011/encrypted-archiveでも常にパスワードを鍵導出関数で変換するようにしています。 まず、なぜパスワードを渡してはいけないのか?簡単に言うと、パスワードは乱数よりも強度が弱いからです。人間が打つパスワードは、覚えやすいようある程度規則性があったり、短かったりします。このため、 よくありがちなパスワードで総当り計算をすることにより、暗号文を解読できてしまう可能性がある 単純にkeyが短くなり、暗号文の強度が下がる 危険性があります。このため、パスワードと乱数(salt)を元に、十分な長さのランダム(に見える)データを生成し、これをkeyに渡す必要があります。 任意の長さのデータを変換する方法としてよくあるのはハッシュ関数ですが、ハッシュ関数は高速に設計されているため、変換したところであまり意味はありません。変換に十分な時間がかかり(ゆえに総当り攻撃に時間がかかり)、かつ安全である変換方式が必要です。この目的で設計されたものが鍵導出関数になります。 鍵導出関数の種類には、以下のようなものがあります。 PBKDF2 ハッシュ関数を組み合わせて鍵導出関数にしたもの。Node.js組み込みのcryptoモジュールに入っている。現在も推奨されているものの、他のものと比較すると弱い。ウィキペディアにもその事が書かれている。 bcrypt PHPに入っていることで有名(だと勝手に思っている)。PBKDF2よりも強いが、ほとんどの実装が先頭の72バイトしか使わない。長いパスワードの意味が無くなってしまう。もっと良い選択肢があるのにこれを選ぶのはちょっと… scrypt bcryptの後に誕生したもので、bcryptよりもいいらしいです(要出典)。 しかし調べると、「暗号通貨界隈でよく使われているせいで、ハードウェアで高速化した実装が存在する」らしいことが判明。 パスワードハッシュ:PBKDF2、Scrypt、Bcrypt、ARGON2 あまり選びたくはない… Argon2 最強の選択肢。最近誕生したもので、パスワードハッシュのコンペで優勝したらしい。以下2つの記事でも推奨されている。 パスワードハッシュ:PBKDF2、Scrypt、Bcrypt、ARGON2 Password Storage - OWASP Cheat Sheet Series また、KeePassも対応している。 Security - KeePass 調べた内容から判断し、新しいもの好きな性分による独断と偏見の選択も加味して@sounisi5011/encrypted-archiveではArgon2のみを採用するという思い切ったことをしています。 なぜIVを乱数で生成してはいけないのか 以下の記事にまとまっていますが、簡単に言うと、セキュリティの脆弱性につながるからです。 本当は怖いAES-GCMの話 - ぼちぼち日記 同じkeyで同じ値のIVを再利用してしまった場合、暗号化されたデータに、何か色々と数学的な処理を行うことで、改ざんが容易になってしまいます。前述した認証タグも「改ざん後の内容に合うもの」を生成できてしまうため、暗号文が正しい内容なのか、それとも改ざんされたものなのか、復号時に判定できません。 ここで、もし乱数を使用してしまった場合、同じ値の乱数が偶然生成されてしまう可能性が生じます。低いように思われますが、誕生日のパラドックスが例に出されているように、無視できるほど低い可能性というわけではありません。 私の理解では、可能な事はあくまで改ざんであり、暗号文の解読はできないっぽいです(間違ってたらすまぬ)。しかしいずれにせよ、正しい暗号文かどうかの判定ができなくなるため、この問題への対応は必要だと考えました。そのため@sounisi5011/encrypted-archiveでは、IVをカウンターで生成しています。具体的な実装は以下になります: packages/encrypted-archive/src/nonce.ts at encrypted-archive-v0.0.2 · sounisi5011/npm-packages JavaScript(ECMAScript)が生成するミリ秒単位のUnix時間と、0からカウントアップするデータを連結してします。JavaScriptのUnix時間は最大8,640,000,000,000,000ミリ秒で、これは「西暦275760年9月13日 午前0時0分0秒」を意味するため、このケタがあふれることはおそらく無いはずです。 また、この後ろに、5バイトのカウンター(現在サポートしている暗号化アルゴリズムはどちらも12バイトのIVが必要なため)が続きます。組み合わせの数は2^40-1で、これは1TiB(テビバイト)のデータ長と同じくらいの膨大な数です。小さなデータを大量に暗号化するような使い方(たとえば、IoT機器の通信データの暗号化など)をしたりしなければ、このケタがあふれることも無いはずですし、その前にUnix時間が変化してしまうでしょう。 まだやり残したこと こだわって作ったところ、なんと完成まで一ヶ月もかかってしまった@sounisi5011/encrypted-archiveですが、まだやり残した事は残っています。 簡単に使えるCLI 後々、「@sounisi5011/encrypted-archive-cli」なんて名前の別のnpmパッケージでも作って公開したいですネ。今のままでは、ちょっとファイルを変換したい時でもコードを書かなければならないので。 暗号化・復号の解析処理 例えばCLIを使う時に、暗号化や復号の進捗表示をしたり、「読み取ったメタデータ」「現在nチャンクを処理中」「現在のIV」などを出したいわけです。しかし現状、そういった細かい解析処理のために使えるものは一切提供されていません。CLIの作成に合わせて追加したいです。 keyを途中のチャンクで再生成する 現在の実装では、keyは暗号化の前に1度だけ生成されます。このkeyは暗号文全体で使われますが、途中でkeyを変更したほうが、暗号文の強度は上がるはずです(要出典)。 たとえ、攻撃者がkeyを1つ割り出したとしても、途中から別のkeyが使われていれば、暗号文全体が復号されてしまう可能性は低くなります。 もし、暗号強度が低下するようなデメリットが無ければ、途中のチャンクに新しいkeyの生成データを含め、以降のチャンクで新しいkeyを使用するのも良いかもしれません。 専用の継承クラスのErrorオブジェクトを投げる エラーが発生した場合、throw演算子でErrorオブジェクトを投げるのがJavaScriptのやり方です。このErrorオブジェクトを、ちゃんと継承した、細分化されたクラスにしたいです。instanceof演算子でErrorオブジェクトの種別を判定し、条件分岐しやすくしたいです。あと、もっと読みやすいエラーメッセージにもしたい。

Viewing all articles
Browse latest Browse all 8900

Trending Articles