介绍
安全远程密码 (SRP) 协议是一种安全身份验证方法,旨在安全地验证用户的凭据,而无需通过网络传输密码。
与传统的密码哈希不同,传统的密码哈希依赖于将密码发送到服务器,然后与存储的哈希进行比较,SRP 永远不会在服务器上传输或存储用户的密码,从而大大降低了拦截或服务器泄露的风险。好处是它可以阻止不同类型的攻击,如 MITM(中间人)攻击,如果用户数据泄露,则不需要更改密码,并让用户和服务器相互确认身份。
Apple 和 1Password 等大公司已将 SRP 作为其身份验证机制的一部分。例如,苹果公司在其iCloud钥匙串中实施了SRP,以安全地跨设备同步密码,而不会暴露密码。同样,1Password 使用用户的主密码和密钥来加密数据,密码和密钥都使用 SRP 来验证帐户并确保它们不会传输到服务器。
关于本文
本文旨在解释 SRP-6a 的每个步骤,并综合概述 RFC-5054 和 RFC-2945。它还映射了本文后面的变量简介和示例代码中的每个计算公式。这对于刚接触 SRP 并希望了解它在 RFC 中如何实现的开发人员特别有用,确保他们可以遵循提到的每个计算步骤。
并非所有开源软件包都完全遵循 RFC 标准,这意味着有些软件包可能无法与其他软件包很好地配合使用。本文严格遵循 SRP-6a,以 windwalker/srp 中的实现为模型。通过遵循此示例,您的实现应该可以轻松与完全符合 RFC 的其他包一起使用。
如果您有兴趣查找更多完全实现 SRP-6a 的软件包,请参阅此 SRP 实现列表。
SRP 流程
SRP-6a 的定义和流程分散在 RFC 2945 和 RFC 5054 中;这是将它们集成起来进行概述的尝试。请严格遵守 RFC 指定的程序,不要进行自定义修改,并且不要不必要地传输任何变量,以避免安全漏洞。
定义
变量 | 名字 | 发送 | 计算 |
I ,identity | 主要标识(用户名或电子邮件)。 | C=>S | |
N | 一个大的安全素数,所有的算术都是模 N 完成的。 | X | |
g | 生成器模 N | X | |
k | 乘数参数 | X | SHA1(N ‖ PAD(g)) |
s | 用户 salt。 | 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 的会话密钥哈希 | X | H(S) |
M1 | 证据消息 1,验证双方生成了相同的会话密钥。 | C=>S | H(H(N) XOR H(g) ‖ H(U) ‖ s ‖ A ‖ B ‖ K) |
M2 | 证据消息 2,验证双方生成了相同的会话密钥。 | C<=S | H(A ‖ M ‖ K) |
注册
当应用程序(Web/移动设备)启动注册流程时,它可能会向用户显示 ()(用户名或电子邮件)和 () 字段。他们输入用户名和密码,然后单击注册按钮。SRP 客户端将生成一个随机 () 和一个密码 (),该密码由 salt、identity 和 password 生成。identity
I
password
P
salt
s
verifier
v
然后应用程序将只发送 ,和 发送到服务器,而不发送 。如果原始密码被意外传输到服务器,即使它被服务器忽略,这也是协议冲突和安全错误。salt
verifier
identity
password
当服务器收到注册请求时,可以将用户信息和 保存到 DB。如果要在保存之前加密 salt 和验证程序,则这是可选的,请确保使用只有服务器知道的密钥对其进行加密。salt
verifier
登录
Hello 和服务器 step1
当用户开始登录过程时,他们可以在表单字段中输入其身份和密码,然后单击登录按钮。SRP 客户端将向服务器发送 Hello 请求。服务器应通过此标识检查用户是否存在,并从用户数据中获取和获取。接下来,服务器将生成一个随机的 private 和一个 public ,并通过数据库、会话或缓存存储记住它们,我们将在后续步骤中需要它们,然后,将 返回给客户端(Server Hello)。此过程类似于握手,为双方创建连接会话。identity
salt
verifier
b
B
salt
B
某些包将客户端 Hello 作为操作调用,并且是服务器质询值。
challenge
B
客户端步骤 1 和 2
收到 and 后,客户端运行步骤 1 生成 and,然后运行步骤 2 使用上述所有值生成客户端证明。它将通过(身份验证操作)发送到服务器。服务器端也使用所有生成的值来生成并进行比较。如果比较失败,服务器将报告错误,如果比较成功,服务器将生成服务器证明并返回给客户端。在此步骤中,身份验证操作已完成,您只需将用户重定向到登录成功页面即可。B
salt
a
A
M1
A
M1
M2
有一个可选的客户端步骤 3 是您可以验证授权服务器是否受信任,并确保双方生成相同的会话密钥 ()。如果您已完成此步骤3,则表示您已完成身份验证握手并执行了双向身份验证。如果要运行 step3 以完成所有进程,可以在 step3 完成后重定向用户。M2
S
关于和S
M
当客户端和服务器生成时,它们都会生成一个预主密钥()。即使双方没有发送给另一方,也应该是相同的。和是一个验证器,用于确保双方具有相同的 .因此,如果您将来想执行其他加密行为,则可以是受信任的会话密钥或加密密钥。M
S
S
S
M1
M2
S
S
示例代码
下面是一个伪代码,用于显示 SRP 服务器端和客户端交叉的工作原理。
const server = SRPServer.create(); const client = SRPClient.create(); // Register const identity = '...'; const password = '...'; // Register: generate new 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); // Send salt and verifier to Server store // Login start // AJAX:hello?{identity} - Server step (1) // salt & verifier has already stored on user data, server can get it from DB // b & B must remember on session, we will use it at following steps. // random() const b = server.generateRandomSecret(); // ((k*v + g^b) % N) const B = server.generateB(b, verifier); // Server returns B & salt to client // Client 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); // Client 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} - Server step (2) // Send identity & A & M1 to server and compare it. // The salt & verifier stored on user data, get it from DB. // The b, B stored in session state, get and clear them. // 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); // Do compare if (!crypto.timeingSafeEquals(M1Client, M1Server)) { throw new Error('Invalid client session proof.'); } // Now create a M2 as server proof // H(A | M | K) const M2Server = server.generateM2(A, M1Server, K); // Server returns M2 to Client // Client step (3) (optional) // H(A | M | K) const M2Client = client.generateM2(A, M1Client, K); // Do compare if (!crypto.timeingSafeEquals(M2Client, M2Server)) { throw new Error('Invalid server session proof.'); } // If all passed, should not throw any exceptions.
一些重要提示
- 无需使用 AJAX 来实现 SRP 流。您可以简单地使用表单发布来完成所有步骤。例如,您可以在网站上将用户名和密码分成 2 个步骤,并将值存储在隐藏的输入中。确保您在浏览器和服务器缓存中存储并可以使用它们,并且不会意外地将它们发送到远程端。
ab
- 是从身份和密码生成的,这意味着如果用户更改了 或 。
verifieridentitypassword
- 始终确保不要向每一端发送任何不必要的值,即使服务器或客户端忽略它们,也会被视为协议违规和安全错误。此外,MITM 攻击者可以使用这些敏感数据。
- 重新启动身份验证进程时,请始终清除值。通常,您可以重新加载页面,以便重置所有值和 JS 对象。如果要开发 SPA 应用,请将整个过程包装在函数中,并且不要将值缓存到对象属性中,如果使用的是 SRP 库,请始终在用户重试时重新创建库对象。
- SRP 不应取代 HTTPS,应始终在应用上使用 SSL/TLS,并启用 Cookie HttpOnly 和安全设置。
相关链接
- thinbus-srp(JS SRP 实现)
- artisansdk/srp(PHP/JS SRP 实现)
- windwalker/srp(PHP/JS SRP 实现)
- 更多 SRP 实施列表