SRP(远程密码安全)在网游登录服的应用
2010-06-25 15:22
337 查看
1.关于SRP的资源
SRP协议是由斯坦福大学计算机科学系的Thomas wu等开发的,英文全称是Security Remote Password(远程密码安全),经过严密的数学论证,SRP被证明是一种非常安全的算法,我们可以在获取到SRP的协议的官方文档 http://srp.stanford.edu/ .当下流行的网络游戏魔兽世界采用的就是SRP6协议.
2.原理,设计
原理具体讲解可参看上面的官方文档,设计在http://srp.stanford.edu/design.html。
3.代码
附上一个SRP6的C语言简单实现。
4.具体应用
a)Wow登录时的SRP6认证
Wow 的服务器有两部分组成: Logon Server (以下简称 LS )和 Realm Server (以下简称 RS )。 LS 接受来自 Wow 客户端的连接,主要有以下几步完成:
检查客户端版本区域等信息,检察账号密码
开始 / 继续传送 Patch (如果有)
与客户端进行 SRP6 的加密会话,把生成的密匙写入数据库
根据客户端请求发送 Realms 列表
当客户端选择好 Realms 后,客户端就从 LS 断开,连接到 RS 上:
认证,使用刚才生成的客户端密匙
如通过,进行游戏循环的交互
RS 和 LS 使用相同的数据库, SRP6 密匙被 LS 生成并写入 DB 后还要由 RS 读取出来进行下一步的认证。
Logon Server 详解
基本的连接过程如下:
客户端准备连接,发送 CMD_AUTH_LOGON_CHALLENGE 数据包,包含了所有登陆所需要的数据比如用户名密码等
服务端返回 CMD_AUTH_LOGON_CHALLENGE 数据包,填充字段包括有效验证,以及计算好的服务端 SRP6 数据
如果有效,客户端发送 CMD_AUTH_LOGON_PROOF 数据包,并把自己计算的 SRP6 数据填充进去
服务端进行验证,发送回 CMD_AUTH_LOGON_PROOF ,包含了 SRP6 验证的结果
如果一切正常,客户端发送 CMD_REALM_LIST 数据包,请求发送有效的 Realm
服务器回复 CMD_REALM_LIST 数据报,并填充过客户端需要的 Realm 数据
客户端的 Realm 列表每隔 3-4 秒就会从服务器端刷新一次。
这个 SPR6 是一种什么样的加密手段呢?以前我也没有用过,看得最多的是 MD5SHA 等 hash 算法。 SPR 算法吸取了 EKE 类型算法的优点进行了改进,非常适合于网络的认证服务,如果我没有记错, J2EE 包含了这个算法的实现。下面简单介绍一下 SRP6a 运作机制,原文见这里。
N N = 2q + 1 , q 是一个素数,下面所有的取模运算都和这个 N 有关
g 一个 N 的模数,应该是 2 个巨大的素数乘得来
k k = H(N,G) 在 SRP6 中 k = 3
s User’s Salt
I 用户名
p 明文密码
H() 单向 hash 函数
^ 求幂运算
u 随机数
a,b 保密的临时数字
A,B 公开的临时数字
x 私有密匙(从 p 和 s 计算得来)
v 密码验证数字
其中 x = H(s,p) 和 v = g ^ x , s 是随机选择的, v 用来将来验证密码。
主机将 { I,s,v } 存入数据库。认证的过程如下:
客户向主机发送 I , A = g ^ a ( a 是一个随机数)
主机向客户发送 s , B = kv + g^b (发送 salt , b 是一个随机数字)
双方同时计算 u = H(A,B)
客户计算机算 x = H(s,p) (开始 hash 密码), S = ((B - kg^x) ^ (a + ux) ) , K = H(S) ,(开始计算会话 Key )
主机计算 S = (Av^u)^b , K = H(S) ,也生成会话 Key
为了完成认证,双方交换 Key ,各自进行如下的计算:
客户接收到来自主机的 key 后,计算 H(A,M,K)
同理,主机计算 M = H(H(N) xor H(g), H(I), s, A, B, K) ,验证是否合自己储存的数值匹配。至此完成验证过程。
Realm Server 详解
从 LS 断开后,开始和 RS 认证:
连接到 RS ,向服务器发送 SMSG_AUTH_CHALLENGE 数据包,包含上次所用的随机种子
服务器发送回 SMSG_AUTH_CHALLENG 。客户端从服务器端发送回来的种子和 SRP6 数据中产生随机种子,生成 SHA1 字符串,用这些数据生成 CMSG_AUITH_SESSION 数据包,发送给服务端。
需要注意的是,这个过程是没有经过加密的。当服务端收到认证回复后,通过客户端产生的种子也生成一个 SHA1 串和来自客户端的进行对比,如果相同,一切 OK 。
下面看一下对账号创建的角色等操作进行分析。一个账号最多可以建 50 个角色吧,我还没有玩过,只是看了一下 Manual 。
客户端发送一个CMSG_CHAR_ENUM数据包请求接受角色
服务端发送回包含所有角色信息的 CMSG_CHAR_ENUM 数据包
这里客户端可以对这些角色进行操作了, CMSG_CHAR_CREATE , CMSG_CHAR_DELETE , CMSG_CHAR_PLAYER_LOGIN
角色登陆完成后,服务器发送回 SMSG_CHAR_DATA 数据包
在游戏循环中是如何操作的呢?
如果玩家立刻退出游戏,那么客户端发送 CMSG_PLAYER_LOGOUT ,服务器回复 SMSG_LOGOUT_COMPLETE
如果玩家选择稍后退出游戏,发送 CMSG_LOGOUT_REQUEST 。服务端回复 SMSG_LOGOUT_RESPONSE 。如果玩家在倒计时阶段退出,发送 CMSG_PLAYER_LOGOUT ,那么玩家的角色依旧等倒计时完成后再退出。
如果玩家中断了退出继续游戏,发送 CMSG_LOGOUT_CANCEL ,服务器回复 SMSG_LOGOUT_CANCEL_ACK 。
b)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;
};
SRP协议是由斯坦福大学计算机科学系的Thomas wu等开发的,英文全称是Security Remote Password(远程密码安全),经过严密的数学论证,SRP被证明是一种非常安全的算法,我们可以在获取到SRP的协议的官方文档 http://srp.stanford.edu/ .当下流行的网络游戏魔兽世界采用的就是SRP6协议.
2.原理,设计
原理具体讲解可参看上面的官方文档,设计在http://srp.stanford.edu/design.html。
3.代码
附上一个SRP6的C语言简单实现。
#include <stdlib.h> #include <stdio.h> #include <memory.h> #include <string.h> // Multiple-precision modular arithmetic // package to support SRP-6 server and clients // Author: Karl Malbrain, malbrain@yahoo.com // configure package for 1024 bit operations #define SIZE (1024 / 32) #ifdef unix #define __int64 long long #else typedef unsigned int uint; typedef unsigned long ulong; #endif typedef unsigned char uchar; typedef unsigned __int64 uint64; // 1024 bit modulus taken from IETF // draft-ietf-tls-srp-10 unsigned long DHmod[SIZE] = { 0xEEAF0AB9, 0xADB38DD6, 0x9C33F80A, 0xFA8FC5E8, 0x60726187, 0x75FF3C0B, 0x9EA2314C, 0x9C256576, 0xD674DF74, 0x96EA81D3, 0x383B4813, 0xD692C6E0, 0xE0D5D8E2, 0x50B98BE4, 0x8E495C1D, 0x6089DAD1, 0x5DC7D7B4, 0x6154D6B6, 0xCE8EF4AD, 0x69B15D49, 0x82559B29, 0x7BCF1885, 0xC529F566, 0x660E57EC, 0x68EDBC3C, 0x05726CC0, 0x2FD4CBF4, 0x976EAA9A, 0xFD5138FE, 0x8376435B, 0x9FC61D2F, 0xC0EB06E3 }; // return 1 if multiple precision number is zero // used to verify A & B are non-zero int mpzero (ulong *a) { int idx = SIZE; while( idx-- ) if( *a++ ) return 0; return 1; } // return remainder of double sized product u/DHmod in u void mpmod (ulong *u) { int i, j, n = SIZE, m = SIZE * 2; uint64 quot, rem, nxt, prod; __int64 cry; while( m ) if( !u[0] ) m--, u++; else break; nxt = 0; for( i = 0; i <= m - n; i++ ) { nxt <<= 32; nxt |= u[i]; rem = nxt % DHmod[0]; quot = nxt / DHmod[0]; while( quot == 0x100000000 || rem < 0x100000000 && (quot * DHmod[1]) > (rem << 32 | u[i + 1]) ) quot -= 1, rem += DHmod[0]; prod = cry = 0; if( quot ) for( j = n; j--; ) { prod += quot * DHmod[j]; cry += (uint64)u[i + j] - (ulong)prod; u[i + j] = (ulong)cry; prod >>= 32; cry >>= 32; } nxt = u[i]; if( !i ) if( cry - prod ) quot++; else continue; else if( u[i - 1] += cry - prod ) quot++; else continue; cry = 0; for( j = n; j--; ) { cry += (uint64)u[i + j] + DHmod[j]; u[i + j] = (ulong)cry; cry >>= 32; } if( i ) u[i - 1] += (ulong)cry; nxt = u[i]; } } // multiply two SIZE numbers into double SIZED result void mpmult (ulong *dest, ulong *what, ulong *by) { int m, n = SIZE; uint64 fact; uint64 cry; memset (dest, 0, SIZE * 2 * sizeof(ulong)); while( n-- ) { cry = 0; if( fact = what ) for( m = SIZE; m--; ) { cry += fact * by[m] + dest[n + m + 1]; dest[n + m + 1] = (ulong)cry; cry >>= 32; } dest = (ulong)cry; } } // exponentiate SIZE base by SIZE exponent // to SIZE result, modulo DHmod void mpexp (ulong *result, ulong *base, ulong *exponent) { ulong prod[SIZE * 2], term[SIZE]; int idx = SIZE * 32; memset (result, 0, SIZE * sizeof(ulong)); memcpy (term, base, SIZE * sizeof(ulong)); result[SIZE - 1] = 1; while( idx ) if( *exponent ) break; else exponent++, idx -= 32; while( idx-- ) { if( exponent[idx / 32] & (1 << (31 - (idx & 0x1f))) ) { mpmult (prod, result, term); mpmod(prod); memcpy (result, prod + SIZE, SIZE * sizeof(ulong)); } mpmult (prod, term, term); mpmod(prod); memcpy (term, prod + SIZE, SIZE * sizeof(ulong)); } } // multiply a by b (single precision multiplier) // and add to dest void mpmpyadd (ulong *dest, ulong *a, ulong b) { ulong result[SIZE * 2]; __int64 cry = 0; int idx = SIZE; while( idx-- ) { cry += dest[idx]; cry += a[idx] * (uint64)b; result[idx + SIZE] = cry; result[idx] = 0; cry >>= 32; } // normalize result result[SIZE - 1] = cry; mpmod (result); memcpy (dest, result + SIZE, SIZE * sizeof(ulong)); } // multiply a by b (single precision multiplier) // and subtract from dest void mpmpysub (ulong *dest, ulong *a, ulong b) { ulong result[SIZE * 2]; __int64 cry = 0; int idx = SIZE; // multiply a by b while( idx-- ) { cry += a[idx] * (uint64)b; result[idx + SIZE] = cry; result[idx] = 0; cry >>= 32; } // normalize result result[SIZE - 1] = cry; mpmod (result); // subtract result from dest for( cry = 0, idx = SIZE; idx--; ) { cry += dest[idx]; cry -= result[idx + SIZE]; dest[idx] = cry; cry >>= 32; } // normalize result if( cry < 0 ) for( cry = 0, idx = SIZE; idx--; ) { cry += dest[idx]; cry += DHmod[idx]; dest[idx] = cry; cry >>= 32; } } // SHA 256 routines // taken from Wikipedia pseudo code //2^32 times the cube root of the first 64 primes 2..311 static ulong k[64] = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 }; // store 64 bit integer void putlonglong (uint64 what, uchar *where) { *where++ = what >> 56; *where++ = what >> 48; *where++ = what >> 40; *where++ = what >> 32; *where++ = what >> 24; *where++ = what >> 16; *where++ = what >> 8; *where++ = what; } // store 32 bit integer void putlong (ulong what, uchar *where) { *where++ = what >> 24; *where++ = what >> 16; *where++ = what >> 8; *where++ = what; } // retrieve 32 bit integer ulong getlong (uchar *where) { ulong ans; ans = *where++ << 24; ans |= *where++ << 16; ans |= *where++ << 8; ans |= *where++; return ans; } // right rotate bits ulong rotate (ulong what, int bits) { return (what >> bits) | (what << (32 - bits)); } // right shift bits ulong shift (ulong what, int bits) { return what >> bits; } // private structure for SHA typedef struct { uchar buff[512/8]; // buffer, digest when full ulong h[256/32]; // state variable of digest uint64 length; // number of bytes in digest int next; // next buffer available } SHA256; // start new SHA run void sha256_begin (SHA256 *sha) { sha->length = 0; sha->next = 0; // 2^32 times the square root of the first 8 primes 2..19 sha->h[0] = 0x6a09e667; sha->h[1] = 0xbb67ae85; sha->h[2] = 0x3c6ef372; sha->h[3] = 0xa54ff53a; sha->h[4] = 0x510e527f; sha->h[5] = 0x9b05688c; sha->h[6] = 0x1f83d9ab; sha->h[7] = 0x5be0cd19; } // digest SHA buffer contents // to state variable void sha256_digest (SHA256 *sha) { ulong nxt, s0, s1, maj, t0, t1, ch; ulong a,b,c,d,e,f,g,h; ulong w[64]; int i; sha->next = 0; for( i = 0; i < 16; i++ ) w[i] = getlong (sha->buff + i * sizeof(ulong)); for( i = 16; i < 64; i++ ) { s0 = rotate(w[i-15], 7) ^ rotate(w[i-15], 18) ^ shift(w[i-15], 3); s1 = rotate(w[i-2], 17) ^ rotate(w[i-2], 19) ^ shift (w[i-2], 10); w[i] = w[i-16] + s0 + w[i-7] + s1; } a = sha->h[0]; b = sha->h[1]; c = sha->h[2]; d = sha->h[3]; e = sha->h[4]; f = sha->h[5]; g = sha->h[6]; h = sha->h[7]; for( i = 0; i < 64; i++ ) { s0 = rotate (a, 2) ^ rotate (a, 13) ^ rotate (a, 22); maj = (a & b) ^ (b & c) ^ (c & a); t0 = s0 + maj; s1 = rotate (e, 6) ^ rotate (e, 11) ^ rotate (e, 25); ch = (e & f) ^ (~e & g); t1 = h + s1 + ch + k[i] + w[i]; h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t0 + t1; } sha->h[0] += a; sha->h[1] += b; sha->h[2] += c; sha->h[3] += d; sha->h[4] += e; sha->h[5] += f; sha->h[6] += g; sha->h[7] += h; } // add to current SHA buffer // digest when full void sha256_next (SHA256 *sha, uchar *what, int len) { while( len-- ) { sha->length++; sha->buff[sha->next] = *what++; if( ++sha->next == 512/8 ) sha256_digest (sha); } } // finish SHA run, output 256 bit result void sha256_finish (SHA256 *sha, uchar *out) { int idx; // trailing bit pad sha->buff[sha->next] = 0x80; if( ++sha->next == 512/8 ) sha256_digest (sha); // pad with zeroes until almost full // leaving room for length, below while( sha->next != 448/8 ) { sha->buff[sha->next] = 0; if( ++sha->next == 512/8 ) sha256_digest (sha); } // n.b. length doesn't include padding from above putlonglong (sha->length * 8, sha->buff + 448/8); sha->next += sizeof(uint64); // must be full now sha256_digest (sha); // output the result, big endian for( idx = 0; idx < 256/32; idx++ ) putlong (sha->h[idx], out + idx * sizeof(ulong)); } #ifdef STANDALONE // proof-of-concept implementation of SHA-6 // calculate shared secret S three ways using // variables known to client, server, both // AES key can be taken from a HASH of S prt (ulong *x) { char buff[SIZE*8 + 1]; int idx = 0; while( idx < SIZE ) sprintf (buff + idx * 8,"%.8x", x[idx]), idx++; for( idx = 0; idx < SIZE*8; idx++ ) if( buff[idx] > '0' ) break; printf ("%s/n", buff + idx); } ulong lrand () { static ulong seed = 0xdeadbeef; return seed = (seed + 23 ) * 65537; } #define HASH (256/8) main (int argc, char **argv) { ulong S2[SIZE], s1[SIZE], s2[SIZE], s3[SIZE]; ulong x[SIZE], g[SIZE], v[SIZE], u[SIZE]; ulong S1[SIZE], t[SIZE]; ulong A[SIZE], a[SIZE]; ulong B[SIZE], b[SIZE]; ulong prod[SIZE * 2]; uchar M1[HASH], M2[HASH]; SHA256 sha[1]; ulong salt[1]; char *user = "my user_id"; char *pwd = "my password"; int i; // select random numbers a, b // n.b. use a cryptographically secure // source of random numbers in any // actual implementation of the SRP-6 // protocol for( i = 0; i < SIZE; i++ ) { a[i] = lrand(); b[i] = lrand(); } // step zero client *salt = lrand(); // step two client sha256_begin( sha ); sha256_next( sha, (uchar *)salt, sizeof(ulong)); sha256_next( sha, user, strlen(user) ); sha256_next( sha, pwd, strlen(pwd) ); memset (x, 0, sizeof(x)); sha256_finish( sha, (uchar *)(x + SIZE) - HASH); memset (g, 0, sizeof(g)); g[SIZE - 1] = 2; mpexp (v, g, x); // step three client mpexp (A, g, a); // step three server mpexp (B, g, b); mpmpyadd (B, v, 3); memset (u, 0, sizeof(u)); sha256_begin( sha ); sha256_next( sha, (uchar *)A, SIZE * sizeof(ulong)); sha256_next( sha, (uchar *)B, SIZE * sizeof(ulong)); sha256_finish( sha, (uchar *)(u + SIZE) - HASH); // step four/five client -- first result sha256_begin( sha ); sha256_next( sha, (uchar *)A, SIZE * sizeof(ulong)); sha256_next( sha, (uchar *)B, SIZE * sizeof(ulong)); mpmpysub (B, v, 3); // re-use v as g^x mpexp (s1, B, a); mpexp (s2, B, u); mpexp (s3, s2, x); mpmult (prod, s1, s3); mpmod(prod); // normalize S version one prt (prod + SIZE); sha256_next( sha, (uchar *)(prod + SIZE), SIZE * sizeof(ulong)); sha256_finish( sha, M1); // step five server -- second result mpexp (t, v, u); mpmult(prod, A, t); mpmod(prod); mpexp (S2, prod + SIZE, b); // compute S version two prt (S2); sha256_begin( sha ); sha256_next( sha, (uchar *)A, SIZE * sizeof(ulong)); sha256_next( sha, (uchar *)B, SIZE * sizeof(ulong)); sha256_next( sha, (uchar *)S2, SIZE * sizeof(ulong)); sha256_finish( sha, M2); // generic calc of S -- third result mpexp (s1, g, a); mpexp (t, s1, b); mpexp (s1, g, x); mpexp (s2, s1, u); mpexp (s3, s2, b); mpmult (prod, t, s3); mpmod(prod); prt (prod + SIZE); } #endif
4.具体应用
a)Wow登录时的SRP6认证
Wow 的服务器有两部分组成: Logon Server (以下简称 LS )和 Realm Server (以下简称 RS )。 LS 接受来自 Wow 客户端的连接,主要有以下几步完成:
检查客户端版本区域等信息,检察账号密码
开始 / 继续传送 Patch (如果有)
与客户端进行 SRP6 的加密会话,把生成的密匙写入数据库
根据客户端请求发送 Realms 列表
当客户端选择好 Realms 后,客户端就从 LS 断开,连接到 RS 上:
认证,使用刚才生成的客户端密匙
如通过,进行游戏循环的交互
RS 和 LS 使用相同的数据库, SRP6 密匙被 LS 生成并写入 DB 后还要由 RS 读取出来进行下一步的认证。
Logon Server 详解
基本的连接过程如下:
客户端准备连接,发送 CMD_AUTH_LOGON_CHALLENGE 数据包,包含了所有登陆所需要的数据比如用户名密码等
服务端返回 CMD_AUTH_LOGON_CHALLENGE 数据包,填充字段包括有效验证,以及计算好的服务端 SRP6 数据
如果有效,客户端发送 CMD_AUTH_LOGON_PROOF 数据包,并把自己计算的 SRP6 数据填充进去
服务端进行验证,发送回 CMD_AUTH_LOGON_PROOF ,包含了 SRP6 验证的结果
如果一切正常,客户端发送 CMD_REALM_LIST 数据包,请求发送有效的 Realm
服务器回复 CMD_REALM_LIST 数据报,并填充过客户端需要的 Realm 数据
客户端的 Realm 列表每隔 3-4 秒就会从服务器端刷新一次。
这个 SPR6 是一种什么样的加密手段呢?以前我也没有用过,看得最多的是 MD5SHA 等 hash 算法。 SPR 算法吸取了 EKE 类型算法的优点进行了改进,非常适合于网络的认证服务,如果我没有记错, J2EE 包含了这个算法的实现。下面简单介绍一下 SRP6a 运作机制,原文见这里。
N N = 2q + 1 , q 是一个素数,下面所有的取模运算都和这个 N 有关
g 一个 N 的模数,应该是 2 个巨大的素数乘得来
k k = H(N,G) 在 SRP6 中 k = 3
s User’s Salt
I 用户名
p 明文密码
H() 单向 hash 函数
^ 求幂运算
u 随机数
a,b 保密的临时数字
A,B 公开的临时数字
x 私有密匙(从 p 和 s 计算得来)
v 密码验证数字
其中 x = H(s,p) 和 v = g ^ x , s 是随机选择的, v 用来将来验证密码。
主机将 { I,s,v } 存入数据库。认证的过程如下:
客户向主机发送 I , A = g ^ a ( a 是一个随机数)
主机向客户发送 s , B = kv + g^b (发送 salt , b 是一个随机数字)
双方同时计算 u = H(A,B)
客户计算机算 x = H(s,p) (开始 hash 密码), S = ((B - kg^x) ^ (a + ux) ) , K = H(S) ,(开始计算会话 Key )
主机计算 S = (Av^u)^b , K = H(S) ,也生成会话 Key
为了完成认证,双方交换 Key ,各自进行如下的计算:
客户接收到来自主机的 key 后,计算 H(A,M,K)
同理,主机计算 M = H(H(N) xor H(g), H(I), s, A, B, K) ,验证是否合自己储存的数值匹配。至此完成验证过程。
Realm Server 详解
从 LS 断开后,开始和 RS 认证:
连接到 RS ,向服务器发送 SMSG_AUTH_CHALLENGE 数据包,包含上次所用的随机种子
服务器发送回 SMSG_AUTH_CHALLENG 。客户端从服务器端发送回来的种子和 SRP6 数据中产生随机种子,生成 SHA1 字符串,用这些数据生成 CMSG_AUITH_SESSION 数据包,发送给服务端。
需要注意的是,这个过程是没有经过加密的。当服务端收到认证回复后,通过客户端产生的种子也生成一个 SHA1 串和来自客户端的进行对比,如果相同,一切 OK 。
下面看一下对账号创建的角色等操作进行分析。一个账号最多可以建 50 个角色吧,我还没有玩过,只是看了一下 Manual 。
客户端发送一个CMSG_CHAR_ENUM数据包请求接受角色
服务端发送回包含所有角色信息的 CMSG_CHAR_ENUM 数据包
这里客户端可以对这些角色进行操作了, CMSG_CHAR_CREATE , CMSG_CHAR_DELETE , CMSG_CHAR_PLAYER_LOGIN
角色登陆完成后,服务器发送回 SMSG_CHAR_DATA 数据包
在游戏循环中是如何操作的呢?
如果玩家立刻退出游戏,那么客户端发送 CMSG_PLAYER_LOGOUT ,服务器回复 SMSG_LOGOUT_COMPLETE
如果玩家选择稍后退出游戏,发送 CMSG_LOGOUT_REQUEST 。服务端回复 SMSG_LOGOUT_RESPONSE 。如果玩家在倒计时阶段退出,发送 CMSG_PLAYER_LOGOUT ,那么玩家的角色依旧等倒计时完成后再退出。
如果玩家中断了退出继续游戏,发送 CMSG_LOGOUT_CANCEL ,服务器回复 SMSG_LOGOUT_CANCEL_ACK 。
b)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;
};
相关文章推荐
- MySQL与安全:ACLs、账户、密码、权限、远程登录
- 搭建ssh无密码远程登录的安全(ssl)shell script
- SRP6针对于网游登录服的应用
- SRP6针对于网游登录服的应用
- Android应用安全实践 -- 某些Android应用在客户端明文存储登录密码
- system用户远程登录em管理错误--用户名密码无效
- mstsc保存用户名和密码,实现自动登录远程桌面
- windows下crywin SSH免密码登录到远程linux服务器详解
- ssh高级安全登录--开启密钥认证,禁用密码登录
- 限制root账户不能使用密码只能使用密钥远程登陆或直接不允许远程登录
- 远程登录修改密码
- linux使用mysql(卸载,安装,修改密码,远程登录)
- 利用ssh-copy-id无需密码登录远程服务器
- linux远程登录ssh免密码
- CentOS SSH安全和配置无密码登录
- 修改密码后退回到登录界面,再按返回键退出应用,关闭所有的Activity
- 密码字段出现在一个不安全的 http:// 页面中。这是一个可能导致用户登录凭据被窃取的安全风险。
- Oracle的密码文件及远程SYSDBA登录
- Linux安全小命令-限制远程root登录