這一篇開始主要介紹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[email protected]/r/n
>>>USR 3 SSO I[email protected]/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.exehttp://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 [email protected]/r/n
>>>USR 3 SSO I [email protected]/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認證方式
- <Envelopexmlns="http://schemas.xmlsoap.org/soap/envelope/"
- xmlns:wsse="http://schemas.xmlsoap.org/ws/2003/06/secext"
- xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
- xmlns:wsp="http://schemas.xmlsoap.org/ws/2002/12/policy"
- xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
- xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/03/addressing"
- xmlns:wssc="http://schemas.xmlsoap.org/ws/2004/04/sc"
- xmlns:wst="http://schemas.xmlsoap.org/ws/2004/04/trust">
- <Header>
- <ps:AuthInfo
- xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL"
- Id="PPAuthInfo">
- <ps:HostingApp>{7108E71A-9926-4FCB-BCC9-9A9D3F32E423}</ps:HostingApp>
- <ps:BinaryVersion>4</ps:BinaryVersion>
- <ps:UIVersion>1</ps:UIVersion>
- <ps:Cookies></ps:Cookies>
- <ps:RequestParams>AQAAAAIAAABsYwQAAAAxMDMz</ps:RequestParams>
- </ps:AuthInfo>
- <wsse:Security>
- <wsse:UsernameTokenId="user">
- <wsse:Username>[email protected]</wsse:Username>
- <wsse:Password>PasswordGoesHere</wsse:Password>
- </wsse:UsernameToken>
- </wsse:Security>
- </Header>
- <Body>
- <ps:RequestMultipleSecurityTokens
- xmlns:ps="http://schemas.microsoft.com/Passport/SoapServices/PPCRL"
- Id="RSTS">
- <wst:RequestSecurityTokenId="RST0">
- <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
- <wsp:AppliesTo>
- <wsa:EndpointReference>
- <wsa:Address>http://Passport.NET/tb</wsa:Address>
- </wsa:EndpointReference>
- </wsp:AppliesTo>
- </wst:RequestSecurityToken>
- <wst:RequestSecurityTokenId="RSTn">
- <wst:RequestType>http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue</wst:RequestType>
- <wsp:AppliesTo>
- <wsa:EndpointReference>
- <wsa:Address>domain</wsa:Address>
- </wsa:EndpointReference>
- </wsp:AppliesTo>
- <wsse:PolicyReferenceURI="policy parameter"></wsse:PolicyReference>
- </wst:RequestSecurityToken>
- ...
- ...
- </ps:RequestMultipleSecurityTokens>
- </Body>
- </Envelope>
以上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, butfor 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 [email protected] 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就是這個結構體的值,發送給服務器。
- typedefstruct 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((unsignedchar*)&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((unsignedchar*)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, (unsignedchar*)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,constchar *magic,char *result)
- {
- int mlen = strlen(magic);
- staticchar 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, (unsignedchar*)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 unsignedchar *input,int length)
- {
- //如果沒制定長度,則根據要加密字符串的長度
- if (length == 0)
- {
- length = strlen((constchar*)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值出錯,導致登錄驗證失敗。所以在程序中必須對這兩種錯誤進行處理。