OK,接上文,继续分析。
飞信登录的第三部就是连接SIPC服务器,验证并获取好友列表等信息。
SIPC是什么含义呢? SIP是会话初始协议(Session initializtion Protocol),是一个标准的协议,其RFC可以在这里下载。协议的规定如何开始一个会话。通常和这个协议一起使用的还有SDP协议。
但飞信只用了SIP,并且把SIP协议做了一些拓展,所以后面有个C。C可能代表China Mobile。关于SIP协议的格式和移动在SIPC上的拓展和不同,请参照nathan2007的文章。这里就不多说了。
上文说过,飞信SIPC服务器的地址是在第一步获取自适应配置中获得,有三个配置有用。
含义 位置 结果举例
标准SIPC直连接 /config/server/sipc-proxy 221.130.46.141:8080
SSLSIPC连接 /config/server/sipc-ssl-proxy 221.130.46.141:443
HTTP代理连接 /config/server/http-tunnel HTTP://221.130.46.141/ht/sd.aspx
从上面可以看出飞信支持三种连接方式,
标准SIPC连接方式就是直接连接服务器的8080端口,SIPC信令直接放在TCP包中,
SSLSIPC连接方式连接服务器的443端口,虽说连接的是443端口,却没有使用SSL加密,仍是明文传输,不做任何处理,和标准直连接没有任何区别,只是端口号变了而已。
HTTP代理连接方式是在只能访问80端口的情况下才启用的连接方式,使用POST方式,SIPC信令就放在POST的数据包中,这个我会详细的写文章分析的。
可见飞信对网络环境的适应能力是非常强的。基本能在限制比较多的网络环境中登录成功。还有个细节不知各位注意没,标准直连接和HTTP连接是连接到同一个服务器上,这就需要这个服务器同时运行两个服务:SIPC服务和WEB服务,这对服务器的稳定性和性能要求还是比较高的。
飞信客户端连接到飞信SIPC服务器上后,在获取好友列表等信息之前是需要完成验证的。
首先是发起注册请求。(姑且就这样叫吧)
01.
R fetion.com.cn SIP-C/4.0
02.
F: 123456789
03.
I: 1
04.
Q: 1 R
05.
CN: 441F7DBA5C3153B61C0660C622F01354
06.
CL: type="pc" ,version="3.6.1860"
07.
-------
08.
SIP-C/4.0 401 Unauthoried
09.
F: 123456789
10.
I: 1
11.
Q: 1 R
12.
W: Digest algorithm="SHA1-sess-v4",nonce="11F38E891D330436110471D742A7C08E",key="CAE3B6C60FC46B7A6FE4316FBABD4E9CC21DD01E330CE449F5BA46818A51F589C7ECD548BC4F6D8AA20BDA43FC75F89164E8EB70A20348251AB56B0059452508A516C955BE1463C1B7D82ED97CEDBD03DFD1DF7C5368FF1636A34E855B10BD19B6624DC68BC921771BE8C5F1E3EE1E5EBB1DB41CF1D0CB4BA41FACC2A54D6AF9010001",signature="57F1AD6CA5082C9BAA8DE5DD5521149903E9A85E4BDC9BE89CEFE39313DF836319E546AF01FE006F40B7243EF2099D813AEC746EDAE4C4003AAA88A1DBE6302C20505784D2458F0510B596D9DC32E2BF4E609BCF18EE46822B84D6EACDD463E0833E5D1CEBF6864920E6CB126456DF9A063385AC9828A34467AEDFEFA2B347A2"
请求:这个请求主要是向服务器请求验证的RSA公钥
其中F是飞信号就是用户uri中@前面的数字,如123456789@fetion.com.cn;p=1234,I是CallId,Q是Sequence,我仍然没有找到规律。。。。(详细说明IQ)
CN:是Cnoce,是客户端随机生成的16字节的16进制表示的字符串,可能服务器需要用这个来生成RSA的密钥的吧,这个没法验证了,只能猜测
CL:是Client,发送的客户端的版本号和平台类型,固定
回复:RSA公钥和一个随机字符,用于登录验证
返回的状态码是401,需要验证
nonce:这个就是一个服务器生成随机字符串,可能根据请求中CN来生成,仅用于验证,没有含义。
key: 这个比较重要,RSA算法中的公钥,使用16进制表示,转换为字节数组后共131字节。后面的signature,也是16进制表示的字节数组,共128字节(256字符),在目前还没有发现含义,至少在登录过程中没有使用,暂且忽略。
因为飞信验证用到了RSA算法,我对算法也不是很懂,上百度google了一下,大致了解了点,可能部分朋友对RSA不熟,我也简单的说明下吧。
RSA算法是第一个能同时用于加密和数字签名的算法。安全性依赖于大素数分解。采用不对称加密和解密。
RSA可以用于数据加密。首先服务器生成一个密钥对:一个公钥和私钥,公钥用于加密,私钥用于解密。服务器保存好私钥,然后把公钥发送给客户端,客户端用这个公钥加密一些数据,并发回给服务器,服务器用刚才保存的私钥解密。公钥是公开的,任何人都可以使用公钥加密发送给服务器,但私钥是不公开的,只有公钥的发布才会持有,公钥加密的信息只有私钥才能解密。
可以看出,RSA可以保证数据在传输过程中的安全性,因为只有私钥才能解密,即使知道了公钥也没用。
当然反过来用也行,私钥加密过的数据,只有公钥才能解密,这个可以用于数字签名。
私钥的参数很多,用不上就不说了,公钥的参数有两个:
modulus:128字节 加密系数,主要的参数
publicExponent:3字节 公共系数,一般是固定的,0×010001
详细的RSA信息可以参考维基百科:http://zh.wikipedia.org/wiki/RSA%E5%8A%A0%E5%AF%86%E6%BC%94%E7%AE%97%E6%B3%95
回到飞信。飞信主要用RSA来做数据加密。在注册请求中返回的W头域中的key就是RSA的公钥,前64字节(也就是128个字符)是modulus,后面的3字节(6个字符)是publicExponent。使用这个公钥来加密用户密码,nonce, Aeskey。下面会有详细的说明。
给出从key中解析出公钥的代码
01.
/**
02.
* 从服务器返回的key字符串解析出RSA公钥
03.
* @param publicKey 服务器返回的key字符串
04.
* @return 解析出来的RSA公钥,可以用这个公钥加密数据
05.
* @throws NoSuchAlgorithmException
06.
* @throws InvalidKeySpecException
07.
*/
08.
private RSAPublicKey parsePublicKey(String publicKey) throwsNoSuchAlgorithmException, InvalidKeySpecException
09.
{
10.
String modulusText = publicKey.substring(0,0x100);
11.
String exponentText = publicKey.substring(0x100);
12.
BigInteger modulus = new BigInteger(1, ConvertHelper.hexString2ByteNoSpace(modulusText));
13.
BigInteger exponent = new BigInteger(1, ConvertHelper.hexString2ByteNoSpace(exponentText));
14.
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
15.
RSAPublicKeySpec bobPubKeySpec = new RSAPublicKeySpec(modulus, exponent);
16.
RSAPublicKey rsapublicKey = (RSAPublicKey) keyFactory.generatePublic(bobPubKeySpec);
17.
return rsapublicKey;
18.
}
接下来就是很关键的一步,SIPC验证注册
01.
R fetion.com.cn SIP-C/4.0
02.
F: 123456789
03.
I: 1
04.
Q: 2 R
05.
A: Digest response="6AC3FEE164709828DCDBA1FC71BAFE9FDD83980DA83959E0993912EA74BF836BC76F196F9C99BD71F64732C00BEEEC1A516C134B637EEFA71BBAF26447B5B310BE3BC3A58FD2E6B22094F16B1CF85F2E5B6AD5C9A60FF6055C7DD8C476A28C97C7A6876176C5EF738FC21CEACB400190B1BF538EC930429DED246F49A9CE7C90",algorithm="SHA1-sess-v4"
06.
AK: ak-value
07.
L: 428
08.
09.
<args><device machine-code="001D0936BCB6" /><caps value="1ff" /><events value="7f" /><user-info mobile-no="159xxxxxx" user-id="987654321"><personal version="0" attributes="v4default" /><custom-config version="0" /><contact-list version="0" buddy-attributes="v4default" /></user-info><credentials domains="fetion.com.cn;m161.com.cn;www.ikuwa.cn;games.fetion.com.cn" /><presence><basic value="400" desc="" /></presence></args>
10.
------
11.
SIP-C/4.0 200 OK
12.
I: 1
13.
Q: 2 R
14.
X: 600
15.
L: 12545
16.
17.
<results><client public-ip="222.210.18.145" login-place="" last-login-ip="222.210.26.134" last-login-place="" last-login-time="4/1/2010 7:55:38 PM"/>.....
上面说到验证需要用的RSA,这里的response就是用第一步返回的key进行RSA加密后的结果。加密的内容只有三个数据:第一步服务器返回的nonce,V4加密过的密码,AESkey。
注意这里的密码是指用userid和明文密码加密过后的结果(两次sha1),AESkey是AES算法的密钥,估计是加密或者解密用户配置的,没有做验证,32字节,可以随机生成就行了。
假设这里的用户密码v4加密后的结果和AESKey都是用16进制表示的,nonce别把当字符串看,
要加密的数据就是
data = hex2byteArray(password)+getUTF8ByteArray(nonce)+hex2byteArray(AESKey)
(+表示字符数组的连接)
注意nonce不是转换为字节数组,而是获取UTF8编码的字节数组。我在这里郁闷了很久。。
rsakey = parsePublicKey(key);
resByteArray = RSAencrypt(data, rsakey);
response = byteArray2Hex(resByteArray);
response就是发送给服务器的结果。
01.
/**
02.
* 生成加密结果
03.
* @param publicKey RSA公钥,从返回的W头部的key获取值,16进制表示的字节数组 67Bytes(134Chars)
04.
* @param password V4加密的密码,指用userid和明文密码加密过后的结果(两次sha1),16进制表示的字节数组 20Bytes(40Chars)
05.
* @param nonce 服务器返回的随机字符串,看做字符串 16Bytes(32Chars)
06.
* @param aeskey AES算法的密钥,估计是加密或者解密用户配置的 32Bytes(64Chars)
07.
* @return 生成的结果,16进制表示的字节数组
08.
* @throws NoSuchAlgorithmException
09.
* @throws InvalidKeySpecException
10.
*/
11.
public String generate(String publicKey, String password, String nonce, String aeskey)throws NoSuchAlgorithmException, InvalidKeySpecException
12.
{
13.
byte[] pb = ConvertHelper.hexString2ByteNoSpace(password);
14.
byte[] nb = ConvertHelper.string2Byte(nonce);
15.
byte[] ab = ConvertHelper.hexString2ByteNoSpace(aeskey);
16.
17.
byte[] res = new byte[pb.length+nb.length+ab.length];
18.
System.arraycopy(nb, 0, res, 0, nb.length);
19.
System.arraycopy(pb, 0, res, nb.length, pb.length);
20.
System.arraycopy(ab, 0, res, pb.length+nb.length, ab.length);
21.
22.
byte[] some = encrypt(parsePublicKey(publicKey), res);
23.
24.
return ConvertHelper.byte2HexStringWithoutSpace(some);
25.
}
后面的参数不是很重要,也简单说一下吧,machine-code是指的是当前活动网卡的MAC地址,可以固定。caps可能是capbilities的缩写,在HTTP连接方式下为ff,在直接连接和SSL连接方式下为1ff,user-info里面的信息可以从SSI登录成功中得到,后面一大堆的version指的是本地数据的版本,类似于版本控制,如果和服务器的版本相同就不回复相应的信息了。可以固定的设置为0,后面还有presence,这个是登录状态的,不同的取值表示的含义不同:400-在线,0-隐身,600-忙碌,100-离开,desc可以对现在这个状态加以描述,默认为空。
这里给出RSA加密方法(就是上面的encrypt()方法)
01.
/**
02.
* 使用RSA加密字节数组
03.
* @param publicKey RSA公钥
04.
* @param obj 要加密的字节数组
05.
* @return byte[] 加密后的字节数组
06.
*/
07.
protected byte[] encrypt(RSAPublicKey publicKey, byte[] obj) {
08.
if (publicKey != null) {
09.
try {
10.
Cipher cipher = Cipher.getInstance("RSA");
11.
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
12.
return cipher.doFinal(obj);
13.
} catch (Exception e){
14.
e.printStackTrace();
15.
}
16.
}
17.
return null;
18.
}
如果一切正常的话,服务器就会返回200,登录成功。返回的数据很多。有登录记录,个人信息,好友分组,好友列表,个人配置等。当然,如果你验证的时候传递了记录的版本号,如果和服务器相同的话,服务器就不会返回数据了 。因为是XML格式的,很容易理解,不赘述了。
如果服务返回的是421 Extension Required,这就需要验证了。
1.
SIP-C/4.0 421 Extension Required
2.
I: 1
3.
Q: 2 R
4.
W: Verify algorithm="picc-ChangeMachine",type="GeneralPic"
5.
L: 191
6.
7.
<results><reason text="飞信发现您本次变更了登录地点。为保证您的帐号安全,需要您输入验证码,这可以防止恶意程序的自动登录。" tips=""/></results>
需要验证的原因也给出来了。在上一篇文章中详细的说明了如何获取验证图片。这里也一样。飞信SSI登录和SIPC注册的验证图片的获取是同一个地址。
获取验证图片需要一个参数alg,这里可以从SIP返回的W头的algorithm中取得。
获取图片之后,得到了一个图片编号,即pid和图片数据,把图片解码出来保存为文件或者渲染到图片控件并让用户识别后,会得到用户输入的验证字符。
假设图片pid为6cbcdacb-44c2-4bd3-82a3-07d9e2e3f967,用户识别上面的字符为:qyxfyd。
获取到这些信息之后,就可以再一次发起注册请求,基本上和上一次的请求相同,只不过多了一个SIP头,Verify。
重复第二步:SIPC验证注册
01.
R fetion.com.cn SIP-C/4.0
02.
F: 123456789
03.
I: 1
04.
Q: 2 R
05.
A: Digest response="3C10B5F148EA52FB42441F640D235D27556920D6753624C8CDABFC0254FCDA89522A5B72FE37BC8D828BF9B7EBB1859B8BB4558D56A83115E724541B4B34316B4F56BBD76002EBDB44AC2E65FC000913E737242A12CB52A6B83A3EE6F38AD36DDEA2528667CDE547DBF57A40E7529D75096835AB621F56750B9857614836C43D",algorithm="SHA1-sess-v4"
06.
AK: ak-value
07.
A: Verify response="qyxfyd",algorithm="picc-ChangeMachine",type="GeneralPic",chid="6cbcdacb-44c2-4bd3-82a3-07d9e2e3f967"
08.
L: 436
09.
10.
<args><device machine-code="001D0936BCB6" /><caps value="1ff" /><events value="7f" /><user-info mobile-no="159xxxxxx" user-id="987654321"><personal version="0" attributes="v4default" /><custom-config version="318214543" /><contact-list version="0" buddy-attributes="v4default" /></user-info><credentials domains="fetion.com.cn;m161.com.cn;www.ikuwa.cn;games.fetion.com.cn" /><presence><basic value="400" desc="" /></presence></args>
很容易看出Verify头中,response就是用户输入的字符串,algorithm就是验证图片的算法,chid就是图片的pid。
如果很幸运,验证成功,就会和上面的返回结果一样,但假如用户识别错了,验证失败,就会返回420,如下
1.
SIP-C/4.0 420 Bad Extension
2.
F: 685592830
3.
I: 1
4.
Q: 2 R
5.
W: Verify algorithm="picc-ChangeMachine",type="GeneralPic"
6.
L: 191
7.
8.
<results><reason text="飞信发现您本次变更了登录地点。为保证您的帐号安全,需要您输入验证码,这可以防止恶意程序的自动登录。" tips=""/></results>
这也一样,继续上面的操作,获取验证图片,提示用户识别,再注册验证,直到登录成功。
当上面的验证成功之后,你当前已经是在线了,就可以向服务器发起其他请求了,比如添加好友,发送消息等等。假如还需要支持群,就需要获取群列表,群成员消息。
但这个时候还不能收到好友的在线情况的,刚才只是返回了好友的列表,好友的状态还是没有发送过来,登录之后如何处理才能获得好友状态,请留意我下篇文章。
更新记录:
2010.04.27 修正了key的长度错误,以前写的是67字节,修改为131字节,我没有仔细数。。感谢 Felix指出!!
2010.05.05 添加文中缺少的encrypt方法,感谢 supertrouper指出!!