6 位數 PIN 與九宮格圖形,到底該怎麼存

whats-new · 學習文章 · 2026-09-19 13:40
Chance Lu

一支支付 App 裡,使用者可能用 6 位數付款密碼、九宮格圖形,或 指紋/臉 來確認付款。這三種東西很容易被當成「三種密碼」,一起丟進同一張表用同一種方式存——那是一個很貴的誤會。

💣

假設那張表是可逆的:有金鑰就能解回原文。只要資料庫和金鑰有一次同時外洩,所有人的付款密碼就是明文。更冤的是——你根本不需要解得回來。

🎯

讀完這篇,你要能用自己的話回答:PIN/圖形為什麼跟生物辨識是兩類東西、雜湊與鹽與「慢」各在防什麼、為什麼 6 位數光靠慢雜湊救不了、以及那張表裡到底該存哪幾欄。

✎就愛考你

這幾題就是本文的重點,也是讀完你該能用自己的話答出來的問題。先想想現在答不答得出來,然後帶著它們往下讀——遇到答案所在的段落就特別留意;讀完再回來點「看答案」對照。

  1. 6 位數 PIN 已經加了鹽、也用 Argon2id 慢雜湊了,為什麼資料庫一外洩還是撐不了多久?看答案收合
    因為 6 位數字只有 100 萬種可能,攻擊者把 100 萬種全算一遍就好,慢雜湊只是讓「全算一遍」從幾秒變幾小時。
    • 鹽讓攻擊者不能一次撞很多人,但對「針對某一個人」的窮舉完全沒幫助。
    • 所以還需要伺服器側密鑰(pepper),以及錯誤次數限制。
  2. 鹽(salt)到底防的是什麼?它防不了什麼?看答案收合
    防「一份算好的對照表打全部人」,防不了「針對單一帳號慢慢猜」。
    • 每筆密碼配一個不同的隨機值,同樣的 PIN 在不同帳號會算出不同雜湊。
    • 攻擊者因此不能先算好「123456 → 某雜湊」再掃全表,也看不出哪兩個人用同一組 PIN。
    • 鹽不是秘密,就跟雜湊一起放在同一列。
  3. pepper 跟 salt 差在哪?為什麼 pepper 不能跟雜湊放同一張表?看答案收合
    salt 是每筆不同、公開放在表裡的隨機值;pepper 是整個系統共用、絕對不能跟表放一起的秘密金鑰。
    • pepper 的價值全在「表被偷了但 pepper 沒被偷」——所以要放進 HSM/KMS/密鑰保管庫,跟資料庫分開。
    • NIST 明講:只要這把密鑰還是秘密,對雜湊表的暴力破解就變得不切實際。
  4. 綜合題:舊表是「可逆存」的,現在要重新設計,PIN 和圖形怎麼搬?看答案收合
    解回原文 → 加鹽慢雜湊 + pepper 存回去,補上失敗計數與鎖定,最後把舊欄位和舊金鑰銷毀。
    • 正因為舊表可逆,PIN/圖形可以批次轉換,不用逼使用者重設。
    • 每列都要存演算法與參數版本(含 pepper 的 key id),之後才升級、輪替得了。

1先分家:這三把「密碼」其實是兩種東西

一句話:PIN 和圖形是「使用者記在腦裡的秘密」,指紋和臉不是——後者伺服器根本拿不到。

這個分法是後面所有結論的骨架,先看清楚:

🧠
比對秘密(PIN、九宮格)

使用者背在腦裡、每次輸入給你比對。圖形密碼本質上就是「經過哪幾個點、什麼順序」的一串資料,跟一串數字沒兩樣。

🔑
證明你握有鑰匙(指紋、臉)

手機的安全晶片幫使用者保管一把私鑰,指紋只是「解鎖那把私鑰」的開關。伺服器驗的是簽章,不是指紋。

所以存法天差地遠:第一類要把秘密「壞掉」再存;第二類在伺服器這一側根本沒有秘密可存,只存一把公鑰。這篇只講第一類,生物辨識那一側留給系列的第二篇。

💡

這段一句話:分不清「比對秘密」和「證明你握有鑰匙」,就會把三種憑證塞進同一張表——錯的不是欄位,是分類。

2「可逆存」為什麼是災難

一句話:可逆=有金鑰就能解回原文,而你根本不需要這個能力。

驗證 PIN 時,你要回答的問題只有一個:「使用者這次輸入的,跟他當初設的是不是同一個?」

這件事用單向的雜湊就能做到,不需要知道原文。存得比需要的多,就是多背一個風險。

🔓

誤解一:「金鑰有好好保管就沒事」。問題是資料庫和金鑰只要有一次同時外洩,或某個內部角色同時拿得到,所有人的付款密碼就一起裸奔。

📜

誤解二:「反正沒規定不能這樣存」。NIST SP 800-63B 講得很直白:驗證方必須把密碼存成「能抵抗離線攻擊」的形式,也就是加鹽並用密碼雜湊方案處理。

💡

這段一句話:驗證只需要知道「對不對」,不需要知道「是什麼」。能單向就別可逆。

3三個詞一次拆開:雜湊、鹽、慢

一句話:hash 讓它壞掉、salt 讓每筆不一樣、慢讓每次猜都很貴。

雜湊(hash)

一個單向函數。丟 PIN 進去吐出一串固定長度的亂碼,但從亂碼算不回 PIN。驗證時用同一個函數再算一次,比對兩串亂碼。

鹽(salt)

每筆密碼配一個不同的隨機值,跟 PIN 一起丟進雜湊。兩個人都設 123456,存下來也不一樣。鹽不是秘密,直接跟雜湊放同一列。

慢(cost factor)

一般雜湊(SHA-256)設計成越快越好,攻擊者一秒能猜幾十億次。密碼雜湊刻意做得很慢、很吃記憶體,讓每一次猜測都貴。

OWASP 對鹽的定義就是「一段獨一無二、隨機產生、加進每個密碼裡一起雜湊的字串」;NIST 要求鹽至少 32 bits、且要避免撞號。

至於「慢」要多慢,NIST 的說法是:cost factor 應該在不拖垮驗證效能的前提下盡量調高,並隨硬體進步逐年調升。

OWASP Password Storage Cheat Sheet 目前的建議(2026-09 查閱之現行版本):

演算法OWASP 建議參數什麼時候選它
Argon2idm=19456 KiB(19 MiB)、t=2、p=1 為最低配置首選
scryptN=2^17、r=8、p=1沒有 Argon2id 時
bcryptwork factor 至少 10;輸入上限 72 bytes舊系統相容
PBKDF2-HMAC-SHA256600,000 次迭代需要 FIPS-140 合規、或只想用內建 API

圖形密碼怎麼套進來?先把「畫了哪幾個點、什麼順序」序列化成固定格式的字串(例如 0-4-8-5),再當成 PIN 一樣加鹽慢雜湊。

(序列化這一步是實作上的常見做法,不是任何規範規定的格式;重點是規則要固定、不可歧義。)

💡

這段一句話:這三個詞分別在賣三件事——不可逆、每筆不同、每次很貴。少了任何一個都有明顯的破口。

4三道防線:為什麼光靠慢雜湊救不了 6 位數

一句話:慢雜湊是在賣「單價」,但 PIN 的「數量」太少,總價還是便宜。

先算數。這兩個數字決定了一切:

🔢
6 位數字=1,000,000 種

106。對比之下,一組 8 碼英數隨機密碼有 1014 級的組合——PIN 少了一億倍。

⌗
九宮格=389,112 種

比 PIN 還少。研究還發現使用者實際選的圖形集中在極少數幾種,比理論值更好猜。

(九宮格畫直線跳過一個還沒連的點時,中間那點會被自動連進去,所以很多看似不同的畫法其實是同一個圖形,合法組合因此大幅減少。)

把數字代進去:假設每次 Argon2id 要 50 毫秒,100 萬次 × 50 ms ≈ 14 小時單核,開 8 核就是不到 2 小時。

(50 ms 是量級估算,實際看你的硬體,我沒有實測。)而且鹽在這裡幫不上忙——鹽只防「一次撞多人」,攻擊者針對某一個帳號慢慢算,鹽擋不住。

所以不能只靠一道。要三道一起上,各擋一種攻擊:

① 加鹽慢雜湊

防預先算好的對照表、防一次撞多人,把每次猜測的單價拉高。

→ 擋對照表攻擊
② pepper(伺服器側密鑰)

一把整個系統共用、絕不放進資料庫的秘密金鑰。沒有它,100 萬種候選也算不出正確答案。

→ 擋離線窮舉
③ 錯誤次數限制

防攻擊者沒拿到資料庫、直接對 API 一直試。超過次數就鎖定該驗證器。

→ 擋線上猜測

pepper 怎麼加、代價是什麼

OWASP 列了兩種做法:先雜湊再 HMAC(post-hashing,把慢雜湊結果再用 HMAC-SHA256 包一層、pepper 當金鑰),或先加 pepper 再雜湊(pre-hashing)。

前者的好處很實際:可以直接套在既有的雜湊上,不用動慢雜湊本身。(HMAC =「帶金鑰的雜湊」:同一份資料,沒有那把金鑰就算不出同樣的結果。)

代價是不好換。資料庫裡存的是 HMAC(pepper, 慢雜湊),內層拿不回來,沒有原 PIN 就算不出新的外層。

OWASP 明講:pepper「無法在不知道使用者密碼的情況下更換……需要強制所有使用者重設」。務實的做法是等使用者下次成功輸入時順手重算,並在紀錄裡記下 pepper 的 key id/版本。

pepper 本身用密碼學安全的隨機數產生器產生,至少 32 bytes。放哪裡?OWASP 說要「跟密碼資料庫分開儲存」、放在密鑰保管庫或 HSM。

NIST 說得更硬:這把密鑰必須與雜湊分開存放、應該「儲存並使用」於 HSM 或 TEE 等硬體保護區——意思是理想上連 HMAC 這一步都交給 KMS/HSM 代算,pepper 根本不進 App 的記憶體。

OWASP 也提醒:pepper 是縱深防禦,單靠它本身沒有額外的安全性質。三道要一起上,不是挑一道做。

次數限制要設多少

💡

這段一句話:三道防線分別擋三種攻擊——慢雜湊擋對照表、pepper 擋離線窮舉、次數限制擋線上猜測。少一道就破一個口。

5那張表裡到底要存什麼

一句話:存「版本 + 鹽 + 雜湊 + 失敗計數」,不存原文、不存 pepper。

每一列存這四樣,缺一不可:

🚫

絕對不存:PIN 原文、任何可解回原文的加密結果、pepper。

有了版本欄位,升級 work factor 就有路可走(OWASP 的做法):等使用者下次成功登入、手上有原文時,用新參數重算一次存回去。

台灣法規怎麼說(電子支付情境)

前面講的「雜湊 + 額外資料 + 金鑰放 HSM」不只是最佳實務,在台灣的電子支付情境裡有對應的法規要求。

要求的文字大致是:使用者身分識別資料中的固定密碼,儲存時應先做不可逆運算(如雜湊);為防止用預先產製的雜湊值推測密碼,還應加密保護或加入不可得知之資料一起運算;採加密者,金鑰應存於 HSM 內並限制匯出。

對照一下就會發現,這三句話跟本文的三件事一模一樣:

法規的說法本文的說法
不可逆運算雜湊,不要可逆加密
加入不可得知之資料pepper(伺服器側密鑰)
金鑰存於 HSM 並限制匯出pepper 不能跟資料庫放一起
誠實提醒(這點請你們法遵確認):這段文字原本在某部辦法的第 14 條,該辦法已於民國 112 年 4 月 25 日廢止;目前對應的是由同業公會擬訂、報主管機關核定的作業基準(我查到的是民國 113 年 10 月 24 日版,相同內容落在第 17 條)。法規會改版、條號會移動,不要直接拿我這段當合規依據——我能確定的只有「要求的實質內容」跟三道防線完全吻合。
想看程式碼?.NET 的 PBKDF2 + pepper 範例(也附 Argon2id 版)

Rfc2898DeriveBytes.Pbkdf2 這組靜態方法從 .NET 6 開始提供;string 版會用 UTF-8 轉 bytes。下面同時示範 OWASP 的「先雜湊再 HMAC」pepper 作法:

using System.Security.Cryptography;

public sealed record PinRecord(int Version, int Iterations, byte[] Salt, byte[] Hash);

public static class PinHasher
{
    const int Iterations = 600_000;  // OWASP:PBKDF2-HMAC-SHA256 建議 600,000 次
    const int SaltSize   = 16;       // 128 bits(NIST 最低 32 bits,多給無妨)
    const int HashSize   = 32;

    // pepper:系統共用的秘密金鑰(CSPRNG 產生、至少 32 bytes)。環境變數是退而求其次;
    // 理想是連下面的 HMAC 都改呼叫 KMS / HSM 的 MAC 操作。絕不進資料庫。
    static byte[] Pepper => Convert.FromBase64String(
        Environment.GetEnvironmentVariable("PIN_PEPPER") ?? throw new InvalidOperationException("PIN_PEPPER 未設定"));

    public static PinRecord Enroll(string pin)
    {
        byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
        return new PinRecord(Version: 1, Iterations, salt, Derive(pin, salt, Iterations));
    }

    public static bool Verify(string pin, PinRecord rec)
    {
        byte[] candidate = Derive(pin, rec.Salt, rec.Iterations);
        // 固定時間比較,避免用回應時間差猜出前幾個 byte
        return CryptographicOperations.FixedTimeEquals(candidate, rec.Hash);
    }

    static byte[] Derive(string pin, byte[] salt, int iterations)
    {
        byte[] slow = Rfc2898DeriveBytes.Pbkdf2(pin, salt, iterations, HashAlgorithmName.SHA256, HashSize);
        return HMACSHA256.HashData(Pepper, slow);   // post-hashing:pepper 當 HMAC 金鑰
    }
}

內建沒有 Argon2id,可用 NuGet 套件 Konscious.Security.Cryptography.Argon2(MemorySize 單位是 KiB)。把 Derive 裡的 PBKDF2 那行換掉即可,但 PinRecord 要能裝下 m/t/p 三個參數(或用 Version 對應一組固定參數):

using Konscious.Security.Cryptography;
using System.Text;

static byte[] Argon2idHash(string pin, byte[] salt)
{
    using var argon2 = new Argon2id(Encoding.UTF8.GetBytes(pin))
    {
        Salt                = salt,
        DegreeOfParallelism = 1,          // p=1
        Iterations          = 2,          // t=2
        MemorySize          = 19 * 1024   // 19456 KiB = 19 MiB(OWASP 最低建議)
    };
    return argon2.GetBytes(32);
}

(套件我只查了 README 的 API 形狀,沒有實際跑過效能;上線前請自己量一次每次雜湊的耗時。)

💡

這段一句話:版本欄位是給未來的自己留的後路——沒有它,你之後想升級參數或換 pepper 都動不了。

★一句話帶走

使用者記在腦裡的秘密要「壞掉」再存,而且要三道防線一起上。慢雜湊擋對照表、pepper 擋離線窮舉、次數限制擋線上猜測——因為 6 位數只有 100 萬種、九宮格只有 389,112 種,單靠演算法選得好是救不了的。

🔗這系列的另外兩篇

📚參考來源