nokakoiのパスキー利用メモ

パスキーに保存されている内容を詳しく説明します。

パスキー(WebAuthn Credential)に保存されているもの

🔑 パスキー自体(ブラウザ/OS に保存)

{
  // 1. 公開鍵
  publicKey: "...",  // サーバー側で検証に使う(今回は未使用)
  
  // 2. 秘密鍵
  privateKey: "...",  // TPM/Secure Enclaveに保存、取り出し不可
  
  // 3. Credential ID
  credentialId: "AQIDBAUGBwgJ...",  // パスキーを識別するID
  
  // 4. RP (Relying Party) 情報
  rpId: "nokakoi.com",  // または "localhost"
  
  // 5. ユーザー情報
  user: {
    id: "...",           // ランダムなバイト列
    name: "nokakoi_A3F2",
    displayName: "nokakoi - Windows (Chrome) [A3F2]"
  }
}

重要: パスキーには nsec は保存されていません


📦 localStorage に保存されているもの

appSettings (JSON)

{
  "preferredSigner": "nsec-passkey",           // ログイン方法
  "passkeyCredentialId": "AQIDBAUGBwgJ...",   // パスキーのID
  "passkeyEncryptedNsec": "1a2b3c4d5e6f...",  // 暗号化されたnsec ← ここ重要!
  "passkeyDeviceInfo": "Windows (Chrome)"      // 端末情報(表示用)
}

nokakoi_device_seed

"Xy7zK3mN9pQ2rS..."  // ランダムな32バイトのシード(Base64)

この device_seed から暗号化キーが生成され、nsec を暗号化/復号化します。

nokakoi_device_id

"A3F2"  // 4文字のランダムコード(表示用)

🔐 セキュリティの仕組み

暗号化の流れ

1. パスキー登録時:
   ┌─────────────────────────────────────┐
   │ nsec (平文)                         │
   │ "3a5f8c2d..."                       │
   └─────────────────────────────────────┘
              ↓
   device_seed から AES-GCMキー を生成
              ↓
   ┌─────────────────────────────────────┐
   │ 暗号化されたnsec                     │
   │ "1a2b3c4d5e6f..."                   │
   └─────────────────────────────────────┘
              ↓
   localStorage に保存 (passkeyEncryptedNsec)

復号化の流れ(自動ログイン時)

1. ページを開く
   ↓
2. パスキー認証を要求 👆
   ↓
3. 認証成功
   ↓
4. device_seed から AES-GCMキー を生成
   ↓
5. passkeyEncryptedNsec を復号化
   ↓
6. nsec (平文) を取得
   ↓
7. ログイン

🛡️ 何が守られているか

パスキーの役割

// パスキーは「認証」のみに使われる
authenticateWithPasskey(credentialId)
  ↓
  👆 指紋/PIN/顔認証
  ↓
  認証成功 → 復号化を許可

暗号化キーの保護

// device_seed は localStorage にある
device_seed = "Xy7zK3mN9pQ2rS..."

// でも、パスキー認証なしでは使えない
if (パスキー認証成功) {
  key = deriveKeyFromSeed(device_seed);
  nsec = decrypt(encryptedNsec, key);
}

📊 保存場所の比較

データ保存場所暗号化取り出し
パスキーの秘密鍵TPM/Secure Enclave✅ ハードウェア❌ 不可能
Credential IDブラウザ + localStorage-✅ 可能
暗号化されたnseclocalStorage✅ AES-GCM✅ 可能
device_seedlocalStorage❌ 平文✅ 可能
nsec(平文)保存されない--

⚠️ 攻撃シナリオと対策

シナリオ1: XSS攻撃で localStorage を読み取り

// 攻撃者が取得できるもの:
const encryptedNsec = localStorage.getItem('appSettings').passkeyEncryptedNsec;
const deviceSeed = localStorage.getItem('nokakoi_device_seed');

// しかし...
const key = deriveKeyFromSeed(deviceSeed);
const nsec = decrypt(encryptedNsec, key); // ← これは可能!

問題: device_seed が平文で保存されているため、XSS攻撃で encryptedNsecdevice_seed の両方を取得されると、パスキー認証なしで復号化できてしまう

対策: パスキー認証をバイパスできないため、実際にはユーザーがページを開いた時点で nsec が復号化されているので、XSS で直接 state.sk を読み取る方が早い。

シナリオ2: 端末を盗まれた場合

攻撃者が端末を物理的に盗む
  ↓
localStorage から encryptedNsec と device_seed を取得
  ↓
パスキー認証が必要 👆
  ↓
❌ 指紋/PIN がないと認証できない

安全: パスキー認証をバイパスできないため、nsec は安全。


まとめ

パスキーに保存されているもの

  • ✅ 公開鍵/秘密鍵(TPMに保存)
  • ✅ Credential ID
  • ✅ ユーザー情報(表示名など)
  • ❌ nsec は保存されていない

localStorage に保存されているもの

  • 暗号化されたnsec (最重要)
  • device_seed (暗号化キーの元)
  • ✅ Credential ID
  • ✅ 端末情報(表示用)

セキュリティのポイント

  1. パスキーは認証のみ

    • nsec の暗号化/復号化には使わない
    • 認証が成功したら復号化を許可する
  2. device_seed が鍵

    • localStorage に平文で保存
    • XSS攻撃で読み取られる可能性あり
    • しかし、パスキー認証がないと意味がない
  3. 実際の脅威

    • XSS で state.sk を直接読む方が簡単
    • パスキー認証は「自動ログイン時の本人確認」に有効

パスキーは 便利さとセキュリティのバランス を取った仕組みです!🎉