Astro でパスワード保護されたレジュメページを作る
自分のサイトにレジュメを置きたかった。必要なときに URL とパスワードを伝えるだけで見てもらえると楽だし、内容の更新もプッシュするだけで済む。
ただ、誰でも見られる状態にはしたくない。Astro は静的サイトジェネレーターなのでサーバーサイド認証はない。Cloudflare Access という手もあったが、閲覧者にメール認証を通してもらう必要がある。パスワード1つで済むほうがシンプルでいい。
やったこと
ビルド時にレジュメの HTML を AES-GCM で暗号化して、静的 HTML に base64 文字列として埋め込む。閲覧者がパスワードを入力すると、ブラウザの Web Crypto API で復号して表示する。
ソースコードを開いても暗号化された文字列しか見えない。noindex も付けているので検索エンジンにも引っかからない。
暗号化
暗号化のユーティリティは src/lib/encrypt.ts に切り出した。
export async function encrypt(plaintext: string, password: string) {
const salt = crypto.getRandomValues(new Uint8Array(16));
// PBKDF2 でパスワードから鍵を導出
const key = await crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt'],
);
// AES-GCM で暗号化
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoder.encode(plaintext),
);
// ...
}
Astro のフロントマターはビルド時に Node.js で実行されるので、ここで暗号化を呼べる。
---
const password = import.meta.env.RESUME_PASSWORD;
const content = fs.readFileSync('src/data/resume-content.html', 'utf-8');
const encrypted = await encrypt(content, password);
---
パスワードは環境変数 RESUME_PASSWORD で管理している。Cloudflare Pages に設定し、Terraform でも管理している。
復号
ブラウザ側では同じ PBKDF2 + AES-GCM のパラメータで復号する。パスワードの照合は SHA-256 ハッシュで先にやって、間違っていたらそこで弾く。
リロードのたびにパスワードを求められるのは面倒なので、sessionStorage に保持して自動復号するようにした。タブを閉じれば消える。
更新フロー
src/data/resume-content.html を編集してプッシュするだけ。ビルド時に勝手に暗号化される。経験年数も new Date().getFullYear() - 2016 で動的に計算しているので、年が変わっても放置でいい。