Passkeys如何運作

Votes

Upvotes

0

Upvotes: 0

Downvotes

0

Downvotes: 0

Net votes

+0

Prompt #1

好, 請說明Passkeys 是如何運作, 如果可以也請提供範例程式碼, 最好在Windows, MacOS與Linux上面的都提供參考

Response #1

Passkeys(通行金鑰)被譽為「密碼殺手」,它的出現是為了徹底解決人類「記不住長密碼」與「密碼容易被盜」這兩個宿命。

簡單來說,Passkeys 是基於 FIDO2 / WebAuthn 標準的非對稱加密技術。你不再需要輸入字串,而是改用設備上的生物辨識(指紋、臉部掃描)或 PIN 碼來解鎖儲存在硬體中的秘密。

1. Passkeys 的核心運作原理:非對稱加密

Passkeys 不像密碼那樣是一個「共享的秘密」(你跟伺服器都知道同一個字串)。它使用的是公鑰 (Public Key)私鑰 (Private Key) 的組合:

  • 私鑰 (Private Key): 永遠儲存在你的裝置硬體安全模組中(如手機的 Secure Enclave 或電腦的 TPM),絕不離開裝置
  • 公鑰 (Public Key): 儲存在網站的伺服器上。

運作流程:

  1. 註冊時: 你的裝置生成一對金鑰,把公鑰傳給伺服器。
  2. 登入時(挑戰-響應機制):
  3. 伺服器發送一個隨機挑戰字串(Challenge)。
  4. 你的裝置使用私鑰對這個挑戰進行數位簽章。
  5. $Signature = sign(Challenge, PrivateKey)$
  6. 伺服器收到簽章後,用對應的公鑰驗證簽章是否正確。如果正確,就代表你擁有該私鑰,允許登入。

2. 不同作業系統的底層實現

雖然前端程式碼是一樣的,但不同作業系統在底層處理私鑰的方式有所不同:

作業系統 底層硬體技術 使用者驗證方式 跨裝置同步
Windows TPM (信賴平台模組) Windows Hello (臉部、指紋、PIN) 透過 Microsoft 帳戶 (較受限)
macOS / iOS Secure Enclave Touch ID / Face ID iCloud Keychain (無縫同步)
Linux TPM / libfido2 實體安全金鑰 (如 YubiKey) 視瀏覽器與硬體金鑰而定
Android TEE (可信執行環境) 指紋、圖形鎖 Google 密碼管理員

3. 範例程式碼 (WebAuthn API)

Passkeys 是透過瀏覽器的 navigator.credentials API 實現的。這段程式碼在 Windows、macOS、Linux 的現代瀏覽器中是通用的,因為瀏覽器會負責去叫用作業系統底層的驗證介面。

A. 註冊 Passkey (前端)

這是讓使用者建立新金鑰的過程。

async function registerPasskey() {
  // 1. 從後端取得註冊參數 (通常包含挑戰值與使用者資訊)
  const optionsFromServer = await fetch('/generate-registration-options');
  const options = await optionsFromServer.json();

  // 轉換格式 (Base64URL -> Uint8Array)
  options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));
  options.user.id = Uint8Array.from(atob(options.user.id), c => c.charCodeAt(0));

  try {
    // 2. 呼叫瀏覽器 API,這時會彈出系統視窗 (Windows Hello / Touch ID)
    const credential = await navigator.credentials.create({
      publicKey: options
    });

    // 3. 將生成的公鑰與憑證傳回後端儲存
    await fetch('/verify-registration', {
      method: 'POST',
      body: JSON.stringify(credential)
    });
    alert("註冊成功!");
  } catch (err) {
    console.error("註冊失敗", err);
  }
}

B. 使用 Passkey 登入 (前端)

這是當使用者下次回來時,證明自己身分的過程。

async function loginWithPasskey() {
  // 1. 從後端取得登入挑戰
  const optionsFromServer = await fetch('/generate-authentication-options');
  const options = await optionsFromServer.json();

  options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));

  try {
    // 2. 調用生物辨識驗證
    const assertion = await navigator.credentials.get({
      publicKey: options
    });

    // 3. 將簽章傳回後端驗證
    const result = await fetch('/verify-authentication', {
      method: 'POST',
      body: JSON.stringify(assertion)
    });

    if (result.ok) {
      window.location.href = "/dashboard";
    }
  } catch (err) {
    console.error("登入失敗", err);
  }
}

4. 為什麼這比之前的方案都安全?

  1. 抗釣魚 (Anti-Phishing): Passkey 會強制綁定網域名稱 (Origin Binding)。如果攻擊者做了一個假網站 g00gle.com,你的瀏覽器會發現網址不對,根本不會跳出 Passkey 驗證視窗。
  2. 伺服器洩漏無感: 伺服器只存公鑰。即便伺服器被駭,駭客拿到的公鑰也無法用來偽造簽章登入網站。
  3. 隱私保護: 生物辨識資訊(指紋等)只留在本地端,網站端只會收到一個「驗證成功」的數位簽章。

總結

Passkey 結合了 Local Storage 的存取速度硬體層級的防護力量。它解決了 XSS 竊取的問題(因為 JS 拿不到私鑰),也解決了密碼外洩的問題(因為根本沒密碼)。

如果你想在自己的專案中實作 Passkeys,通常最難的部分在於後端的「簽章驗證邏輯」。你需要我介紹一些流行的後端函式庫(如 Node.js 或 Python)來簡化這個過程嗎?

Prompt #2

好, 我需要Node.js及Python FastAPI兩種後端函示庫的範例, 請提供

Response #2

實作 Passkeys(WebAuthn)最困難的部分在於後端的二進位資料處理與加密簽章驗證。幸運的是,Node.js 與 Python 都有成熟的函式庫能幫你處理這些複雜細節。

在實作之前,請記住後端的核心任務只有兩個:

  1. 產生選項 (Options): 包含隨機的「挑戰值 (Challenge)」,防止重放攻擊。
  2. 驗證回應 (Verify): 驗證瀏覽器傳回的數位簽章是否與存儲的公鑰吻合。

1. Node.js 實作範例:使用 @simplewebauthn/server

這是目前 Node.js 生態系中最受歡迎、文件最齊全的函式庫。

安裝

npm install @simplewebauthn/server

核心邏輯

const {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} = require('@simplewebauthn/server');

// 1. 註冊第一步:產生註冊選項給前端
app.get('/generate-registration-options', async (req, res) => {
  const options = await generateRegistrationOptions({
    rpName: '我的安全網站',
    rpID: 'localhost',
    userID: 'user_123', // 應從資料庫獲取
    userName: 'user@example.com',
    attestationType: 'none',
    authenticatorSelection: {
      residentKey: 'required',
      userVerification: 'preferred',
    },
  });

  // 重要:挑戰值 (challenge) 必須存入 Session,稍後驗證時比對
  req.session.currentChallenge = options.challenge;
  res.json(options);
});

// 2. 註冊第二步:驗證前端傳回的公鑰
app.post('/verify-registration', async (req, res) => {
  const { body } = req;
  const expectedChallenge = req.session.currentChallenge;

  const verification = await verifyRegistrationResponse({
    response: body,
    expectedChallenge,
    expectedOrigin: 'http://localhost:3000',
    expectedRPID: 'localhost',
  });

  if (verification.verified) {
    // 將 verification.registrationInfo.credentialID 與 credentialPublicKey 存入資料庫
    res.json({ success: true });
  }
});

2. Python FastAPI 實作範例:使用 webauthn

Python 社群通常使用由 Duo Labs 維護的 webauthn 函式庫,它非常嚴謹且符合標準。

安裝

pip install webauthn fastapi uvicorn

核心邏輯

from fastapi import FastAPI, Request
from webauthn import (
    generate_registration_options,
    verify_registration_response,
    options_to_json,
)
from webauthn.helpers.structs import RegistrationCredential

app = FastAPI()

# 模擬資料庫與 Session
DB_USER_ID = "user_123"
RP_ID = "localhost"
RP_NAME = "My Python App"
temporary_storage = {}

@app.get("/generate-registration-options")
async def registration_options():
    options = generate_registration_options(
        rp_id=RP_ID,
        rp_name=RP_NAME,
        user_id=DB_USER_ID,
        user_name="user@example.com",
    )
    # 儲存挑戰值供後續驗證
    temporary_storage["challenge"] = options.challenge
    return options_to_json(options)

@app.post("/verify-registration")
async def verify_registration(credential: dict):
    expected_challenge = temporary_storage.get("challenge")

    try:
        verification = verify_registration_response(
            credential=credential,
            expected_challenge=expected_challenge,
            expected_origin="http://localhost:8000",
            expected_rp_id=RP_ID,
        )
        # 驗證成功後,將 verification.credential_id 和 public_key 存入資料庫
        return {"verified": True}
    except Exception as e:
        return {"verified": False, "error": str(e)}

實作時的關鍵細節 (陷阱題)

  1. RP ID (Relying Party ID):
  2. 這通常是你的網域名稱(如 example.com)。
  3. 注意: 如果你在本地測試,必須使用 localhost。如果透過 IP 存取(如 192.168...),大多數瀏覽器會因為安全限制拒絕執行 WebAuthn。

  4. Binary 轉換:

  5. WebAuthn 原生處理的是 ArrayBuffer。當你在前端收到 credential 後,直接用 JSON.stringify 可能會出錯。建議使用像 simplewebauthn/browser 的前端庫來簡化轉換工作。

  6. HTTPS 是強制性的:

  7. 除了 localhost 以外,Passkeys 必須在 HTTPS 環境下運作。

  8. 公鑰儲存:

  9. 驗證成功後,你會拿到一個 public_key。請將它以二進位 (Blob/Buffer) 格式存入資料庫。登入時,你需要根據使用者的 credential_id 撈出對應的公鑰來驗證簽章。

下一步建議

實作 Passkeys 後,你的系統將達到金融級的安全標準。

你會希望我提供一段前端的「萬用處理腳本」,讓你能輕鬆將剛才提到的二進位資料格式化並傳送給 Node.js 或 Python 後端嗎?

Prompt #3

好, 請提供一段前端的「萬用處理腳本」

Response #3

實作 Passkeys 最繁瑣的地方在於資料格式的轉換。WebAuthn API 規定所有金鑰相關資料都必須是 ArrayBuffer(二進位格式),但後端 API 通常只能傳輸 JSON 格式(Base64 字串)。

這段「萬用前端腳本」的核心任務就是處理 Base64URL $\leftrightarrow$ Uint8Array 的轉換,並提供註冊與登入的標準介面。

1. 現代推薦:使用 @simplewebauthn/browser

在 2026 年,最穩定的做法是使用成熟的函式庫。它已經幫你寫好了所有的轉換邏輯。

安裝:

npm install @simplewebauthn/browser

萬用邏輯:

import { startRegistration, startAuthentication } from '@simplewebauthn/browser';

// 註冊 Passkey
async function handleRegister() {
  const resp = await fetch('/generate-registration-options');
  const optionsJSON = await resp.json();

  let regResp;
  try {
    // 這個函式會處理所有二進位轉換,並跳出系統驗證視窗 (Windows Hello/TouchID)
    regResp = await startRegistration({ optionsJSON });
  } catch (error) {
    console.error('註冊中斷:', error);
    return;
  }

  await fetch('/verify-registration', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(regResp),
  });
}

2. 原生 Vanilla JS 腳本(無依賴版本)

如果你不想安裝套件,這段腳本包含了解析 WebAuthn 必備的轉換工具函數,適用於所有現代瀏覽器。

/**
 * 萬用 WebAuthn 處理器 (Vanilla JS)
 */
const PasskeyHelper = {
  // 輔助工具:將 Base64URL 轉為 ArrayBuffer
  bufferFromBase64(base64url) {
    const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
    const binStr = window.atob(base64);
    const bin = new Uint8Array(binStr.length);
    for (let i = 0; i < binStr.length; i++) bin[i] = binStr.charCodeAt(i);
    return bin.buffer;
  },

  // 輔助工具:將 ArrayBuffer 轉為 Base64URL (傳回後端用)
  bufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let str = '';
    for (const b of bytes) str += String.fromCharCode(b);
    return window.btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  },

  // 註冊流程
  async create(options) {
    // 轉換後端傳來的挑戰值與 ID
    options.challenge = this.bufferFromBase64(options.challenge);
    options.user.id = this.bufferFromBase64(options.user.id);
    if (options.excludeCredentials) {
      options.excludeCredentials.forEach(c => c.id = this.bufferFromBase64(c.id));
    }

    const credential = await navigator.credentials.create({ publicKey: options });

    // 封裝結果傳回給後端
    return {
      id: credential.id,
      rawId: this.bufferToBase64(credential.rawId),
      type: credential.type,
      response: {
        attestationObject: this.bufferToBase64(credential.response.attestationObject),
        clientDataJSON: this.bufferToBase64(credential.response.clientDataJSON),
      }
    };
  },

  // 登入流程
  async get(options) {
    options.challenge = this.bufferFromBase64(options.challenge);
    if (options.allowCredentials) {
      options.allowCredentials.forEach(c => c.id = this.bufferFromBase64(c.id));
    }

    const assertion = await navigator.credentials.get({ publicKey: options });

    return {
      id: assertion.id,
      rawId: this.bufferToBase64(assertion.rawId),
      type: assertion.type,
      response: {
        authenticatorData: this.bufferToBase64(assertion.response.authenticatorData),
        clientDataJSON: this.bufferToBase64(assertion.response.clientDataJSON),
        signature: this.bufferToBase64(assertion.response.signature),
        userHandle: assertion.response.userHandle ? this.bufferToBase64(assertion.response.userHandle) : null,
      }
    };
  }
};

3. 在不同作業系統的表現

這段腳本在所有平台運作邏輯一致,但瀏覽器調用的系統介面會根據 OS 改變:

平台 瀏覽器行為 系統反應
Windows 呼叫 Windows Hello API 彈出藍色視窗要求輸入 PIN 碼、掃描臉部指紋
macOS 呼叫 Local Authentication 螢幕右上角彈出視窗,要求按下 Touch ID 或輸入電腦密碼。
Linux 呼叫 libfido2 (視桌面環境) 視瀏覽器版本,通常會提示插入 USB 硬體金鑰 (YubiKey) 或使用手機掃描 QR Code。
Android/iOS 呼叫系統通行金鑰管理員 彈出全螢幕底部的 Sheets,刷臉或指紋即可完成。

實務建議:2026 年的「自動填充」 (Conditional UI)

目前的趨勢是讓 Passkey 出現在密碼欄位的「自動填充」選單中,這不需要使用者點擊「Passkey 登入」按鈕。

實作方式:

  1. <input> 加上 autocomplete="username webauthn"
  2. 在呼叫 navigator.credentials.get 時,傳入 mediation: 'conditional'

這能讓使用者就像使用傳統密碼管理器一樣,點一下欄位就完成生物辨識登入。

你想知道如何將這個「自動填充 (Autofill)」功能整合進剛才的腳本中,讓你的登入頁面更符合 2026 年的 UX 標準嗎?

Comments

Login to add a comment.

No comments yet.