PageCryptベースのHTML暗号化を確認する
HTMLページをパスワード保護したい場合、Basic認証やDigest認証などサーバー設定が必要な手法を使うのが一般的です。 しかし、"静的ホスティング上で完結させたい"といったシーンでは、PageCrypt[1]のようにクライアント側で復号する仕組みが有用です。
PageCryptは、HTMLファイルを事前に暗号化し、JavaScriptで復号化することで、サーバーの追加設定を不要にしています。
本記事では、PythonとJavaScriptでPageCrypt相当の処理を再現しながら、AES-GCM暗号とPBKDF2(あるいはargon2)によるキー導出を解説します。
Pythonの暗号化ライブラリには pycryptodome
[2] を、JavaScriptの復号には Web Crypto API
を使用しています。
- [全体の流れ]
- [AES-GCM による暗号化/復号化]
- [PBKDF2]
- [argon2]
- [まとめ]
- [(おまけ) 追加認証データ(AAD)]
- [(おまけ) 画像の対応]
- [参考]
全体の流れ
以下のようなフローで、パスワードと平文(HTML)を用いて、AES-GCMで暗号化します。
- 平文とパスワードを用意する
- PBKDF2を用いてパスワードから鍵(key)を生成
- 生成した鍵を利用して、AES-BCMで暗号化を行う
- salt、IV[3]、暗号文と認証タグ(tag)を1つにまとめる
上記フローからもわかるように、パスワードを直接AESの鍵として使わないです。 あくまで、パスワードを利用し、PBKDF2を通して鍵を作成し、安全に暗号化する設計になっています。
実際の運用では「salt」「IV」「暗号文」「認証タグ」の4セットをHTMLに埋め込みます。 ユーザーがページを開いてパスワードを入力すると、JavaScriptがそれらの情報を使って復号処理を行い、ページを展開する仕組みです。
AES-GCM による暗号化/復号化
AESにはCBCやCTRなど複数のモードがありますが、ここではAES-GCM(Galois/Counter Mode)を利用します。
AES-GCMは、認証タグ(MAC)が自動的に生成・検証される「認証付き暗号」であり、改ざん検知が標準で行えるのが特徴です。
まずは「hello」を暗号化・復号する簡単なサンプルコードを示します。
=
=
, =
return + +
=
return
= b
= b*16
=
=
=
=
=
# Derived Key: 30303030303030303030303030303030
# Encrypted data: b'7GYaE4mekELCvfQHhONbxe7JSejHy/nW/LfkQ5Fu9ks8'
# Decrypted data: b'hello'
"hello"が暗号化され、復号により元通り"hello"に戻っています。
改ざん検知の例
暗号文の一部を改ざんした場合、認証タグとの照合が失敗し、.decrypt_and_verify()
はエラーを吐きます。
試しに暗号文の一部を書き換えます。
= b*16
=
^= 0x01 # 🔥
# Decrypted data: b'hello'
# Error: MAC check failed
このように、改ざんを検知して復号できなくなります。
JavaScriptで復号する
上記Pythonで暗号化したデータを、JavaScriptのWeb Crypto APIで復号する例です。
;
Decrypted data: hello
問題なく復号できています。
PBKDF2
AES-GCMで利用する鍵の生成には、PBKDF2を使うのが一般的です。
PBKDF2はパスワードとsaltを用い、ハッシュ計算を複数回繰り返すことで、攻撃を困難にする仕組みです。
AES鍵は16バイト(128ビット)や32バイト(256ビット)の長さが必要ですが、ユーザー入力のパスワードは長さや強度がバラバラなので、PBKDF2で安全に鍵を導出します。
以下の例ではPBKDF2を10万回反復し、AES-256(32バイト)の鍵を導出しています。
return
暗号化結果には暗号文の先頭にsaltも加えて出力し、復号時はそれを取り出して同じ鍵導出を行います。
=
= b
=
=
=
=
# Salt: 8094f8bc4474837fbe7fd0a6b3777cd1
# Derived Key: 93c9bd309716836ce01b954297e4ca99a5c5d30a080fe3b0df09dbe92b1e1963
# Base64 Encoded Data: b'gJT4vER0g3++f9Cms3d80ZX2181OikMw7IADnsZlg11rqY8wqVrOsY2cCnANGWFq1g=='
JavaScript側で同じくPBKDF2を使えば復号できます。
;
Decrypted data: hello
argon2
オリジナルのPageCryptにはありませんが、argon2を使ったキー導出に差し替えることもできます。
2015年のパスワードハッシュコンペティション[4]の優勝者であるArgon2は、PBKDF2の代替として利用可能です。 たとえば、BitwardenだとArgon2idを実装しています[5]。
以下の例でも2idを利用しています。
Pythonではargon2-cffi
[6]、JavaScriptではargon2-browser
[7]を利用します。
基本的な部分はPBKDF2の場合と同じで、以下のように鍵を生成します。
return
# Salt: d10b6d0d2f02808426efaedfc2453896
# Derived Key: 3d7b67a07cb91dce957eda7b2903502de7de8e68e0fa9f616278afcfe3f8d74d
# Base64 Encoded Data: b'0QttDS8CgIQm767fwkU4lvgFvJZ95BYFJPjPa/MKXwKh0LwEuZeye2BSzMiCD5r7ZQ=='
JavaScript側では argon2-browser
をCDNで読み込み、同様にキーを生成することで復号可能です。
Decrypted data: hello
まとめ
PageCryptのように、「AES-GCM + PBKDF2 でHTMLを暗号化し、クライアント側で復号する」という流れを確認しました。
本家PageCrypt実装はPBKDF2のみですが、argon2にも差し替え可能であり、必要に応じて派生させられます。
静的ページで手軽にパスワード保護をしたいときに役立ちます。
(おまけ) 追加認証データ(AAD)
AES-GCMでは、「AAD(Additional Authenticated Data)」という領域に、暗号化するデータ本体以外のメタデータを載せることができます。
AADは、暗号の強さ自体には影響しませんが、通信の安全性には関係します。 例えば、プロトコルのバージョン番号や受信者のアドレスなどを AAD に含めないと、攻撃者が自由に書き換えることができてしまいます。 その結果、古い安全性の低いバージョンのプロトコルに変更されるなどのリスクが生じます。
ただ、GCM の動作を変えるような情報は、認証タグの検証より前にチェックしておく方が安全です[8]。
= b*16
= b
= b
=
=
, =
=
=
(おまけ) 画像の対応
HTML内の画像も暗号化したい場合、画像をbase64で埋め込む方法があります。
=
=
= f
あとは <img src="data:image/png;base64,iVBORw0KGg..." />
という形でHTMLに埋め込めば、ひとつのHTMLファイルで完結します。
参考
12バイトの長さのIV(nonce)が、"NIST SP 800-38D"によって推奨されているようです。
WASM実装のようです。{https://cdnjs.com/libraries/argon2-browser}