安全遠端密碼協議,英文為 Secure Remote Password(SRP)是一種安全的認證方法,目的是在不通過網路明文傳輸密碼的情況下安全地驗證用戶的身分。
傳統的密碼雜湊 (Hashing) 是將明文密碼發送到伺服器再雜湊,然後與原本存儲的雜湊值進行比對。
而 SRP 永遠不會在伺服器上傳輸或存儲用戶的密碼,顯著降低了被攔截或伺服器被侵害的風險。它的好處是能阻止諸如 MITM(中間人攻擊)等不同類型的攻擊,不需要在用戶數據洩露後更改密碼,並允許用戶和伺服器相互確認身份。
像蘋果和 1Password 這樣的主流公司已經將 SRP 作為其認證機制的一部分。例如,蘋果在其 iCloud 鑰匙串中實施了SRP,以安全地同步跨設備的密碼而不暴露它們。同樣,1Password 使用用戶的主密碼和密鑰來加密資料,並且使用 SRP 來認證帳戶並確保它們不會傳輸到伺服器。
關於本文章
本文章主要解釋 SRP-6a 的每個步驟,並提供 RFC-5054 和 RFC-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 2945 和 RFC 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 | 乘數參數 | X | SHA1(N ‖ PAD(g)) |
s | 用戶鹽 | C<=S | random() |
v | 密碼驗證值 | X | g^x % N |
x | 鹽+身份+密碼的雜湊值。 | X | SHA1(s ‖ SHA1(I ‖ ":" ‖ P)) |
a , b | 客戶端和伺服器的私鑰 | X | random() |
A | 客戶端公鑰 | C=>S | g^a % N |
B | 伺服器公鑰 | C<=S | k*v + g^b % N |
u | 防止攻擊者得知用戶驗證值的的值 | X | H(PAD(A) ‖ PAD(B)) |
S (客戶端) | 預共享密鑰(安全的共用密鑰) | X | (B - (k * g^x)) ^ (a + (u * x)) % N |
S (伺服器) | 預共享密鑰(安全的共用密鑰) | X | (A * v^u) ^ b % N |
K | 用於生成 M 的 Session Key | X | H(S) |
M1 | Evidence message1,用於驗證雙方生成了相同的 Session Key。 | C=>S | H(H(N) XOR H(g) ‖ H(U) ‖ s ‖ A ‖ B ‖ K) |
M2 | Evidence message2,用於驗證雙方生成了相同的 Session Key。 | C<=S | H(A ‖ M ‖ K) |
使用者註冊過程
當一個應用程序(網站或手機)開始註冊流程時,可能會向用戶顯示帳戶 Identity
(I
)(用戶名或電子郵件)和密碼 Password
(P
)表單欄位。
用戶輸入帳戶和密碼後點擊註冊按鈕。SRP 客戶端將生成一個隨機鹽 Salt
(s
)以及從鹽、帳戶和密碼生成的密碼驗證值 Verifier
(v
)。
然後應用程序將只發送 Salt
, Verifier
與 Identity
到伺服器,不發送密碼。如果密碼被意外傳輸到伺服器,即使這個密碼沒有被伺服器作任何處理,依然會被視為違反規範和安全漏洞。
伺服器收到註冊請求時,可以將 Identity
與其他 User 資訊,還有 Salt
與 Verifier
保存到資料庫。如果您想在保存前加密 Salt and Verifier,請確保使用僅有伺服器知道的密鑰進行加密。
使用者登入過程
Hello 和 伺服器 Step 1
當用戶開始登錄過程時,他們可能會在表單欄位上輸入他們的帳戶和密碼,然後點擊登錄按鈕。SRP 客戶端將向伺服器發送一個帶有帳戶名稱 (Identity
) 的 Client Hello 請求。伺服器應該通過這個帳戶名稱檢查用戶是否存在,並從用戶數據獲取 Salt
和 Verifier
。
接下來,伺服器將生成一個隨機的私有 b
和公開 B
,並通過數據庫、Session 或快取記住它們,因為我們將在後續步驟中需要它們,然後將 Salt
和 B
返回給客戶端(Server Hello)。這個過程類似於握手,為雙方創建連接會話。
有些套件將客戶端 Hello 稱為 Challenge,而 B 是 Server Challenge Value。
客戶端 Step 1 and 2
接收到 B
和 Salt
後,客戶端運行 Step 1 以生成 a
, A
和 x
,然後運行 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
也應該是相同的。M1
和 M2
是確保雙方擁有相同 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 / 個資加密 等等高規格資安功能,歡迎尋找夏木樂替您量身訂做網站系統。
相關資源:
thinbus-srp (JS SRP 實作)
artisansdk/srp (PHP/JS SRP 實作)
windwalker/srp (PHP/JS SRP 實作)
延伸閱讀: