題目與適用範圍
一個擁有 10,000,000 個帳戶的 SaaS 正在改造密碼認證。目前資料庫中約 7,000,000 筆記錄是 cost 為 10 的 bcrypt,2,000,000 筆是 210000 次迭代的 PBKDF2-HMAC-SHA256,另有 1,000,000 筆是沒有獨立鹽值的 SHA-256。現有記錄格式不統一,部分資料列無法直接看出演算法和參數。認證服務尖峰需要處理每秒 1500 次密碼驗證,單一執行個體最多允許 24 個高成本雜湊併發。
請設計遷移到 Argon2id 的完整方案。回答需要說明威脅模型、參數選擇與容量測量、可演進的儲存格式、混合演算法驗證、登入時升級、併發密碼修改、長期未登入帳戶、pepper 管理、資料庫或金鑰外洩後的回應、帳戶列舉與資源耗盡防護,以及上線和完成標準。題目中的帳戶數、演算法比例、舊參數和併發數只是面試約束,不是通用安全閾值。
這是一道後端安全與認證工程題。重點是驗證器和遷移狀態機,不要求設計完整的註冊、找回密碼、MFA 或工作階段系統;這些流程只在影響憑證遷移和外洩處置時討論。
面試官在考什麼
第一,候選人能否區分線上猜測與離線破解。限流能約束登入介面,卻無法限制取得雜湊庫的攻擊者。密碼是低熵的人類秘密,SHA-256 這類快速摘要會讓攻擊者高速列舉候選;專用密碼雜湊透過獨立鹽值、可調運算成本和記憶體成本,提高每次猜測的代價。
第二,候選人是否理解參數必須在真實容量上測量。說「使用 Argon2id」只是演算法選擇。完整方案還要決定記憶體 m、迭代 t、平行度 p、鹽值和輸出長度,並計算併發驗證對記憶體、CPU、延遲和阻斷服務面的影響。參數越高不會自動越安全;如果服務在攻擊流量下耗盡記憶體,認證可用性會先失守。
第三,候選人能否遷移不可逆資料。系統拿不到使用者明文密碼,不能離線把 bcrypt 結果「解密後轉換」為 Argon2id。常規路徑是在一次舊演算法驗證成功後,使用本次提交的明文重新計算目前格式;長期未登入帳戶則需要保留受控的舊驗證器、做暫時包裹,或要求重設,選擇取決於舊演算法風險和外洩歷史。
最後,優秀回答會處理競態與維運邊界。登入升級不能覆蓋同時發生的密碼重設;pepper 不應和雜湊庫一起存放;演算法版本、參數和遷移狀態需要可觀測,但日誌不能包含密碼、完整雜湊或 pepper。完成標準也不能只寫「新程式碼已發布」。
回答前先確認
- 現有每種格式能否可靠識別? 需要知道演算法、參數、字元編碼、鹽值位置和歷史函式庫版本。無法識別的資料列不能猜測演算法後嘗試多個驗證器。
- 舊 bcrypt 如何處理超過 72 位元組的輸入? 必須重現歷史實作的編碼和截斷語意,避免遷移時無聲改變使用者實際憑證。
- 資料庫、備份或舊雜湊是否曾經外洩? 已外洩的無鹽快速摘要不能靠替目前資料庫再包一層就撤回,可能需要強制重設和工作階段處置。
- 認證執行個體的真實資源預算是多少? 需要基準記憶體、CPU 配額、雜湊併發上限、自動擴縮速度、目標 p95/p99 延遲和可接受失敗率。
- 是否存在 FIPS 等合規約束? 這會影響可選演算法和實作,但不能用合規標籤取代參數測量與遷移設計。
- pepper 是否已經存在? 要確認儲存位置、呼叫依賴、版本、輪替能力、稽核邊界和金鑰服務不可用時的降級策略。
- 密碼修改和工作階段簽發的交易邊界是什麼? 遷移成功、併發重設和工作階段簽發的順序必須明確,否則舊密碼可能在重設後仍換取新工作階段。
- 長期不活躍帳戶的業務價值和風險如何分層? 高權限帳戶、近期活躍帳戶和多年未登入帳戶可以採用不同截止日期和重新驗證要求。
30 秒回答框架
「我先把目標定義為抵抗雜湊庫外洩後的離線猜測,並另外保護線上認證容量。新密碼使用帶隨機獨立鹽值的 Argon2id,記錄中儲存演算法、版本、m/t/p、鹽值和輸出;pepper 若啟用,則放在資料庫之外的金鑰系統中。參數以 OWASP 目前最低設定作為候選起點,再在生產同規格執行個體上按併發記憶體、CPU 和 p99 延遲壓測,不能照抄一個數值。
登入時先依記錄版本選擇唯一舊驗證器。驗證成功後計算目前 Argon2id 記錄,以舊雜湊作為條件執行 compare-and-swap;若併發修改導致失敗,就重新讀取並驗證最新記錄,不能覆蓋密碼重設。新建和重設密碼直接寫目前格式。無鹽 SHA-256 帳戶設定更短遷移期限;沒有可靠安全包裹或已發生外洩時要求重設。
介面使用統一錯誤、虛擬目前雜湊、帳戶與來源雙維度限流和有界雜湊併發,避免列舉及記憶體耗盡。上線後按演算法版本觀察遷移比例、延遲、記憶體、失敗、CAS 衝突和重設完成率;只有舊高風險格式清零、競態測試與尖峰壓測通過、金鑰輪替演練完成,才宣布遷移結束。」
分步詳解
第一步:先畫清線上與離線攻擊面
正常登入是線上路徑。攻擊者受到介面吞吐、帳戶限流、來源限流、風險控制和監控約束。資料庫外洩後的猜測是離線路徑:攻擊者在自己的 GPU、ASIC 或雲端執行個體上執行驗證演算法,不受應用程式限流控制。密碼儲存主要要提高後一條路徑中每次候選嘗試的成本。
無鹽 SHA-256 有兩個問題。它計算很快,相同密碼還會得到相同摘要,使攻擊者能重複使用預先計算結果並識別密碼重複使用群組。每筆記錄獨立的隨機鹽值讓相同密碼產生不同輸出,迫使攻擊者逐筆計算;鹽值可以和雜湊一起儲存,不需要保密。鹽值不能把快速 SHA-256 變成合格的密碼雜湊,真正的計算阻力來自專用、可調且最好具有記憶體成本的演算法。
Pepper 是另一層控制。它是跨記錄共用或按版本管理的伺服器端秘密,必須和密碼資料庫分離,通常放在金鑰管理系統、HSM 或受保護執行環境中。它可以降低「只外洩資料庫」時的風險,但無法取代獨立鹽值和慢速雜湊。一旦資料庫與 pepper 同時外洩,舊 pepper 對應的記錄不能只靠資料庫批次處理安全換鑰,因為系統沒有使用者明文。
第二步:選擇參數並計算認證容量
對新系統,選擇維護良好的 Argon2id 實作。OWASP 目前給出的最低候選之一是 m=19456 KiB、t=2、p=1。RFC 9106 還給出更高記憶體的通用建議,包括記憶體受限情境下 64 MiB、3 次迭代、4 路平行的方案。兩份資料的目標和執行約束不同,不能把任意一組數字當成所有 Web 服務的固定答案。
更可靠的選擇過程是:
- 在與生產相同的 CPU、記憶體限制和執行環境上,從經過審查的候選設定開始;
- 測量單次驗證的中位數、p95、p99、實際常駐記憶體和 CPU 時間;
- 使用正常尖峰、登入洪峰、錯誤密碼和不存在帳戶的混合流量壓測;
- 設定有界併發,觀察排隊時間、容器 OOM、CPU throttling 和上游逾時;
- 在可用性預算內選擇盡可能高的攻擊成本,並記錄硬體、函式庫版本和測量日期。
以 19 MiB 為例,24 個同時執行的雜湊僅工作記憶體理論值就是 456 MiB,還未包含程序基準、函式庫開銷、請求物件和安全餘量。如果單次驗證在目標執行個體上需要 250 毫秒,一個執行個體的粗略上限約為每秒 96 次完成;1500 次每秒至少需要約 16 個始終可用的同規格執行個體,實際還要為尾端延遲、故障和擴容延遲留餘量。這些計算只用於揭示容量級別,最終值必須來自壓測。
鹽值可採用每筆記錄獨立產生的 128 位元隨機值,輸出可採用 256 位元。使用成熟函式庫產生並解析標準編碼,避免自行拼接密碼學欄位。密碼輸入在雜湊前應遵循穩定、經過記錄的位元組編碼;如果支援 Unicode,需要明確正規化策略並保持註冊、登入和遷移一致。
第三步:讓憑證記錄能夠自我描述和演進
記錄需要攜帶驗證所需的非秘密資訊。例如 Argon2 編碼可以表示為:
$argon2id$v=19$m=19456,t=2,p=1$SALT_BASE64$TAG_BASE64舊格式也要建立確定性對應,例如 {bcrypt}、{pbkdf2-sha256}、{sha256-legacy}。標籤屬於解析協議,不代表安全背書。未知標籤、缺少欄位、非法 Base64 或超出允許範圍的參數都應拒絕並進入受控修復佇列,不能依序嘗試多個演算法直到某個結果符合。
參數解析必須有上限。攻擊者如果能竄改記錄,可能把記憶體或迭代參數改成極大值,藉登入請求消耗服務資源。驗證器只接受部署允許的演算法和參數範圍;儲存值超界時記錄不含敏感資料的安全事件,並要求帳戶復原。
憑證表至少要支援目前編碼、憑證更新時間和安全處置狀態。遷移報表可以從編碼前綴彙總演算法版本,或維護低基數的 scheme_id。不要把完整雜湊、鹽值、候選密碼、pepper 版本秘密或驗證中間值寫入日誌、指標標籤和分析系統。
第四步:在成功登入時安全升級
遷移函式先解析記錄,再呼叫唯一符合的驗證器。只有舊憑證驗證成功,系統才擁有本次請求中的正確明文,可以計算目前 Argon2id 記錄。昂貴計算放在資料庫短交易之外,寫入使用舊記錄作為條件:
record = loadCredential(userId)
ok = verifyByDeclaredScheme(record.hash, submittedPassword)
if !ok: rejectWithGenericError()
if needsRehash(record.hash):
upgraded = hashWithCurrentPolicy(submittedPassword)
changed = compareAndSwap(userId, expected=record.hash, replacement=upgraded)
if !changed:
latest = loadCredential(userId)
if !verifyByDeclaredScheme(latest.hash, submittedPassword):
rejectAndAskForFreshLogin()
issueSessionAfterCredentialStateIsConfirmed()Compare-and-swap 防止登入升級覆蓋同時發生的密碼重設。條件更新失敗可能表示另一次登入已完成相同遷移,也可能表示使用者剛換了不同密碼。必須重新讀取並使用本次輸入驗證最新記錄;不能因為舊記錄曾經符合就繼續簽發工作階段。新註冊、主動改密和找回密碼都直接寫目前格式,不再產生舊記錄。
升級寫入失敗時,登入是否允許繼續要由風險邊界決定。可重試的資料庫錯誤可以暫時保留舊記錄並記錄遷移失敗,但高風險舊格式可以規定「升級成功才允許簽發工作階段」。無論採用哪種策略,都要限制重試,避免一次登入重複執行多個高成本雜湊。
第五步:分別處理可接受舊格式與高風險舊格式
Bcrypt 和足夠強的 PBKDF2 可以在受控期限內保留唯讀驗證能力,並在成功登入後升級。舊驗證器需要最小化暴露,只用於存量記錄,不能繼續建立新憑證。監控每種格式的剩餘帳戶數、活躍程度和遷移速度,到達截止日期後刪除舊驗證能力前,必須先處理仍依賴它的帳戶。
無鹽 SHA-256 風險更高。對未發生外洩且無法立刻要求所有使用者重設的系統,可以把現有摘要當作輸入,再用帶新鹽值的慢速演算法做一次離線包裹,作為暫時資料庫加固。驗證時先按歷史規則計算 SHA-256,再驗證外層;使用者成功登入後仍要把真實提交密碼改寫為標準 Argon2id 記錄。
這種包裹不等同於 Argon2id(password)。它無法撤回曾經外洩的內層摘要,也無法修復歷史編碼、截斷或弱密碼問題。如果舊雜湊或備份已經外洩、帳戶權限較高,或無法確信格式語意,應將帳戶設為需要重設,撤銷相關工作階段,並透過獨立驗證流程復原。多年未登入的普通帳戶也可以在截止日期後凍結認證,等使用者回來再走找回流程,而不是永久保留最弱驗證器。
第六步:設計 pepper 版本和外洩回應
如果使用 pepper,可以在密碼雜湊前採用受審查的 keyed 預處理,或對雜湊輸出再做 HMAC;具體組合應由成熟函式庫和安全審查決定。資料庫記錄只儲存非秘密的 pepper 版本識別,實際金鑰留在資料庫與備份之外。認證服務透過最小權限存取,快取時間和金鑰服務故障策略需要明確。
常規輪替可以短期同時持有「目前」和「上一版」金鑰。舊版驗證成功後,使用使用者提交的明文和目前 pepper 重算完整記錄。不能無限保留舊金鑰;輪替計畫必須統計剩餘舊版本帳戶,並為未遷移帳戶安排重設或凍結。
外洩回應按證據分層:
- 只外洩資料庫:立即保全證據、修復入口、提高監控,評估演算法與參數後決定是否強制重設;pepper 仍保密可以提供額外時間,但不能保證弱密碼安全。
- 只外洩 pepper:輪替金鑰,調查呼叫日誌,並評估攻擊者是否同時可能取得雜湊庫。
- 資料庫與對應 pepper 均外洩:按密碼雜湊暴露處理,強制受影響帳戶重設,撤銷或縮短相關工作階段,並提醒使用者處理重複使用密碼。
輪替成功不代表歷史外洩消失。處置記錄要包含受影響版本、帳戶範圍、備份、工作階段、通知、完成比例和殘留例外。
第七步:同時防止列舉和資源耗盡
不存在帳戶、錯誤密碼、停用帳戶和遷移失敗對外回傳統一的錯誤語意,狀態碼和回應形態也應保持一致。不存在帳戶仍執行一個受控的目前策略虛擬雜湊,減少「直接回傳」和「執行昂貴驗證」之間的明顯時間差。混合舊演算法可能產生不同耗時,還需透過遷移、執行時間補償和統計測量減少可利用差異;不應承諾網路上的每次回應完全等時。
昂貴雜湊本身會形成阻斷服務面。進入雜湊佇列前,應用程式要執行不洩露帳戶是否存在的粗粒度來源限流和請求合法性檢查;對允許進入的請求,再使用帳戶維度與來源維度的獨立配額、全域有界佇列和每個執行個體 24 個併發許可。單純按「IP 加使用者名稱」組合鍵限流,會讓攻擊者不斷更換其中一項繞過總量約束。
佇列滿時快速回傳統一的暫時失敗,不能無界堆積請求。擴容指標應包含排隊時間、雜湊併發、記憶體餘量和 CPU throttling,而不只是請求數。日誌只記錄帳戶的不可逆內部識別、演算法低基數版本、結果類別、限流維度和延遲區間;禁止記錄提交密碼、完整憑證記錄或金鑰材料。
第八步:分階段發布並用證據驗收
先做唯讀盤點,確認每筆舊記錄可解析,並在隔離環境使用已知測試向量驗證歷史實作。接著發布「可讀所有受支援舊格式、只寫目前格式」的程式碼。先讓新註冊和改密流量寫入 Argon2id,再小比例啟用登入升級,觀察 CAS 衝突、驗證失敗和資源曲線,最後逐步擴大。
上線前至少執行以下驗證:
- 每種歷史格式的正確密碼、錯誤密碼、邊界長度、Unicode 和損壞記錄測試;
- 登入升級與併發密碼重設競態,證明舊登入不能覆蓋新密碼或簽發錯誤工作階段;
- 目前、上一版和未知 pepper 的輪替與故障演練;
- 每秒 1500 次目標負載和攻擊混合流量下的延遲、記憶體、CPU、佇列和擴容測試;
- 不存在帳戶與各類失敗回應的訊息、狀態、大小和時延分布檢查;
- 資料庫、備份、日誌、指標和錯誤追蹤中敏感欄位的掃描。
遷移看板按演算法和風險層統計總數、最近活躍數、每日成功升級數、失敗原因、強制重設完成率和截止日期。技術完成標準包括:所有新寫入都採用目前策略;無鹽 SHA-256 和已外洩版本清零;不再需要的舊驗證器已刪除;剩餘可接受舊格式符合書面例外;尖峰壓測與競態測試通過;pepper 輪替和復原演練有證據;認證 SLO 在約定範圍內。
高品質示範回答
「我會先區分兩個攻擊面。線上攻擊受介面限流約束,雜湊庫外洩後的離線攻擊不受約束,所以密碼必須使用帶獨立隨機鹽值、可調成本且記憶體困難的專用雜湊。新記錄選擇成熟實作的 Argon2id,並自我描述演算法版本、m/t/p、鹽值和輸出。Pepper 若啟用,會按版本放在資料庫和備份之外的金鑰系統中。
參數不會從部落格直接複製。我會把 OWASP 目前的 19 MiB、t=2、p=1 作為最低候選之一,在生產同規格執行個體上測量單次和併發的 p50/p95/p99、CPU 與真實記憶體,再使用正常尖峰和攻擊流量壓測。19 MiB 乘 24 個併發已經是 456 MiB 工作記憶體,所以服務要有有界佇列、併發許可和足夠執行個體餘量。鹽值使用成熟函式庫為每筆記錄隨機產生,記錄參數以便日後升級。
登入時只按記錄宣告的格式選擇驗證器。舊密碼驗證成功後,在資料庫交易外計算目前 Argon2id 記錄,再以舊雜湊為條件更新。若 CAS 失敗,我會重新讀取最新憑證並再次驗證本次輸入;這樣併發登入可以收斂,而併發密碼重設不會被舊登入覆蓋。確認憑證仍有效後才簽發工作階段。註冊、主動改密和找回流程從第一天起只寫新格式。
我會給 bcrypt 與仍可接受的 PBKDF2 一個明確遷移期,只保留驗證能力。無鹽 SHA-256 進入更高風險佇列。未曾外洩時可以用帶新鹽值的慢速外層做短期包裹,但它不能取代使用真實密碼重新雜湊;已外洩、高權限和長期不活躍帳戶在截止日前重設或凍結。舊驗證器不會永久保留。
介面使用統一錯誤和虛擬目前雜湊,減少帳戶列舉差異,並結合來源、帳戶兩個獨立限流維度、全域佇列和記憶體併發上限,防止雜湊型阻斷服務。完成證據包括版本分布、高風險舊格式清零、遷移失敗與 CAS 衝突、尖峰壓測、競態測試、敏感日誌掃描、pepper 輪替和外洩演練。只有這些指標通過且舊驗證器按計畫下線,我才宣布遷移結束。」
常見錯誤與改進
- 用 SHA-256 加鹽儲存密碼 → 鹽值阻止跨帳戶重複使用計算,卻不降低單次高速猜測能力 → 使用專用、可調且記憶體困難的密碼雜湊。
- 聲稱雜湊無法破解 → 弱密碼仍可透過候選猜測得到符合結果 → 把目標表述為提高每次離線猜測的成本,並搭配密碼阻止清單和 MFA。
- 把 OWASP 參數當成永久最佳值 → 硬體、函式庫、執行個體資源和流量會變化 → 記錄基準環境,定期測量並按版本升級。
- 離線「轉換」所有 bcrypt 為 Argon2id → 不知道明文時無法得到標準新雜湊 → 在成功驗證時重新雜湊,剩餘帳戶按風險包裹、重設或凍結。
- 遷移更新不帶舊值條件 → 登入可能覆蓋剛完成的密碼重設 → 使用 compare-and-swap,失敗後重新讀取並重新驗證。
- 雜湊時持有使用者資料列鎖 → 高成本計算放大鎖定時間和連線池占用 → 交易外計算,短交易條件寫入。
- 把 pepper 存在同一資料庫設定表 → 一次資料庫外洩同時取得兩層材料 → 把金鑰放在獨立受保護系統並按版本稽核。
- 輪替 pepper 只修改環境變數 → 舊記錄無法使用新金鑰直接驗證 → 保留短期雙版本驗證,並在成功登入後完整重算或要求重設。
- 不存在帳戶立即回傳 → 回應時間暴露帳戶是否存在 → 使用統一錯誤、虛擬雜湊和實際時延分布測試。
- 無限併發執行 Argon2id → 攻擊者能放大記憶體和 CPU 消耗 → 在雙維度限流後使用有界佇列和併發許可。
- 忽略 bcrypt 的輸入邊界 → 歷史 72 位元組語意可能在遷移後改變憑證 → 重現舊驗證行為,並對超界帳戶要求明確改密。
- 只看新註冊已用 Argon2id → 大量活躍或高風險舊記錄仍然暴露 → 按演算法、風險和活躍度追蹤存量,設定清零與例外標準。
延伸追問
追問一:為什麼鹽值可以明文儲存,pepper 卻要保密?
鹽值的職責是讓每筆記錄的計算輸入不同,避免相同密碼共用輸出和預先計算成果;攻擊者知道鹽值後仍必須逐筆猜測。Pepper 的額外價值來自攻擊者只取得資料庫時還缺少一個伺服器端秘密,因此它必須與雜湊庫分離。兩者職責不同,pepper 不能取代每筆記錄的獨立鹽值。
追問二:能否在資料庫裡直接提高 Argon2id 的迭代次數?
不能從舊輸出推導出使用更高參數的標準密碼雜湊,因為缺少使用者明文。可以把舊輸出再包一層作為暫時加固,但記錄語意會改變,也不能消除舊輸出曾外洩的風險。標準升級仍應在正確密碼驗證成功時重新計算,或要求使用者重設。
追問三:CAS 失敗後為什麼不能直接允許登入?
失敗既可能是另一個請求完成了相同遷移,也可能是使用者同時透過找回流程設定了新密碼。後一種情況下,本次舊密碼已經失效。重新讀取最新記錄並驗證本次輸入,才能區分兩種情況;驗證失敗就不能簽發工作階段。
追問四:Argon2id 參數越高越好嗎?
更高記憶體或時間成本會增加離線攻擊成本,也會增加合法登入的資源消耗。過高參數會造成尾端延遲、佇列堆積、執行個體 OOM,甚至讓低成本請求變成阻斷服務放大器。合適參數是在實際硬體、併發、擴容和 SLO 約束下測得的最高可持續成本,並需要隨硬體和威脅變化重新評估。
追問五:如何處理五年未登入但仍是 bcrypt 的帳戶?
先按帳戶權限、外洩歷史和舊參數判斷風險。普通低風險帳戶可以在遷移截止日後凍結密碼登入,使用者回來時透過受控復原流程設定新密碼;高權限或受影響帳戶應更早強制重設並撤銷工作階段。永久保留舊驗證器會讓遷移永遠無法結束。