2 * @file nexus.c MSN Nexus functions
6 * Purple is the legal property of its developers, whose names are too numerous
7 * to list here. Please refer to the COPYRIGHT file distributed with this
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
27 #include "notification.h"
29 /**************************************************************************
31 **************************************************************************/
33 #define SSO_VALID_TICKET_DOMAIN 0
34 #define SSO_VALID_TICKET_POLICY 1
35 static char *ticket_domains
[][2] = {
36 /* http://msnpiki.msnfanatic.com/index.php/MSNP15:SSO */
37 /* {"Domain", "Policy Ref URI"}, Purpose */
38 {"messengerclear.live.com", NULL
}, /* Authentication for messenger. */
39 {"messenger.msn.com", "?id=507"}, /* Authentication for receiving OIMs. */
40 {"contacts.msn.com", "MBI"}, /* Authentication for the Contact server. */
41 {"messengersecure.live.com", "MBI_SSL"}, /* Authentication for sending OIMs. */
42 {"spaces.live.com", "MBI"}, /* Authentication for the Windows Live Spaces */
43 {"livecontacts.live.com", "MBI"}, /* Live Contacts API, a simplified version of the Contacts SOAP service */
44 {"storage.live.com", "MBI"}, /* Storage REST API */
47 /**************************************************************************
49 **************************************************************************/
52 msn_nexus_new(MsnSession
*session
)
57 nexus
= g_new0(MsnNexus
, 1);
58 nexus
->session
= session
;
60 nexus
->token_len
= sizeof(ticket_domains
) / sizeof(char *[2]);
61 nexus
->tokens
= g_new0(MsnTicketToken
, nexus
->token_len
);
63 for (i
= 0; i
< nexus
->token_len
; i
++)
64 nexus
->tokens
[i
].token
= g_hash_table_new_full(g_str_hash
, g_str_equal
,
71 msn_nexus_destroy(MsnNexus
*nexus
)
74 for (i
= 0; i
< nexus
->token_len
; i
++) {
75 g_hash_table_destroy(nexus
->tokens
[i
].token
);
76 g_free(nexus
->tokens
[i
].secret
);
77 g_slist_free(nexus
->tokens
[i
].updates
);
80 g_free(nexus
->tokens
);
81 g_free(nexus
->policy
);
83 g_free(nexus
->cipher
);
84 g_free(nexus
->secret
);
88 /**************************************************************************
89 * RPS/SSO Authentication
90 **************************************************************************/
93 rps_create_key(const char *key
, int key_len
, const char *data
, size_t data_len
)
95 const guchar magic
[] = "WS-SecureConversation";
96 const int magic_len
= sizeof(magic
) - 1;
98 PurpleCipherContext
*hmac
;
99 guchar hash1
[20], hash2
[20], hash3
[20], hash4
[20];
102 hmac
= purple_cipher_context_new_by_name("hmac", NULL
);
104 purple_cipher_context_set_option(hmac
, "hash", "sha1");
105 purple_cipher_context_set_key_with_len(hmac
, (guchar
*)key
, key_len
);
106 purple_cipher_context_append(hmac
, magic
, magic_len
);
107 purple_cipher_context_append(hmac
, (guchar
*)data
, data_len
);
108 purple_cipher_context_digest(hmac
, sizeof(hash1
), hash1
, NULL
);
110 purple_cipher_context_reset(hmac
, NULL
);
111 purple_cipher_context_set_option(hmac
, "hash", "sha1");
112 purple_cipher_context_set_key_with_len(hmac
, (guchar
*)key
, key_len
);
113 purple_cipher_context_append(hmac
, hash1
, 20);
114 purple_cipher_context_append(hmac
, magic
, magic_len
);
115 purple_cipher_context_append(hmac
, (guchar
*)data
, data_len
);
116 purple_cipher_context_digest(hmac
, sizeof(hash2
), hash2
, NULL
);
118 purple_cipher_context_reset(hmac
, NULL
);
119 purple_cipher_context_set_option(hmac
, "hash", "sha1");
120 purple_cipher_context_set_key_with_len(hmac
, (guchar
*)key
, key_len
);
121 purple_cipher_context_append(hmac
, hash1
, 20);
122 purple_cipher_context_digest(hmac
, sizeof(hash3
), hash3
, NULL
);
124 purple_cipher_context_reset(hmac
, NULL
);
125 purple_cipher_context_set_option(hmac
, "hash", "sha1");
126 purple_cipher_context_set_key_with_len(hmac
, (guchar
*)key
, key_len
);
127 purple_cipher_context_append(hmac
, hash3
, sizeof(hash3
));
128 purple_cipher_context_append(hmac
, magic
, magic_len
);
129 purple_cipher_context_append(hmac
, (guchar
*)data
, data_len
);
130 purple_cipher_context_digest(hmac
, sizeof(hash4
), hash4
, NULL
);
132 purple_cipher_context_destroy(hmac
);
134 result
= g_malloc(24);
135 memcpy(result
, hash2
, sizeof(hash2
));
136 memcpy(result
+ sizeof(hash2
), hash4
, 4);
142 des3_cbc(const char *key
, const char *iv
, const char *data
, int len
, gboolean decrypt
)
144 PurpleCipherContext
*des3
;
148 des3
= purple_cipher_context_new_by_name("des3", NULL
);
149 purple_cipher_context_set_key(des3
, (guchar
*)key
);
150 purple_cipher_context_set_batch_mode(des3
, PURPLE_CIPHER_BATCH_MODE_CBC
);
151 purple_cipher_context_set_iv(des3
, (guchar
*)iv
, 8);
155 purple_cipher_context_decrypt(des3
, (guchar
*)data
, len
, (guchar
*)out
, &outlen
);
157 purple_cipher_context_encrypt(des3
, (guchar
*)data
, len
, (guchar
*)out
, &outlen
);
159 purple_cipher_context_destroy(des3
);
164 #define CRYPT_MODE_CBC 1
165 #define CIPHER_TRIPLE_DES 0x6603
166 #define HASH_SHA1 0x8004
168 msn_rps_encrypt(MsnNexus
*nexus
)
171 const char magic1
[] = "SESSION KEY HASH";
172 const char magic2
[] = "SESSION KEY ENCRYPTION";
173 PurpleCipherContext
*hmac
;
176 char *key1
, *key2
, *key3
;
183 usr_key
= g_malloc(sizeof(MsnUsrKey
));
184 usr_key
->size
= GUINT32_TO_LE(28);
185 usr_key
->crypt_mode
= GUINT32_TO_LE(CRYPT_MODE_CBC
);
186 usr_key
->cipher_type
= GUINT32_TO_LE(CIPHER_TRIPLE_DES
);
187 usr_key
->hash_type
= GUINT32_TO_LE(HASH_SHA1
);
188 usr_key
->iv_len
= GUINT32_TO_LE(8);
189 usr_key
->hash_len
= GUINT32_TO_LE(20);
190 usr_key
->cipher_len
= GUINT32_TO_LE(72);
192 key1
= (char *)purple_base64_decode((const char *)nexus
->tokens
[MSN_AUTH_MESSENGER
].secret
, &key1_len
);
193 key2
= rps_create_key(key1
, key1_len
, magic1
, sizeof(magic1
) - 1);
194 key3
= rps_create_key(key1
, key1_len
, magic2
, sizeof(magic2
) - 1);
196 iv
= (int *)usr_key
->iv
;
200 len
= strlen(nexus
->nonce
);
201 hmac
= purple_cipher_context_new_by_name("hmac", NULL
);
202 purple_cipher_context_set_option(hmac
, "hash", "sha1");
203 purple_cipher_context_set_key_with_len(hmac
, (guchar
*)key2
, 24);
204 purple_cipher_context_append(hmac
, (guchar
*)nexus
->nonce
, len
);
205 purple_cipher_context_digest(hmac
, 20, hash
, NULL
);
206 purple_cipher_context_destroy(hmac
);
208 /* We need to pad this to 72 bytes, apparently */
209 nonce_fixed
= g_malloc(len
+ 8);
210 memcpy(nonce_fixed
, nexus
->nonce
, len
);
211 memset(nonce_fixed
+ len
, 0x08, 8);
212 cipher
= des3_cbc(key3
, usr_key
->iv
, nonce_fixed
, len
+ 8, FALSE
);
215 memcpy(usr_key
->hash
, hash
, 20);
216 memcpy(usr_key
->cipher
, cipher
, 72);
223 response
= purple_base64_encode((guchar
*)usr_key
, sizeof(MsnUsrKey
));
230 /**************************************************************************
232 **************************************************************************/
234 /* Used to specify which token to update when only doing single updates */
235 typedef struct _MsnNexusUpdateData MsnNexusUpdateData
;
236 struct _MsnNexusUpdateData
{
241 typedef struct _MsnNexusUpdateCallback MsnNexusUpdateCallback
;
242 struct _MsnNexusUpdateCallback
{
248 nexus_parse_token(MsnNexus
*nexus
, int id
, xmlnode
*node
)
250 char *token_str
, *expiry_str
;
252 char **elems
, **cur
, **tokens
;
253 xmlnode
*token
= xmlnode_get_child(node
, "RequestedSecurityToken/BinarySecurityToken");
254 xmlnode
*secret
= xmlnode_get_child(node
, "RequestedProofToken/BinarySecret");
255 xmlnode
*expires
= xmlnode_get_child(node
, "LifeTime/Expires");
260 /* Use the ID that the server sent us */
262 id_str
= xmlnode_get_attrib(token
, "Id");
266 id
= atol(id_str
+ 7) - 1; /* 'Compact#' or 'PPToken#' */
267 if (id
>= nexus
->token_len
)
268 return FALSE
; /* Where did this come from? */
271 token_str
= xmlnode_get_data(token
);
272 if (token_str
== NULL
)
275 g_hash_table_remove_all(nexus
->tokens
[id
].token
);
277 elems
= g_strsplit(token_str
, "&", 0);
279 for (cur
= elems
; *cur
!= NULL
; cur
++) {
280 tokens
= g_strsplit(*cur
, "=", 2);
281 g_hash_table_insert(nexus
->tokens
[id
].token
, tokens
[0], tokens
[1]);
282 /* Don't free each of the tokens, only the array. */
289 nexus
->tokens
[id
].secret
= xmlnode_get_data(secret
);
291 nexus
->tokens
[id
].secret
= NULL
;
293 /* Yay for MS using ISO-8601 */
294 expiry_str
= xmlnode_get_data(expires
);
295 nexus
->tokens
[id
].expiry
= purple_str_to_time(expiry_str
,
296 FALSE
, NULL
, NULL
, NULL
);
299 purple_debug_info("msn", "Updated ticket for domain '%s', expires at %" G_GINT64_FORMAT
".\n",
300 ticket_domains
[id
][SSO_VALID_TICKET_DOMAIN
],
301 (gint64
)nexus
->tokens
[id
].expiry
);
306 nexus_parse_collection(MsnNexus
*nexus
, int id
, xmlnode
*collection
)
311 node
= xmlnode_get_child(collection
, "RequestSecurityTokenResponse");
317 for (; node
&& result
; node
= node
->next
) {
318 xmlnode
*endpoint
= xmlnode_get_child(node
, "AppliesTo/EndpointReference/Address");
319 char *address
= xmlnode_get_data(endpoint
);
321 if (g_str_equal(address
, "http://Passport.NET/tb")) {
322 /* This node contains the stuff for updating tokens. */
324 xmlnode
*cipher
= xmlnode_get_child(node
, "RequestedSecurityToken/EncryptedData/CipherData/CipherValue");
325 xmlnode
*secret
= xmlnode_get_child(node
, "RequestedProofToken/BinarySecret");
327 nexus
->cipher
= xmlnode_get_data(cipher
);
328 data
= xmlnode_get_data(secret
);
329 nexus
->secret
= (char *)purple_base64_decode(data
, NULL
);
333 result
= nexus_parse_token(nexus
, id
, node
);
342 nexus_got_response_cb(MsnSoapMessage
*req
, MsnSoapMessage
*resp
, gpointer data
)
344 MsnNexus
*nexus
= data
;
345 MsnSession
*session
= nexus
->session
;
350 msn_session_set_error(session
, MSN_ERROR_SERVCONN
, _("Windows Live ID authentication:Unable to connect"));
354 if (!nexus_parse_collection(nexus
, -1,
355 xmlnode_get_child(resp
->xml
,
356 "Body/RequestSecurityTokenResponseCollection"))) {
357 msn_session_set_error(session
, MSN_ERROR_SERVCONN
, _("Windows Live ID authentication:Invalid response"));
361 ticket
= msn_nexus_get_token_str(nexus
, MSN_AUTH_MESSENGER
);
362 response
= msn_rps_encrypt(nexus
);
363 msn_got_login_params(session
, ticket
, response
);
367 /*when connect, do the SOAP Style windows Live ID authentication */
369 msn_nexus_connect(MsnNexus
*nexus
)
371 MsnSession
*session
= nexus
->session
;
372 const char *username
;
373 const char *password
;
379 MsnSoapMessage
*soap
;
381 purple_debug_info("msn", "Starting Windows Live ID authentication\n");
382 msn_session_set_login_step(session
, MSN_LOGIN_STEP_GET_COOKIE
);
384 username
= purple_account_get_username(session
->account
);
385 password
= purple_connection_get_password(session
->account
->gc
);
386 password_xml
= g_markup_escape_text(password
, MIN(strlen(password
), 16));
388 purple_debug_info("msn", "Logging on %s, with policy '%s', nonce '%s'\n",
389 username
, nexus
->policy
, nexus
->nonce
);
391 domains
= g_string_new(NULL
);
392 for (i
= 0; i
< nexus
->token_len
; i
++) {
393 g_string_append_printf(domains
, MSN_SSO_RST_TEMPLATE
,
395 ticket_domains
[i
][SSO_VALID_TICKET_DOMAIN
],
396 ticket_domains
[i
][SSO_VALID_TICKET_POLICY
] != NULL
?
397 ticket_domains
[i
][SSO_VALID_TICKET_POLICY
] :
401 request
= g_strdup_printf(MSN_SSO_TEMPLATE
, username
, password_xml
, domains
->str
);
402 g_free(password_xml
);
403 g_string_free(domains
, TRUE
);
405 soap
= msn_soap_message_new(NULL
, xmlnode_from_str(request
, -1));
407 msn_soap_message_send(session
, soap
, MSN_SSO_SERVER
, SSO_POST_URL
, TRUE
,
408 nexus_got_response_cb
, nexus
);
412 nexus_got_update_cb(MsnSoapMessage
*req
, MsnSoapMessage
*resp
, gpointer data
)
414 MsnNexusUpdateData
*ud
= data
;
415 MsnNexus
*nexus
= ud
->nexus
;
416 char iv
[8] = {0,0,0,0,0,0,0,0};
427 char *decrypted_data
;
432 purple_debug_info("msn", "Got Update Response for %s.\n", ticket_domains
[ud
->id
][SSO_VALID_TICKET_DOMAIN
]);
434 enckey
= xmlnode_get_child(resp
->xml
, "Header/Security/DerivedKeyToken");
436 if (g_str_equal(xmlnode_get_attrib(enckey
, "Id"), "EncKey"))
438 enckey
= xmlnode_get_next_twin(enckey
);
441 purple_debug_error("msn", "Invalid response in token update.\n");
445 tmp
= xmlnode_get_data(xmlnode_get_child(enckey
, "Nonce"));
446 nonce
= (char *)purple_base64_decode(tmp
, &len
);
447 key
= rps_create_key(nexus
->secret
, 24, nonce
, len
);
452 /* Don't know what this is for yet */
453 tmp
= xmlnode_get_data(xmlnode_get_child(resp
->xml
,
454 "Header/EncryptedPP/EncryptedData/CipherData/CipherValue"));
456 decrypted_pp
= des3_cbc(key
, iv
, tmp
, len
, TRUE
);
458 purple_debug_info("msn", "Got Response Header EncryptedPP: %s\n", decrypted_pp
);
459 g_free(decrypted_pp
);
463 tmp
= xmlnode_get_data(xmlnode_get_child(resp
->xml
,
464 "Body/EncryptedData/CipherData/CipherValue"));
467 xmlnode
*rstresponse
;
469 unescaped
= (char *)purple_base64_decode(tmp
, &len
);
472 decrypted_data
= des3_cbc(key
, iv
, unescaped
, len
, TRUE
);
474 purple_debug_info("msn", "Got Response Body EncryptedData: %s\n", decrypted_data
);
476 rstresponse
= xmlnode_from_str(decrypted_data
, -1);
477 if (g_str_equal(rstresponse
->name
, "RequestSecurityTokenResponse"))
478 nexus_parse_token(nexus
, ud
->id
, rstresponse
);
480 nexus_parse_collection(nexus
, ud
->id
, rstresponse
);
481 g_free(decrypted_data
);
484 updates
= nexus
->tokens
[ud
->id
].updates
;
485 nexus
->tokens
[ud
->id
].updates
= NULL
;
486 while (updates
!= NULL
) {
487 MsnNexusUpdateCallback
*update
= updates
->data
;
489 purple_timeout_add(0, update
->cb
, update
->data
);
491 updates
= g_slist_delete_link(updates
, updates
);
498 msn_nexus_update_token(MsnNexus
*nexus
, int id
, GSourceFunc cb
, gpointer data
)
500 MsnSession
*session
= nexus
->session
;
501 MsnNexusUpdateData
*ud
;
502 MsnNexusUpdateCallback
*update
;
503 PurpleCipherContext
*sha1
;
504 PurpleCipherContext
*hmac
;
524 guchar signature
[20];
527 MsnSoapMessage
*soap
;
529 update
= g_new0(MsnNexusUpdateCallback
, 1);
533 if (nexus
->tokens
[id
].updates
!= NULL
) {
534 /* Update already in progress. Just add to list and return. */
535 purple_debug_info("msn",
536 "Ticket update for user '%s' on domain '%s' in progress. Adding request to queue.\n",
537 purple_account_get_username(session
->account
),
538 ticket_domains
[id
][SSO_VALID_TICKET_DOMAIN
]);
539 nexus
->tokens
[id
].updates
= g_slist_prepend(nexus
->tokens
[id
].updates
,
543 purple_debug_info("msn",
544 "Updating ticket for user '%s' on domain '%s'\n",
545 purple_account_get_username(session
->account
),
546 ticket_domains
[id
][SSO_VALID_TICKET_DOMAIN
]);
547 nexus
->tokens
[id
].updates
= g_slist_prepend(nexus
->tokens
[id
].updates
,
551 ud
= g_new0(MsnNexusUpdateData
, 1);
555 sha1
= purple_cipher_context_new_by_name("sha1", NULL
);
557 domain
= g_strdup_printf(MSN_SSO_RST_TEMPLATE
,
559 ticket_domains
[id
][SSO_VALID_TICKET_DOMAIN
],
560 ticket_domains
[id
][SSO_VALID_TICKET_POLICY
] != NULL
?
561 ticket_domains
[id
][SSO_VALID_TICKET_POLICY
] :
563 purple_cipher_context_append(sha1
, (guchar
*)domain
, strlen(domain
));
564 purple_cipher_context_digest(sha1
, 20, digest
, NULL
);
565 domain_b64
= purple_base64_encode(digest
, 20);
569 now_str
= g_strdup(purple_utf8_strftime("%Y-%m-%dT%H:%M:%SZ", tm
));
572 timestamp
= g_strdup_printf(MSN_SSO_TIMESTAMP_TEMPLATE
,
574 purple_utf8_strftime("%Y-%m-%dT%H:%M:%SZ", tm
));
575 purple_cipher_context_reset(sha1
, NULL
);
576 purple_cipher_context_append(sha1
, (guchar
*)timestamp
, strlen(timestamp
));
577 purple_cipher_context_digest(sha1
, 20, digest
, NULL
);
578 timestamp_b64
= purple_base64_encode(digest
, 20);
581 purple_cipher_context_destroy(sha1
);
583 signedinfo
= g_strdup_printf(MSN_SSO_SIGNEDINFO_TEMPLATE
,
588 for (i
= 0; i
< 6; i
++)
590 nonce_b64
= purple_base64_encode((guchar
*)&nonce
, sizeof(nonce
));
592 key
= rps_create_key(nexus
->secret
, 24, (char *)nonce
, sizeof(nonce
));
593 hmac
= purple_cipher_context_new_by_name("hmac", NULL
);
594 purple_cipher_context_set_option(hmac
, "hash", "sha1");
595 purple_cipher_context_set_key_with_len(hmac
, (guchar
*)key
, 24);
596 purple_cipher_context_append(hmac
, (guchar
*)signedinfo
, strlen(signedinfo
));
597 purple_cipher_context_digest(hmac
, 20, signature
, NULL
);
598 purple_cipher_context_destroy(hmac
);
599 signature_b64
= purple_base64_encode(signature
, 20);
601 request
= g_strdup_printf(MSN_SSO_TOKEN_UPDATE_TEMPLATE
,
611 g_free(timestamp_b64
);
614 g_free(signature_b64
);
618 soap
= msn_soap_message_new(NULL
, xmlnode_from_str(request
, -1));
620 msn_soap_message_send(session
, soap
, MSN_SSO_SERVER
, SSO_POST_URL
, TRUE
,
621 nexus_got_update_cb
, ud
);
625 msn_nexus_get_token(MsnNexus
*nexus
, MsnAuthDomains id
)
627 g_return_val_if_fail(nexus
!= NULL
, NULL
);
628 g_return_val_if_fail(id
< nexus
->token_len
, NULL
);
630 return nexus
->tokens
[id
].token
;
634 msn_nexus_get_token_str(MsnNexus
*nexus
, MsnAuthDomains id
)
636 static char buf
[1024];
637 GHashTable
*token
= msn_nexus_get_token(nexus
, id
);
642 g_return_val_if_fail(token
!= NULL
, NULL
);
644 msn_t
= g_hash_table_lookup(token
, "t");
645 msn_p
= g_hash_table_lookup(token
, "p");
647 g_return_val_if_fail(msn_t
!= NULL
, NULL
);
648 g_return_val_if_fail(msn_p
!= NULL
, NULL
);
650 ret
= g_snprintf(buf
, sizeof(buf
) - 1, "t=%s&p=%s", msn_t
, msn_p
);
651 g_return_val_if_fail(ret
!= -1, NULL
);