ota2000
2 min read

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 で動的に計算しているので、年が変わっても放置でいい。