MSNP18协议分析(二)— MSN登录身份认证

这一篇开始主要介绍MSN登录部分的协议分析,总体来说,登陆这一块是整个MSNP协议的一大块,也是比较复杂的一部分。整个登录的过程主要包括:连接服务器,身份验证,获取用户信息和联系人列表,把联系人列表发送给服务器,发送个人信息和状态,上线通知。我也打算按照登录的顺序去进行介绍。

这里我主要关注的是如何成功的登录,但是因为大多数都是抓包,也没有权威的官方资料参考,所以不能保证完全正确和详细,有些也不能给出合理的解释,但还是尽可能的介绍登录中会出现的各种情况,以及登录中用到的命令。

在使用命令之前,我们需要注意的时,普通命令后面都带有/r/n结尾。而palyload命令是根据数据长度找到结尾。我们在发送时必须注意这一点,如果发送的数据没有结尾标识,服务器会断开连接。另外发送的命令都需要以UTF8格式发送。

 

 

一 连接服务器

 

登录最终的目的是登录到NS服务器,与之建立连接并进行交互。一直到用户退出或注销时,才断开与服务器的连接。在登录到NS服务器之前,我们需要先连接到DS服务,并获得NS服务器的地址。

 

1 连接到DS服务器

 

DS服务器的地址是:messenger.hotmail.com ,端口号是1863在通过Socke连接之前需要对这个域名进行解析,而且解析的IP是不固定的,所以不建议使用固定的IP去连接DS服务器。以下是MSN Live 2009登录时和DS服务器之间的交互数据。

>>>VER 1 MSNP18 MSNP17 CVR0/r/n

<<<VER 1 MSNP18/r/n

>>>CVR 2 0x0804 winnt 5.1 i386 MSNMSGR 14.0.8089.0726 msmsgs test@live.cn/r/n

>>>USR 3 SSO I test@live.cn/r/n

<<<CVR 2 14.0.8089 14.0.8089 14.0.8089 http://msgruser.dlservice.microsoft.com/download/0/9/7/0974F7CD-D082-46FE-922D-806670345793/zh-chs/wlsetup-cvr.exe http://download.live.com/?sku=messenger/r/n

<<<XFR 3 NS 207.46.124.241:1863 U D/r/n

 

VER命令

这是和DS服务器发送的第一条消息,他是告诉服务器客户端支持的MSNP版本。MSNP版本功能在上一篇文章有所介绍,发送的版本号可以是多个版本。发送的版本号是区分大小写的。最后一个参数是CVR0,他实际是(CVQ, VER) 的命令集合,用来设置客户端版本,使得客户端能够升级。但具体作用不详,不发送也是可以的。最后要注意的是最后一个参数结尾要带上”/r/n”。服务器在接受到命令后,会从我们发送的协议版本中选一个他支持的最高本版,如果我们发送的协议版本他不支持,就返回一个0。目前最新的协议支持到了MSNP21。

 

CVR命令

这是是CVR命令的格式CVR trid lcid osName osVersion processorType clientName clientVersion [brandId] [userHandle]。聪从名字就能看出每个参数的作用。 lcid是地区编码,0x804表示的是中国。 clientVersion我们可以根据MSNP对应的官方版本来确定; brandID官方客户端是msmsgs ,最后一个就是登陆用户的账号。服务器会返回给你版本信息,最新的MSN下载地址,这些可以无视,我们又不是用MS的客户端。哈

USR命令

USR命令是在登录过程中用来身份验证的一个命令,会多次使用,在连接DS服务器时比较简单。第一个参数是SSO,表示身份验证方式。从MSNP15开始就是用此方法验证,而之前的版本使用TWN。所以要支持多个协议方式登陆,首先要区分身份验证的方式。第二个参数是 (I = Initiate, S = Respond to Challenge). 我们这里第一次使用,发送I,最后一个参数还是用户的账号。USR还有多种用法,后面用到时继续介绍。

 

XFR命令

服务器返回的XFR命令的作用是通知你NS服务器的地址,另一个作用就是在聊天时返回给你一个SB服务器的地址。第一个参数NS表示这是发送的NS地址,第二个参数就是服务器的IP地址和端口号。最后的U D表示什么不知道。接受到XFR后,DS服务器会自动断开SOCKET连接。这里XFR是否会不返回NS地址,不太清楚,起码目前没有发生过,当然程序中还是需要对这种情况进行处理。

 

以上就是连接DS服务的全部过程,介绍了几个命令的用法。要注意的是,这里并没有太多的介绍可能返回的错误。比如你已经登陆了,在发送这里的USR命令就会出错;如果发送消息过于频繁也会出错等等。但是一般,我们不需要太过关注这些,而且错误情况太多也根本关注不过来。后面我会给出所有服务器返回的错误代码的列表,实际出现问题时在去解决。这里我们只关注登录过程。需要注意的是发送命令不一定需要发送一条,接受一条才能在发送一条。有些命令可以不需要等待上一条返回就发送的。

 

 

2 连接到NS服务器

 

从XFR获得到了NS服务器的IP地址很端口号以后,我们就能使用SOCKET连接到NS服务器了。

>>VER 1 MSNP18 MSNP17 CVR0/r/n

>>>CVR 2 0x0804 winnt 5.1 i386 MSNMSGR 14.0.8089.0726 msmsgs test@live.cn/r/n

>>>USR 3 SSO I test@live.cn/r/n

<<<VER 1 MSNP18/r/n

<<<CVR 2 14.0.8089 14.0.8089 14.0.8089/r/n http://msgruser.dlservice.microsoft.com/download/0/9/7/0974F7CD-D082-46FE-922D-806670345793/zh-chs/wlsetup-cvr.exe http://download.live.com/?sku=messenger

<<<GCF 0 5521/r/n<Policies>省略</Policies>

<<<USR 3 SSO S MBI_KEY_OLD kVJy53UCBCupSu09gfE9rH47mtDGzmINva5/vEiYJIfxft+an0igQBC445kNyIkV/r/n

以上是MSN 2009连接NS服务时发送的前3条命令,我们可以看到这里没有等待上一条返回,而是一起发送了。这里的3条命令和DS服务器发送的一样,这里就不在过冬介绍了。但是发现我们发送的USR命令后,返回的和DS时不一样了。

 

GCF命令

此命令是从服务器获得配置文件信息,可以看他是一个playLoad命令,命令后面是ID好和数据长度。数据是使用XML存放。 但是从MSNP13开始此命令已经不在使用,所以我么可以不处理这一条命令。

 

USR命令

在前面我们发送过USR命令,在这里我们收到了服务器发送来的USR命令。这里我们要关注的是MBI_KEY_OLD 已经后面的一传值。这个就是SSO认证过程中需要用到的KEY。这里返回的类型可能是MBI、MBI_SSL 、MBI_KEY_OLD等类型。

 

 

3 断开服务器连接

 

断开服务器连接有多种方式,对于NS服务器,我们不需要主动断开,在获得XFR之后,服务器会主动断开SOCKET连接。对于NS服务器,我们退出时可以发送OUT命令,这个命令不需要ID和任何参数,服务器接收到OUT之后就会断开连接,这是一种比较好的断开方法。对于SB服务器一样,我们也只需要发送一个OUT命令。

我们还可以直接断开客户端的SOCKET连接,但是这样服务器不一定能马上改变我们的状态,好友也不一定马上能看到我们下线或退出对话。而如果我们发送的命令不正确,有些情况,服务求就会马上断开连接。有一些错误服务器在断开之前会返回错误代码。

 

OUT命令

我们能给服务器发送OUT表示我们退出,服务器有时也会在断开我们之前发送OUT通知我们。OUT [Reason]这是服务器发送OUT给我们时候的格式。当收到的与原因是OTH时,表示我们被其他登陆点注销,这可能是其他登陆点不支持多点登陆,也可能是被其他登陆点选择注销;当收到原因是RCT <delay> 的时候,表示服务器断开,指定的时间之后可以进行重连;SSD则表示服务器挂了;TME则表示登陆的终端太多,我们被服务器kick out了。我们可以根据不同情况进行处理。.

 

 

UUN命令:UUN trid receiverHandle[;EPID] appID size/r/nBODY

从MSNP16之后,开始支持多点登陆,我们可以注销掉其他登陆点的MSN。使用的命令就是UUN。这个命令作用是发送客户端1对1的通知消息。使用这个命令注销其他登陆点只是其中的一个功能。第一个参数要通知的客户端用户的账号,第二个参数是EPID,这个是可选的。支持多点登陆的版本,每个登陆点会有一个GUID来表示终端。第三个参数是操作类型ID,我们注销其他登陆点使用4;最后是发送BODY的大小;注销其他终端,官方服务发送的是大小为14的一串字符ngoawyplzthxbye,没有尝试能否使用其他字符,我认为应该是可以的。服务器接受到这个消息后,会根据EPID向其他客户端发送OUT OTH命令。注意发送EPID时不要少了前面的分号。 发送成功服务器会返回UUN trid OK。

App ID Description
0 Debug App ID
1 2-share
2 Voice conversation invites (never sent in UUN, only UBN)
3 P2P bootstrapping
4 Log off my remote endpoints
5 Closing the conversation window
6 Contact List Update
7 Clear RL Prompt
8 Forward non-session message (i.e. Offline IM)
9 RESERVED
10 TURN bridge notification
11 Meta-data channel for audio calls
12 Signaling channel for Voice over MSNP

以上是在UUN命令中使用到的AppID编号,同样这个编号也在UBN命令中使用。

 

UBN命令:UBN senderHandle;EPID appID size/r/nBody (>=MSNP 16)

在MSNP小于16的版本中,没有EPID这一项目。而这个命令从MSNP13开始支持。和UUN一样,他们的body内容最大长度为6144个字节,使用ASCII编码。这个命令接受到的可能是其他终端对UUN的返回结果,也可能是服务器发送来的邀请。

 

 

 

二 身份验证

 

从MSNP15开始,微软采用了新的身份认证方式SSO(Single Sign-On)。在MSN整个使用过程中,并不仅仅是登录需要身份验证,我们从WebService取得联系人列表,个人信息,用户头像,离线消息等等都需要身份验证。于是在登录的时候,我们进行身份验证的请求,服务会发送给我们一些列的ticket,他们对应了不同的服务请求。所以在开始登陆之前我们必须进行SSO认证,获得所需的ticket。SSO认证是通过WebServices服务完成的,SSO认证的地址为:https://login.live.com/RST.srf ,需要注意的是对于msn。com结尾的账号,需要提交到 https://msnia.login.live.com/pp550/RST.srf 进行认证。目前好像无法注册msn.com,也没有可用的账号,所以无法测试。

 

1 SSO认证方式

 

以上XML就是我们要提交到WebService的SOAP格式。首先我们需要填充自己的账号和密码到XML中。我们看到Body中有<wst:RequestSecurityToken Id=”RSTn“>…</wst:RequestSecurityToken>这样一个块,每一个代表一个你想要进行身份验证的域。RST0是必须的,我们添加对应的身份认证时,只需要构造从RST1开始节点块。这样你可以只提交一次,就获得多个不同的域的身份认证。 下面列出了所有域的相关信息。

RST Dimain域 Policy Ref Purpose
RST0 http://Passport.NET/tb 作用不明,但是必须包含才能请求成功
RST1 messengerclear.live.com NS USR命令获得的 Authentication for messenger.
目前在登录时使用
RST2 messenger.msn.com ?id=507 Messenger website authentication。
目前在获得离线消息时使用
RST3 contacts.msn.com MBI (used in WLM 8.5.1288.816) Authentication for the Contact server.
目前在操作联系人时使用
RST4 messengersecure.live.com MBI_SSL 未知
RST5 spaces.msn.com spaces.live.com MBI Authentication for the Windows Live Spaces
应该是登录到空间使用
RST6 livecontacts.live.com MBI Live Contacts API, a simplified version of the Contacts SOAP service
目前没有使用到
RST7 storage.live.com MBI Storage REST API
目前获取用户头像时使用

 

我们只需要根据上表的内容,填充自己需要的域节点的内容,提交到服务器,就可以获得对应服务的ticket。RTS1的Policy Ref是我们前面USR命令所获得的MBI_KEY_OLD,前面说过了,这个参数可能有MBI、MBI_SSL等类型,所以要根据USR返回值填如XML中。网上有些地方是在NS连接前去进行SSO认证,这个是不正确的,因为我们还没获得Policy Ref。虽然目前基本发送的都是MBI_KEY_OLD。然后服务器会返回给我们如下的XML:

<S:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Header>
       There is really data here, but for space it has be removed
   </S:Header>
   <S:Body>
       <wst:RequestSecurityTokenResponseCollection
           xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
           xmlns:wst="http://schemas.xmlsoap.org/ws/2004/04/trust"
           xmlns:wsse="http://schemas.xmlsoap.org/ws/2003/06/secext"
           xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
           xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
           xmlns:wsp="http://schemas.xmlsoap.org/ws/2002/12/policy"
           xmlns:psf="http://schemas.microsoft.com/Passport/SoapServices/SOAPFault">
           <wst:RequestSecurityTokenResponse>
               <wst:TokenType>urn:passport:legacy</wst:TokenType>
               <wsp:AppliesTo xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing">
                   <wsa:EndpointReference>
                       <wsa:Address>http://Passport.NET/tb</wsa:Address>
                   </wsa:EndpointReference>
               </wsp:AppliesTo>
               <wst:LifeTime>
                   <wsu:Created>2006-12-06T05:12:10Z</wsu:Created>
                   <wsu:Expires>2006-12-07T05:12:10Z</wsu:Expires>
               </wst:LifeTime>
               <wst:RequestedSecurityToken>
                   <EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#"
                    Id="BinaryDAToken0"
                    Type="http://www.w3.org/2001/04/xmlenc#Element">
                       <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc">
                           </EncryptionMethod>
                       <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                           <ds:KeyName>http://Passport.NET/STS</ds:KeyName>
                       </ds:KeyInfo>
                       <CipherData>
                           <CipherValue>
                               cipher data you don't need to worry about
                           </CipherValue>
                       </CipherData>
                   </EncryptedData>
               </wst:RequestedSecurityToken>
               <wst:RequestedTokenReference>
                   <wsse:KeyIdentifier ValueType="urn:passport"></wsse:KeyIdentifier>
                   <wsse:Reference URI="#BinaryDAToken0"></wsse:Reference>
               </wst:RequestedTokenReference>
               <wst:RequestedProofToken>
                   <wst:BinarySecret>ignore this one</wst:BinarySecret>
               </wst:RequestedProofToken>
           </wst:RequestSecurityTokenResponse>
           <wst:RequestSecurityTokenResponse>
               <wst:TokenType>urn:passport:compact</wst:TokenType>
               <wsp:AppliesTo xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing">
                   <wsa:EndpointReference>
                       <wsa:Address>messengerclear.live.com</wsa:Address>
                   </wsa:EndpointReference>
               </wsp:AppliesTo>
               <wst:LifeTime>
                   <wsu:Created>2006-12-06T05:12:10Z</wsu:Created>
                   <wsu:Expires>2006-12-06T13:12:10Z</wsu:Expires>
               </wst:LifeTime>
               <wst:RequestedSecurityToken>
                   <wsse:BinarySecurityToken Id="Compactn">
                       t=<ticket goes here>&p=
                   </wsse:BinarySecurityToken>
               </wst:RequestedSecurityToken>
               <wst:RequestedTokenReference>
                   <wsse:KeyIdentifier ValueType="urn:passport:compact"></wsse:KeyIdentifier>
                   <wsse:Reference URI="#Compactn"></wsse:Reference>
               </wst:RequestedTokenReference>
               <wst:RequestedProofToken>
                   <wst:BinarySecret>binary secret (you need this)</wst:BinarySecret>
               </wst:RequestedProofToken>
           </wst:RequestSecurityTokenResponse>
           <wst:RequestSecurityTokenResponse>
               <wst:TokenType>urn:passport:legacy</wst:TokenType>
               <wsp:AppliesTo xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing">
                   <wsa:EndpointReference>
                       <wsa:Address>site domain</wsa:Address>
                   </wsa:EndpointReference>
               </wsp:AppliesTo>
               <wst:LifeTime>
                   <wsu:Created>2006-12-06T05:12:10Z</wsu:Created>
                   <wsu:Expires>2006-12-06T05:20:30Z</wsu:Expires>
               </wst:LifeTime>
               <wst:RequestedSecurityToken>
                   <wsse:BinarySecurityToken Id="PPTokenn">
                       t=<site ticket here>&p=<site profile here>
                   </wsse:BinarySecurityToken>
               </wst:RequestedSecurityToken>
               <wst:RequestedTokenReference>
                   <wsse:KeyIdentifier ValueType="urn:passport"></wsse:KeyIdentifier>
                   <wsse:Reference URI="#PPTokenn"></wsse:Reference>
               </wst:RequestedTokenReference>
           </wst:RequestSecurityTokenResponse>
           ...
           ...
       </wst:RequestSecurityTokenResponseCollection>
   </S:Body>
</S:Envelope>

我们发送的XML中请求了几个认证的域,服务求就会返回对应的<wst:RequestSecurityTokenResponse>节点。其中<wsse:BinarySecurityToken Id=”Compactn“> 和 RSTn 中的数字n对应,其节点值t就是对用的认证使用的ticket<wsse:BinarySecurityToken Id=”PPTokenn“>节点中包含ticket和profile,因为对用的服务需要使用到这两个值,目前我们在获取离线消息时需要使用。如果我们验证时的账户和密码不正确时,返回的XML会包含wsse:FailedAuthentication

至此我们已经完成了SSO认证的过程,我们可以把ticket 、profile保存下来,供后面使用,这里要注意,返回的XML中包含ticke的过期时间,如果过期了,必须从新进行请求。

 

 

2 登录验证

 

前面我们已经获得所有需要的ticket,当然登录需要的ticket也获得了。另外我们还需要从保存登录ticket节点下<wst:BinarySecret>节点中的值,我们叫做BinarySecret。以下是在完成SSO认证之后,进行的登录身份验证发送的命令。

 

<<<USR 3 SSO S MBI_KEY_OLD kVJy53UCBCupSu09gfE9rH47mtDGzmINva5/vEiYJIfxft+an0igQBC445kNyIkV/r/n

>>SSO认证—webservices

<<<获得各个服务的ticket

>>>USR 4 SSO S t=EwBoAswbAQAUs1/VcBU2sH7mwYy3BysWZ71CRDGAAP2ngxo5hiNLK0FzCGY1llPy8F5Uv8GQVTL2FDWo0UFZ2P4kDFk95WWhFl4ydSN8zJQVpzq5YlhSaTJc/JziNMZV0RBOaNDv0yuuGKPZ7gkaHLF5QDF5t0xChHLupla0+WYt5N3rnfRjU8QnYqgvkdMtEolkfInY0lxyyfmBSVeSA2YAAAhynJ+hLqBEy7gBZf6U97YpNCZdVsu8Uwr5GMs0FY8aWocMlSKU0V3rBP4igonwRPb/VLYGAztVZxFHZx/abNHOt/Sh83M7LWhqvPPbcWKuOIcevMzwParNhKZ2VWS1TymnQuqnA8igbYrDtAi1QcD6iEoQy/sM4cb7ryM/MjUZCbWQ6xg5CecLNlZT5onfqZ2IJJSZjrRO/9ZEU0rpH7N8nj/ycJVgiKJR8emWDec7eh1CbFltxtFz3ZCnWcwU+GCqhTU7IsTLrX6LYxjSTUm4eG8x19mxbJpjEv1Zpgqajdwfobu4FzKe0yF64hnVJd2Kv8hqnTq38XtcocSNexs/Sue4SknaOMG2u69Yu7b6CD/Ih+BOu4V2ZKn/EndfcW7tC7jh0x8qdTzy7sjJqQuz6sGMCr1uEt3Lx7tlT7nj+RDW5MG9MEMFid4ordfe2R1DV4bKB3D4xZWc7gT4rjfTcxtlRnrP6kOM6n9hPDScewrj4fknSzW1OKd8yCEtdQjb4pnvvmWRWqDO1ImQPLcK5Q3CqYb0BGf53w3fL/FEoWD3m0drktFlK3nGW4F3smsfdOkFGnYF/zSg7Jg9erW7Z8LlAQ==&p= HAAAAAEAAAADZgAABIAAAAgAAAAUAAAASAAAAAAAAAAAAAAAoHfdK2pjBY1wLTHXDzh9VtaqYOb2g4n+epmErFlCejBt2pMDFMNR4fFjknY6XsftT1qboX3K45VNK3ICTjEbPsvR1Sog3D14Rz2Q6JPKFrEAWZ23PEjCV6f8XJw= {95DC3D4B-7FBF-46B5-B670-FE0E75B89217}

<<<USR 4 OK test@live.cn 1 0

以上继续使用USR SSO S命令,最后一个参数的格式是t=…&p=…{epid} , 这里的t就是我们SSO中取得和登录有关的ticket,而p是由USR 3中那一长串nonce和上面取得的BinarySecret通过计算得到的,具体计算方法后面介绍,最后一个参数是客户端的ID,是一个GUID,用来唯一标识一个终端。如果重复,会导致之前登录的终端被踢下线。

发送此条命令之后,如果验证通过,服务器会发送USR OK通知客户端登录成果,如果发送的t和p不对,会返回错误代码:911 ERR_AUTHENTICATION_FAILED

 

 

3 生成登录是发送的p

 

上面的USR 4命令中包含了p,这个p是由USR 3中得到的nonce和webservice请求获得的BinarySecret计算得到P。在介绍算法之前,我们需要一个结构体,我们最终的p就是这个结构体的值,发送给服务器。

typedef struct tagMSGRUSRKEY
{
	/** 28. Does not count data */
	unsigned int uStructHeaderSize;
	/** CRYPT_MODE_CBC (1) */
	unsigned int uCryptMode;
	/** TripleDES (0x6603) */
	unsigned int uCipherType;
	/** SHA1 (0x8004) */
	unsigned int uHashType;
	/** 8 */
	unsigned int uIVLen;
	/** 20 */
	unsigned int uHashLen;
	/** 72 */
	unsigned int uCipherLen;
	/** IV Data */
	unsigned char aIVBytes[8];
	/** Hash Data */
	unsigned char aHashBytes[20];
	/** Cipher Date */
	unsigned char aCipherBytes[72];
} MSGUSRKEY;

算法是按一下步骤来的:http://msnpiki.msnfanatic.com/index.php/MSNP15:SSO#Computing_the_return_value

 

1. 对获得的BinarySecret进行BASE64编码,存放到key1中。

 

2.使用SHA_HMAC加密key1,按一下方法进行

hash1 = SHA1-HMAC(key1,"WS-SecureConversationSESSION KEY HASH")
hash2 = SHA1-HMAC(key1,hash1+"WS-SecureConversationSESSION KEY HASH")
hash3 = SHA1-HMAC(key1,hash1)
hash4 = SHA1-HMAC(key1,hash3+"WS-SecureConversationSESSION KEY HASH")

然后把hash的20个字节,以及hash的前4个字节组成一个24字节的字符串存入 key2.

 

3.使用SHA_HMAC加密key2,按一下方法进行

hash1 = SHA1-HMAC(key2,"WS-SecureConversationSESSION KEY ENCRYPTION")
hash2 = SHA1-HMAC(key2,hash1+"WS-SecureConversationSESSION KEY ENCRYPTION")
hash3 = SHA1-HMAC(key2,hash1)
hash4 = SHA1-HMAC(key2,hash3+"WS-SecureConversationSESSION KEY ENCRYPTION")

然后把hash的20个字节,以及hash的前4个字节组成一个24字节的字符串存入 key3。

 

4. 使用SHA_HMAC加密key2和前面得到的nonoc,存入hash = SHA1-HMAC(key2, nonce)

 

5. 官方客户端会在nonce的后面追加8个字节的16进制数08

 

6. 创建8个字节随机数data

 

7. 使用TripleDes algorithm算法。模式设置为CBC,将上面的key3,随机数data 以及 第5步产生的nonce进行加密,存入encrypted_data

 

8. 填充结构体

aIVBytes使用第6步创建的随机数填充,aHashBytes使用第4步的hash填充,aCipherBytes使用第7步计算的encrypted_data填充。而其他节点的值则按照结构体注释中的填写,如果不想使用默认值进行相应调整

 

9. 对结构体进行BASE64编码

以下是用C++代码实现的整个计算过程,我们使用openssl提供的SHA_HMAC,TripleDes algorithm以及BASE64方法。

char* TCreateSSOP::GetSSOP(void)
{
	MSGUSRKEY key;
	_NS_compute_usrkey(&key, (char*)m_szNonce.c_str(), (char*)m_szBinarySecret.c_str());
	return base64((unsigned char*)&key, sizeof(MSGUSRKEY));
}

int TCreateSSOP::_NS_compute_usrkey(MSGUSRKEY *key, char *challenge, char *secret)
{
	char *key1;
	char key2[24];
	char key3[24];
	int klen = 0;
	char *chg;
	DES_cblock iv = {0,0,0,0,0,0,0,0};
	DES_key_schedule sched1, sched2, sched3;
	const_DES_cblock dkey1, dkey2, dkey3;

	key->uStructHeaderSize = 28;
	key->uCryptMode = 1;
	key->uCipherType = 0x6603;
	key->uHashType = 0x8004;
	key->uIVLen = 8;
	key->uHashLen = 20;
	key->uCipherLen = 72;
	memset(key->aIVBytes, 0, 8);

	key1 = (char*)unbase64((unsigned char*)secret, strlen(secret));
	klen = 24;
	_NS_compute_hash(key1, klen, "WS-SecureConversationSESSION KEY HASH", key2);
	_NS_compute_hash(key1, klen, "WS-SecureConversationSESSION KEY ENCRYPTION", key3);

	HMAC(EVP_sha1(), (unsigned char*)key2, klen, (unsigned char*)challenge, strlen(challenge), key->aHashBytes, NULL);

	memcpy(dkey1, key3, 8);
	memcpy(dkey2, key3+8, 8);
	memcpy(dkey3, key3+16, 8);
	DES_set_key_unchecked(&dkey1, &sched1);         // set the key schedule
	DES_set_key_unchecked(&dkey2, &sched2);         // set the key schedule
	DES_set_key_unchecked(&dkey3, &sched3);         // set the key schedule
	chg = (char*)malloc(strlen(challenge)+9);
	memcpy(chg, challenge, strlen(challenge));
	memset(chg+strlen(challenge), 8, 8);
	*(chg+strlen(challenge)+8) = 0;
	DES_ede3_cbc_encrypt((unsigned char*)chg, key->aCipherBytes, 72, &sched1, &sched2, &sched3, &iv, DES_ENCRYPT);
	free(chg);
	free(key1);
	return 0;
}


char* TCreateSSOP::_NS_compute_hash(char *key, int klen, const char *magic, char *result)
{
	int mlen = strlen(magic);
	static char ret[24];
	unsigned int mdlen1, mdlen2, mdlen3, mdlen4;
	unsigned char hash1[EVP_MAX_MD_SIZE];
	unsigned char hash2[EVP_MAX_MD_SIZE];
	unsigned char hash3[EVP_MAX_MD_SIZE];
	unsigned char hash4[EVP_MAX_MD_SIZE];
	unsigned char buf[EVP_MAX_MD_SIZE*2];

	HMAC(EVP_sha1(), (unsigned char*)key, klen, (unsigned char*)magic, mlen, hash1, &mdlen1);
	memcpy(buf, hash1, mdlen1);
	memcpy(buf+mdlen1, magic, mlen);
	HMAC(EVP_sha1(), (unsigned char*)key, klen, buf, mlen+mdlen1, hash2, &mdlen2);
	HMAC(EVP_sha1(), (unsigned char*)key, klen, hash1, mdlen1, hash3, &mdlen3);
	memcpy(buf, hash3, mdlen3);
	memcpy(buf+mdlen3, magic, mlen);
	HMAC(EVP_sha1(), (unsigned char*)key, klen, buf, mdlen3+mlen, hash4, &mdlen4);
	memcpy(ret, hash2, 20);
	memcpy(ret+20, hash4, 4);
	memcpy(result,ret, 24);
	return ret;
}


char* base64(const unsigned char *input, int length)
{
	//如果没制定长度,则根据要加密字符串的长度
	if (length == 0)
	{
		length = strlen((const char*)input); // 需要加密长度
	}
		
	 //计算存放加密数据的长度
	int bufferLen = length/3*4 + (length%3 ?4 : 0) + 1; 
	char *buffer = (char*)malloc(bufferLen);
	memset(buffer, 0, bufferLen);

	//base64加密
	EVP_EncodeBlock((unsigned char*)buffer, input, length);
	return buffer;
}

 

 

 

三 小结

 

 

自此,我们成功的完成了从连接DS服务器到MSN的登录身份验证,注意,这里仅仅是完成身份验证,并不是完成了实际的登录过程。除此之外,我们还获得了和面获取联系人列表、离线消息,联系人头像等操作时,请求Webservice服务所需要的ticket。整个登录过程复杂的就在于构建SSO请求的XML,以及计算p值。

在登录过程中可能出现的错误就是账号或密码错误导致SSO认证失败,也可能是计算P值出错,导致登录验证失败。所以在程序中必须对这两种错误进行处理

0

如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

10 评论

  1. [quote=luoaz][reply]cc_net[/reply]
    楼主,你有看你那边的离线消息是用的哪个地址吗?[/quote]

    记的一点,离线消息会从NS服务器发过来,包含Mail-Data: too-large的话要从WEBSEERVICE取得,否则直接从发过来的消息中取得结构是<MD><E/><Q/><M/><M/></MD>

    从webservice获取代码
    TWebServiceProcessor oimGetMetadata;
    oimGetMetadata.SetTimeOut(10); oimGetMetadata.AddRequestHeader("SOAPAction: http://www.hotmail.msn.com/ws/2004/09/oim/rsi/GetMetadata&quot;);
    oimGetMetadata.AddRequestHeader("Content-Type: text/xml; charset=utf-8");
    oimGetMetadata.SetRequestHeader();
    oimGetMetadata.SetRequestURL("https://rsi.hotmail.com/rsi/rsi.asmx&quot;);
    oimGetMetadata.SetRequestBody(buf);
    string strbuf = oimGetMetadata.SendRequest();

    buff里面包含了SSO后得到的t和p

    0
  2. 回复 pfmnpa:[e04]有时间会写的。现在项目做完了,好多东西也忘记了。应该变做项目变写的。但是没时间

    0

发表评论

电子邮件地址不会被公开。 必填项已用*标注