以Mangos代码为参考, 解析SRP6的原理和实现.
(转载请注明来源于金庆的专栏)
SRP全称Secure Remote Password(安全远程密码),是一个开源认证协议。
SRP简化后的原理是:
1. 服务器不保存密码或密码的散列值, 防止字典攻击.
而只是保存验证因子(verifier).
2. 客户端和服务器可以各自计算出一个会话秘钥(session key), 其值相同. 防止窃听.
参考:
Wow 服务器解析 ( http://www.cppblog.com/Jedimaster/archive/2006/10/14/13674.aspx )
SRP Protocol Design ( http://srp.stanford.edu/design.html )
魔兽世界服务器端编写参考资料 ( http://www.asstudio.de/wow/wow.htm )
RFC2954中文翻译 ( http://www.cnpaf.net/rfc/rfc2945.txt )
SRP是什么意思?_百度知道 ( http://zhidao.baidu.com/question/59783252.html )
源码 mangos/src/realmd/AuthSocket.cpp
== Mangos SRP6认证过程 ==
1. 客户端发送用户名和版本信息
struct AUTH_LOGON_CHALLENGE_C
{
uint8 cmd;
uint8 error;
uint16 size;
uint8 gamename[4];
uint8 version1;
uint8 version2;
uint8 version3;
uint16 build;
uint8 platform[4];
uint8 os[4];
uint8 country[4];
uint32 timezone_bias;
uint32 ip;
uint8 I_len;
uint8 I[1];
};
大部份信息用来决定是否封阻该用户登录.
SRP6相关的只有I, 为用户名.
SRP6相关的字段都是按协议中的符号定义的.
1.1 _SetVSFields(rI)设置v, s字段
从数据库中获取密码散列值rI(字段名sha_pass_hash), 应该是密码p,
x = H(s, p)
v = g^x (密码学中的计算一般都是在最后对大质数N取模: v = g.ModExp(x, N);)
这个应该是验证因子v.
然后v, s存入数据库. x为临时值, 用后丢弃.
salt值s是在连接时设置的随机值.
/// Accept the connection and set the s random value for SRP6
void AuthSocket::OnAccept()
{
s.SetRand(s_BYTE_SIZE * 8);
}
s是32字节长, s_BYTE_SIZE = 32.
安全大质数N, 及其生成元g, 是固定的:
N.SetHexStr("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7");
g.SetDword(7);
RFC2945:
For
maximum security, N should be a safe prime (i.e. a number of the form
N = 2q + 1, where q is also prime). Also, g should be a generator
modulo N (see [SRP] for details), which means that for any X where 0
< X < N, there exists a value x for which g^x % N == X.
为了最大化安全性,N可以是一个安全的素数
(也就是,一个类似于N=2q + 1形式的数,同时q是个素数)。
而且,g将是一个以N为模的生成元,
意味着,对任何X,有0 < X < N,存在一个值x,使得g^x % N == X。
Mangos保存了密码p, 是错误的. 服务器不应该保存密码或其散列值.
应该在创建用户时, 由客户端取s值, 计算v, 将{I, s, v}传输到服务器并保存.
登录时, 特定用户的s, v应该是固定的, 从数据库读取, 而不是每次登录时随机.
1.2 取b值, 计算B
b.SetRand(19 * 8);
BigNumber gmod=g.ModExp(b, N);
B = ((v * 3) + gmod) % N;
b为19字节长的随机数. 不知为何是19字节长, 不是16或32?
b是服务器的临时秘钥, B为临时公钥.
B = kv + g^b
在SRP6中k=3, 而在最新的SRP6a中, k=H(N, g).
1.3 服务端返回 CMD_AUTH_LOGON_CHALLENGE 数据包
返回的数据结构没有用struct定义, 只是用ByteBuffer依次填入数据.
ByteBuffer pkt;
pkt << (uint8) AUTH_LOGON_CHALLENGE;
pkt << (uint8) 0x00;
pkt << (uint8)REALM_AUTH_SUCCESS;
pkt.append(B.AsByteArray(32), 32); // 32 bytes
pkt << (uint8)1;
pkt.append(g.AsByteArray(), 1);
pkt << (uint8)32;
pkt.append(N.AsByteArray(), 32);
pkt.append(s.AsByteArray(), s.GetNumBytes()); // 32 bytes
pkt.append(unk3.AsByteArray(), 16);
pkt << (uint8)0; // Added in 1.12.x client branch
SendBuf((char const*)pkt.contents(), pkt.size());
B, g, N, s 是服务器发给客户端的SRP6参数.
unk3是个16字节长的随机数, 不知道干什么用的. (unknown3?)
按SRP6的协议, 应该是客户端先发送自己的用户名和公钥(I, A), 但在Mangos中,
是服务器在没有收到A时就发送盐值和自己的公钥(s, B).
这个次序应该无关紧要. 这样做的原因是服务器要先发送N, g到客户端, 这样可少一次消息交互.
客户端计算公钥A时要用到N, g: A = g^a (隐含对N取模).
2. 客户端发送 CMD_AUTH_LOGON_PROOF 数据包请求验证
struct AUTH_LOGON_PROOF_C
{
uint8 cmd;
uint8 A[32];
uint8 M1[20];
uint8 crc_hash[20];
uint8 number_of_keys;
uint8 unk; // Added in 1.12.x client branch
};
A, M1有用
2.1 计算u, S, K
u = sha(A, B);
S = (A * (v.ModExp(u, N))).ModExp(b, N);
K = H(S);
其中K分奇偶位分别计算, 应该不是SRP的方法, 不知是否会降低散列效果.
2.2 计算M并与M1比较验证
M = sha(sha(N) xor sha(g), sha(I), s, A, B, K)
2.3 M1验证通过后计算M2, 用于客户端验证
M2 = sha(A, M, K)
2.4 服务端发回 CMD_AUTH_LOGON_PROOF
包含了 SRP6 验证的结果 M2
struct AUTH_LOGON_PROOF_S
{
uint8 cmd;
uint8 error;
uint8 M2[20];
uint32 unk1;
uint32 unk2;
uint16 unk3;
};