什麼是 Secure Remote Password(SRP-6a)? 如何保護用戶密碼? 實作範例說明

Apple 也在用的安全技術

#資訊安全

什麼是 Secure Remote Password(SRP-6a)? 如何保護用戶密碼? 實作範例說明

安全遠端密碼協議,英文為 Secure Remote Password(SRP)是一種安全的認證方法,目的是在不通過網路明文傳輸密碼的情況下安全地驗證用戶的身分。

傳統的密碼雜湊 (Hashing) 是將明文密碼發送到伺服器再雜湊,然後與原本存儲的雜湊值進行比對。

而 SRP 永遠不會在伺服器上傳輸或存儲用戶的密碼,顯著降低了被攔截或伺服器被侵害的風險。它的好處是能阻止諸如 MITM(中間人攻擊)等不同類型的攻擊,不需要在用戶數據洩露後更改密碼,並允許用戶和伺服器相互確認身份。

像蘋果和 1Password 這樣的主流公司已經將 SRP 作為其認證機制的一部分。例如,蘋果在其 iCloud 鑰匙串中實施了SRP,以安全地同步跨設備的密碼而不暴露它們。同樣,1Password 使用用戶的主密碼和密鑰來加密資料,並且使用 SRP 來認證帳戶並確保它們不會傳輸到伺服器。

關於本文章

本文章主要解釋 SRP-6a 的每個步驟,並提供 RFC-5054RFC-2945 的整合說明。也會在後面的變數介紹和示範代碼中對每個計算公式進行說明。這對於剛接觸SRP並希望了解其在RFC 中的實施方式的開發者特別有幫助,確保他們可以跟隨提到的每一個計算步驟。

要注意的是,並非所有開源 SRP 套件都完全遵循 RFC 標準,這意味著有些套件可能無法與其他套件良好整合。大多數的套件省略了 H(PAD(A) ‖ PAD(B)) 這個步驟為 H(A ‖ B)

而本文則是嚴格遵循 SRP-6a 規範,使用 windwalker/srp 的實現作為模型。按照此示範操作,您的實作應該能夠輕鬆地與其他完全符合 RFC 的套件協同使用。

如果您有興趣想要找到更多 100% 實現 SRP-6a 的套件,請見此SRP實作列表

SRP 的流程

SRP-6a 的定義和過程分散在 RFC 2945RFC 5054 中;最早是 RFC 2945 先定義了 SRP 的基本架構,後來 RFC5054 進一步完善了整個驗證流程,稱為 SRP-6a 標準。但 RFC 5054 並沒有重頭介紹 RFC 2945 的部份,僅只是提及而已。所以完整的實作,必須要交叉觀看兩個標準才行。

這篇文章嘗試將它們整合起來一起說明。若您要實作 SRP,請嚴格遵循 RFC 指定的程序,不要進行自定義修改,並避免不必要地傳輸任何變數以防止安全漏洞。

定義

這個表格解釋了 SRP 協議中涉及的各種變數的摘要說明,並解釋了他們的傳輸方向與計算方式。傳輸方向必須嚴格遵守,任何意外的傳輸不該傳輸的值,都會被視為漏洞。

變數名稱傳輸計算方式
I, identity主要身份(用戶名或電子郵件)。C=>S
N安全的大質數,所有變數都是靠 modpow N 得出的。X
g一個起始向量值X
k乘數參數XSHA1(N ‖ PAD(g))
s用戶鹽C<=Srandom()
v密碼驗證值Xg^x % N
x鹽+身份+密碼的雜湊值。XSHA1(s ‖ SHA1(I ‖ ":" ‖ P))
a, b客戶端和伺服器的私鑰Xrandom()
A客戶端公鑰C=>Sg^a % N
B伺服器公鑰C<=Sk*v + g^b % N
u防止攻擊者得知用戶驗證值的的值XH(PAD(A) ‖ PAD(B))
S (客戶端)預共享密鑰(安全的共用密鑰)X(B - (k * g^x)) ^ (a + (u * x)) % N
S (伺服器)預共享密鑰(安全的共用密鑰)X(A * v^u) ^ b % N
K用於生成 M 的 Session KeyXH(S)
M1Evidence message1,用於驗證雙方生成了相同的 Session Key。C=>SH(H(N) XOR H(g) ‖ H(U) ‖ s ‖ A ‖ B ‖ K)
M2Evidence message2,用於驗證雙方生成了相同的 Session Key。C<=SH(A ‖ M ‖ K)

使用者註冊過程

當一個應用程序(網站或手機)開始註冊流程時,可能會向用戶顯示帳戶 IdentityI)(用戶名或電子郵件)和密碼 PasswordP)表單欄位。

用戶輸入帳戶和密碼後點擊註冊按鈕。SRP 客戶端將生成一個隨機鹽 Salts)以及從鹽、帳戶和密碼生成的密碼驗證值 Verifierv)。

然後應用程序將只發送 Salt, VerifierIdentity 到伺服器,不發送密碼。如果密碼被意外傳輸到伺服器,即使這個密碼沒有被伺服器作任何處理,依然會被視為違反規範和安全漏洞。

伺服器收到註冊請求時,可以將 Identity 與其他 User 資訊,還有 SaltVerifier保存到資料庫。如果您想在保存前加密 Salt and Verifier,請確保使用僅有伺服器知道的密鑰進行加密。

使用者登入過程

Hello 和 伺服器 Step 1

當用戶開始登錄過程時,他們可能會在表單欄位上輸入他們的帳戶和密碼,然後點擊登錄按鈕。SRP 客戶端將向伺服器發送一個帶有帳戶名稱 (Identity) 的 Client Hello 請求。伺服器應該通過這個帳戶名稱檢查用戶是否存在,並從用戶數據獲取 SaltVerifier

接下來,伺服器將生成一個隨機的私有 b 和公開 B,並通過數據庫、Session 或快取記住它們,因為我們將在後續步驟中需要它們,然後將 SaltB 返回給客戶端(Server Hello)。這個過程類似於握手,為雙方創建連接會話。

有些套件將客戶端 Hello 稱為 Challenge,而 B 是 Server Challenge Value。

客戶端 Step 1 and 2

接收到 BSalt 後,客戶端運行 Step 1 以生成 a, Ax,然後運行 Step 2 使用上述所有值生成客戶端證明 M1。它將與 A一起發送給伺服器。伺服器端也使用所有生成的值生成 M1 並進行比較。

如果比較失敗,伺服器將回報錯誤,如果比較成功,伺服器將生成伺服器證明 M2 並返回給客戶端。到此步驟,認證過程完成,您可以簡單地將用戶重定向到登錄成功頁面。

客戶端 Step 3 是可選的,您可以驗證 M2 以確認伺服器是可信的,並確保雙方生成相同的會話密鑰(S)。如果您完成了這個 Step 3,這意味著您完成了認證握手並進行了雙向認證。

如果您確定想運行 Step 3 的話,您可以等 Step 3 通過後再將用戶重新導向到完成頁面。

關於 S 和 M

當客戶端和伺服器生成 M 時,他們都會生成一個預共享密鑰 Pre-master secret(S)。即使雙方沒有將 S 發送給對方,S 也應該是相同的。M1M2 是確保雙方擁有相同 S 的驗證器。

因此,如果您未來想進行其他加密行為,S 可以是一個可信的共享密鑰。(但不可取代公私鑰,通常是傳輸公鑰時,可以靠 S 再度加密)

示範代碼

以下是一個示範如何在 SRP 伺服器端和客戶端之間交互工作的偽碼。

const server = SRPServer.create();
const client = SRPClient.create();

// 註冊
const identity = '...';
const password = '...';

// 註冊: 產生新的 salt & verifier

// random()
const salt = client.generateSalt();
// (SHA(s | SHA(I | `:` | P)))
const x = client.generateX(salt, identity, password);
// (g^x % N)
const verifier = client.generateVerifier(x);


// 將 salt and verifier 發送到伺服器儲存


// 開始登入
// AJAX:hello?{identity} - 伺服器 step (1)
// salt & verifier 已經存到使用者資料中, 伺服器可以從 DB 取出
// b & B 必須暫存在 session 中,後續的步驟會使用到他們

// random()
const b = server.generateRandomSecret();
// ((k*v + g^b) % N)
const B = server.generateB(b, verifier);


// 伺服器回傳 B & salt 給客戶端


// 客戶端 step (1)

// random()
const a = client.generateRandomSecret();
// (g^a % N)
const A = client.generateA(a);
// (SHA(s | SHA(I | `:` | P)))
const x = client.generateX(salt, identity, password);


// 客戶端 step (2)

// H(PAD(A) | PAD(B))
const u = client.generateU(A, B);
// ((B - (k * g^x)) ^ (a + (u * x)) % N)
const S = client.generateS(a, B, x, u);
// H(S)
const K = client.hash(S);
// H(H(N) xor H(g), H(I), s, A, B, K)
const M1Client = client.generateM1(identity, salt, A, B, K);


// AJAX:authenticate?{identity,A,M1} - 伺服器端 step (2)
// 發送 identity & A & M1 去伺服
// 之前 salt & verifier 已存在 user data 可以從 DB 取出.
// 而 b, B 則要從 session 中取出並從 session 移除.

// H(PAD(A) | PAD(B))
const u = server.generateU(A, B);
// ((A * v^u) ^ b % N)
const S = server.generateS(A, b, verifier, u);
// H(S)
const K = server.hash(S);
// H(H(N) xor H(g), H(I), s, A, B, K)
const M1Server = server.generateM1(identity, salt, A, B, K);

// 執行比較
if (!crypto.timeingSafeEquals(M1Client, M1Server)) {
  throw new Error('Invalid client session proof.');
}

// 通過

// 建立 M2 來做後續驗證
// H(A | M | K)
const M2Server = server.generateM2(A, M1Server, K);

// 伺服器回傳 M2 給客戶端
// 客戶端 step (3) (可選)

// H(A | M | K)
const M2Client = client.generateM2(A, M1Client, K);


// 比較
if (!crypto.timeingSafeEquals(M2Client, M2Server)) {
  throw new Error('Invalid server session proof.');
}

// 全部成功

一些重要注意事項

  • 不一定要使用 AJAX 實現 SRP 流程。您也可以簡單地使用表單提交來完成所有步驟。例如,您可以在網站上將帳戶和密碼分成兩個步驟,並在 Hidden Input 中存儲值。確保您在瀏覽器和伺服器快取中存儲了 a 和 b,以便在步驟之間使用它們,並確保不會意外將它們發送到另一端。

  • 驗證值 (verifier) 是根據帳戶和密碼生成的,這意味著如果用戶更改了帳戶或密碼其中一個,就必須重新創建一個新的 verifier 來替換舊的。換句話說,就算更改 Email 也需要重新輸入密碼。

  • 始終確保您不向各方發送任何不必要的值(見上面的表格),即使伺服器或客戶端忽略這些被意外送出的值,也被視為違反規範和安全漏洞。而且中間人攻擊者可以利用這些敏感數據。

  • 每次重新開始認證過程時,請始終清除所有值。通常您可以重新整理頁面,以便重置所有值和 JS 物件。假設您正在開發 SPA 應用程序,請將整個過程封裝在一個函數中,不要將值快取到任何物件屬性中,如果您使用一些 SRP 套件,當用戶重試時請始終重新創套件的物件。

  • SRP 不應取代 HTTPS,您應始終在您的應用程序上使用 SSL/TLS,並啟用 Cookies HttpOnly 和安全設定。

夏木樂的實作

夏木樂是非常重視安全,並具有專業技術的廠商。我們編寫了完全合規的 SRP-6a 函式庫,且自 2023 年開始,我們的網站完全支援以 SRP 登入認證,高度保護使用者的帳密安全。如果您的網站需要 SRP / MFA / 個資加密 等等高規格資安功能,歡迎尋找夏木樂替您量身訂做網站系統。

相關資源:

延伸閱讀:

相關文章