パスキーに保存されている内容を詳しく説明します。
パスキー(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 | - | ✅ 可能 |
| 暗号化されたnsec | localStorage | ✅ AES-GCM | ✅ 可能 |
| device_seed | localStorage | ❌ 平文 | ✅ 可能 |
| 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攻撃で encryptedNsec と device_seed の両方を取得されると、パスキー認証なしで復号化できてしまう。
対策: パスキー認証をバイパスできないため、実際にはユーザーがページを開いた時点で nsec が復号化されているので、XSS で直接 state.sk を読み取る方が早い。
シナリオ2: 端末を盗まれた場合
攻撃者が端末を物理的に盗む
↓
localStorage から encryptedNsec と device_seed を取得
↓
パスキー認証が必要 👆
↓
❌ 指紋/PIN がないと認証できない
安全: パスキー認証をバイパスできないため、nsec は安全。
まとめ
パスキーに保存されているもの
- ✅ 公開鍵/秘密鍵(TPMに保存)
- ✅ Credential ID
- ✅ ユーザー情報(表示名など)
- ❌ nsec は保存されていない
localStorage に保存されているもの
- ✅ 暗号化されたnsec (最重要)
- ✅
device_seed(暗号化キーの元) - ✅ Credential ID
- ✅ 端末情報(表示用)
セキュリティのポイント
-
パスキーは認証のみ
- nsec の暗号化/復号化には使わない
- 認証が成功したら復号化を許可する
-
device_seed が鍵
- localStorage に平文で保存
- XSS攻撃で読み取られる可能性あり
- しかし、パスキー認証がないと意味がない
-
実際の脅威
- XSS で
state.skを直接読む方が簡単 - パスキー認証は「自動ログイン時の本人確認」に有効
- XSS で
パスキーは 便利さとセキュリティのバランス を取った仕組みです!🎉