Mercurial > pidgin.yaz
view libpurple/protocols/msn/nexus.c @ 26776:83e6e710cbf3
Add a new signal which is emitted (after the account is connected) if the
associated account actions are changed. This is necessary since XMPP's
account actions are not known when setting the account to connected. Also
added support for push updates as specified in XEP-0050 2.3.
Refs #7233.
author | Paul Aurich <paul@darkrain42.org> |
---|---|
date | Sun, 26 Apr 2009 06:45:55 +0000 |
parents | 3f6dbc414357 |
children | caef0d8dcb04 39716f7d2c93 |
line wrap: on
line source
/** * @file nexus.c MSN Nexus functions * * purple * * Purple is the legal property of its developers, whose names are too numerous * to list here. Please refer to the COPYRIGHT file distributed with this * source distribution. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA */ #include "msn.h" #include "soap.h" #include "nexus.h" #include "notification.h" /************************************************************************** * Valid Ticket Tokens **************************************************************************/ #define SSO_VALID_TICKET_DOMAIN 0 #define SSO_VALID_TICKET_POLICY 1 static char *ticket_domains[][2] = { /* http://msnpiki.msnfanatic.com/index.php/MSNP15:SSO */ /* {"Domain", "Policy Ref URI"}, Purpose */ {"messengerclear.live.com", NULL}, /* Authentication for messenger. */ {"messenger.msn.com", "?id=507"}, /* Authentication for receiving OIMs. */ {"contacts.msn.com", "MBI"}, /* Authentication for the Contact server. */ {"messengersecure.live.com", "MBI_SSL"}, /* Authentication for sending OIMs. */ {"spaces.live.com", "MBI"}, /* Authentication for the Windows Live Spaces */ {"livecontacts.live.com", "MBI"}, /* Live Contacts API, a simplified version of the Contacts SOAP service */ {"storage.live.com", "MBI"}, /* Storage REST API */ }; /************************************************************************** * Main **************************************************************************/ MsnNexus * msn_nexus_new(MsnSession *session) { MsnNexus *nexus; int i; nexus = g_new0(MsnNexus, 1); nexus->session = session; nexus->token_len = sizeof(ticket_domains) / sizeof(char *[2]); nexus->tokens = g_new0(MsnTicketToken, nexus->token_len); for (i = 0; i < nexus->token_len; i++) nexus->tokens[i].token = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); return nexus; } void msn_nexus_destroy(MsnNexus *nexus) { int i; for (i = 0; i < nexus->token_len; i++) { g_hash_table_destroy(nexus->tokens[i].token); g_free(nexus->tokens[i].secret); g_slist_free(nexus->tokens[i].updates); } g_free(nexus->tokens); g_free(nexus->policy); g_free(nexus->nonce); g_free(nexus->cipher); g_free(nexus->secret); g_free(nexus); } /************************************************************************** * RPS/SSO Authentication **************************************************************************/ static char * rps_create_key(const char *key, int key_len, const char *data, size_t data_len) { const guchar magic[] = "WS-SecureConversation"; const int magic_len = sizeof(magic) - 1; PurpleCipherContext *hmac; guchar hash1[20], hash2[20], hash3[20], hash4[20]; char *result; hmac = purple_cipher_context_new_by_name("hmac", NULL); purple_cipher_context_set_option(hmac, "hash", "sha1"); purple_cipher_context_set_key_with_len(hmac, (guchar *)key, key_len); purple_cipher_context_append(hmac, magic, magic_len); purple_cipher_context_append(hmac, (guchar *)data, data_len); purple_cipher_context_digest(hmac, sizeof(hash1), hash1, NULL); purple_cipher_context_reset(hmac, NULL); purple_cipher_context_set_option(hmac, "hash", "sha1"); purple_cipher_context_set_key_with_len(hmac, (guchar *)key, key_len); purple_cipher_context_append(hmac, hash1, 20); purple_cipher_context_append(hmac, magic, magic_len); purple_cipher_context_append(hmac, (guchar *)data, data_len); purple_cipher_context_digest(hmac, sizeof(hash2), hash2, NULL); purple_cipher_context_reset(hmac, NULL); purple_cipher_context_set_option(hmac, "hash", "sha1"); purple_cipher_context_set_key_with_len(hmac, (guchar *)key, key_len); purple_cipher_context_append(hmac, hash1, 20); purple_cipher_context_digest(hmac, sizeof(hash3), hash3, NULL); purple_cipher_context_reset(hmac, NULL); purple_cipher_context_set_option(hmac, "hash", "sha1"); purple_cipher_context_set_key_with_len(hmac, (guchar *)key, key_len); purple_cipher_context_append(hmac, hash3, sizeof(hash3)); purple_cipher_context_append(hmac, magic, magic_len); purple_cipher_context_append(hmac, (guchar *)data, data_len); purple_cipher_context_digest(hmac, sizeof(hash4), hash4, NULL); purple_cipher_context_destroy(hmac); result = g_malloc(24); memcpy(result, hash2, sizeof(hash2)); memcpy(result + sizeof(hash2), hash4, 4); return result; } static char * des3_cbc(const char *key, const char *iv, const char *data, int len, gboolean decrypt) { PurpleCipherContext *des3; char *out; size_t outlen; des3 = purple_cipher_context_new_by_name("des3", NULL); purple_cipher_context_set_key(des3, (guchar *)key); purple_cipher_context_set_batch_mode(des3, PURPLE_CIPHER_BATCH_MODE_CBC); purple_cipher_context_set_iv(des3, (guchar *)iv, 8); out = g_malloc(len); if (decrypt) purple_cipher_context_decrypt(des3, (guchar *)data, len, (guchar *)out, &outlen); else purple_cipher_context_encrypt(des3, (guchar *)data, len, (guchar *)out, &outlen); purple_cipher_context_destroy(des3); return out; } #define CRYPT_MODE_CBC 1 #define CIPHER_TRIPLE_DES 0x6603 #define HASH_SHA1 0x8004 static char * msn_rps_encrypt(MsnNexus *nexus) { MsnUsrKey *usr_key; const char magic1[] = "SESSION KEY HASH"; const char magic2[] = "SESSION KEY ENCRYPTION"; PurpleCipherContext *hmac; size_t len; guchar hash[20]; char *key1, *key2, *key3; gsize key1_len; int *iv; char *nonce_fixed; char *cipher; char *response; usr_key = g_malloc(sizeof(MsnUsrKey)); usr_key->size = GUINT32_TO_LE(28); usr_key->crypt_mode = GUINT32_TO_LE(CRYPT_MODE_CBC); usr_key->cipher_type = GUINT32_TO_LE(CIPHER_TRIPLE_DES); usr_key->hash_type = GUINT32_TO_LE(HASH_SHA1); usr_key->iv_len = GUINT32_TO_LE(8); usr_key->hash_len = GUINT32_TO_LE(20); usr_key->cipher_len = GUINT32_TO_LE(72); key1 = (char *)purple_base64_decode((const char *)nexus->tokens[MSN_AUTH_MESSENGER].secret, &key1_len); key2 = rps_create_key(key1, key1_len, magic1, sizeof(magic1) - 1); key3 = rps_create_key(key1, key1_len, magic2, sizeof(magic2) - 1); iv = (int *)usr_key->iv; iv[0] = rand(); iv[1] = rand(); len = strlen(nexus->nonce); hmac = purple_cipher_context_new_by_name("hmac", NULL); purple_cipher_context_set_option(hmac, "hash", "sha1"); purple_cipher_context_set_key_with_len(hmac, (guchar *)key2, 24); purple_cipher_context_append(hmac, (guchar *)nexus->nonce, len); purple_cipher_context_digest(hmac, 20, hash, NULL); purple_cipher_context_destroy(hmac); /* We need to pad this to 72 bytes, apparently */ nonce_fixed = g_malloc(len + 8); memcpy(nonce_fixed, nexus->nonce, len); memset(nonce_fixed + len, 0x08, 8); cipher = des3_cbc(key3, usr_key->iv, nonce_fixed, len + 8, FALSE); g_free(nonce_fixed); memcpy(usr_key->hash, hash, 20); memcpy(usr_key->cipher, cipher, 72); g_free(key1); g_free(key2); g_free(key3); g_free(cipher); response = purple_base64_encode((guchar *)usr_key, sizeof(MsnUsrKey)); g_free(usr_key); return response; } /************************************************************************** * Login **************************************************************************/ /* Used to specify which token to update when only doing single updates */ typedef struct _MsnNexusUpdateData MsnNexusUpdateData; struct _MsnNexusUpdateData { MsnNexus *nexus; int id; }; typedef struct _MsnNexusUpdateCallback MsnNexusUpdateCallback; struct _MsnNexusUpdateCallback { GSourceFunc cb; gpointer data; }; #if !GLIB_CHECK_VERSION(2, 12, 0) static gboolean nexus_remove_all_cb(gpointer key, gpointer val, gpointer data) { return TRUE; } #endif static gboolean nexus_parse_token(MsnNexus *nexus, int id, xmlnode *node) { char *token_str, *expiry_str; const char *id_str; char **elems, **cur, **tokens; xmlnode *token = xmlnode_get_child(node, "RequestedSecurityToken/BinarySecurityToken"); xmlnode *secret = xmlnode_get_child(node, "RequestedProofToken/BinarySecret"); xmlnode *expires = xmlnode_get_child(node, "LifeTime/Expires"); if (!token) return FALSE; /* Use the ID that the server sent us */ if (id == -1) { id_str = xmlnode_get_attrib(token, "Id"); if (id_str == NULL) return FALSE; id = atol(id_str + 7) - 1; /* 'Compact#' or 'PPToken#' */ if (id >= nexus->token_len) return FALSE; /* Where did this come from? */ } token_str = xmlnode_get_data(token); if (token_str == NULL) return FALSE; #if GLIB_CHECK_VERSION(2, 12, 0) g_hash_table_remove_all(nexus->tokens[id].token); #else g_hash_table_foreach_remove(nexus->tokens[id].token, nexus_remove_all_cb, NULL); #endif elems = g_strsplit(token_str, "&", 0); for (cur = elems; *cur != NULL; cur++) { tokens = g_strsplit(*cur, "=", 2); g_hash_table_insert(nexus->tokens[id].token, tokens[0], tokens[1]); /* Don't free each of the tokens, only the array. */ g_free(tokens); } g_strfreev(elems); g_free(token_str); if (secret) nexus->tokens[id].secret = xmlnode_get_data(secret); else nexus->tokens[id].secret = NULL; /* Yay for MS using ISO-8601 */ expiry_str = xmlnode_get_data(expires); nexus->tokens[id].expiry = purple_str_to_time(expiry_str, FALSE, NULL, NULL, NULL); g_free(expiry_str); purple_debug_info("msn", "Updated ticket for domain '%s', expires at %" G_GINT64_FORMAT ".\n", ticket_domains[id][SSO_VALID_TICKET_DOMAIN], (gint64)nexus->tokens[id].expiry); return TRUE; } static gboolean nexus_parse_collection(MsnNexus *nexus, int id, xmlnode *collection) { xmlnode *node; gboolean result; node = xmlnode_get_child(collection, "RequestSecurityTokenResponse"); if (!node) return FALSE; result = TRUE; for (; node && result; node = node->next) { xmlnode *endpoint = xmlnode_get_child(node, "AppliesTo/EndpointReference/Address"); char *address = xmlnode_get_data(endpoint); if (g_str_equal(address, "http://Passport.NET/tb")) { /* This node contains the stuff for updating tokens. */ char *data; xmlnode *cipher = xmlnode_get_child(node, "RequestedSecurityToken/EncryptedData/CipherData/CipherValue"); xmlnode *secret = xmlnode_get_child(node, "RequestedProofToken/BinarySecret"); nexus->cipher = xmlnode_get_data(cipher); data = xmlnode_get_data(secret); nexus->secret = (char *)purple_base64_decode(data, NULL); g_free(data); } else { result = nexus_parse_token(nexus, id, node); } g_free(address); } return result; } static void nexus_got_response_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data) { MsnNexus *nexus = data; MsnSession *session = nexus->session; const char *ticket; char *response; if (resp == NULL) { msn_session_set_error(session, MSN_ERROR_SERVCONN, _("Windows Live ID authentication:Unable to connect")); return; } if (!nexus_parse_collection(nexus, -1, xmlnode_get_child(resp->xml, "Body/RequestSecurityTokenResponseCollection"))) { msn_session_set_error(session, MSN_ERROR_SERVCONN, _("Windows Live ID authentication:Invalid response")); return; } ticket = msn_nexus_get_token_str(nexus, MSN_AUTH_MESSENGER); response = msn_rps_encrypt(nexus); msn_got_login_params(session, ticket, response); g_free(response); } /*when connect, do the SOAP Style windows Live ID authentication */ void msn_nexus_connect(MsnNexus *nexus) { MsnSession *session = nexus->session; const char *username; const char *password; char *password_xml; GString *domains; char *request; int i; MsnSoapMessage *soap; purple_debug_info("msn", "Starting Windows Live ID authentication\n"); msn_session_set_login_step(session, MSN_LOGIN_STEP_GET_COOKIE); username = purple_account_get_username(session->account); password = purple_connection_get_password(session->account->gc); password_xml = g_markup_escape_text(password, MIN(strlen(password), 16)); purple_debug_info("msn", "Logging on %s, with policy '%s', nonce '%s'\n", username, nexus->policy, nexus->nonce); domains = g_string_new(NULL); for (i = 0; i < nexus->token_len; i++) { g_string_append_printf(domains, MSN_SSO_RST_TEMPLATE, i+1, ticket_domains[i][SSO_VALID_TICKET_DOMAIN], ticket_domains[i][SSO_VALID_TICKET_POLICY] != NULL ? ticket_domains[i][SSO_VALID_TICKET_POLICY] : nexus->policy); } request = g_strdup_printf(MSN_SSO_TEMPLATE, username, password_xml, domains->str); g_free(password_xml); g_string_free(domains, TRUE); soap = msn_soap_message_new(NULL, xmlnode_from_str(request, -1)); g_free(request); msn_soap_message_send(session, soap, MSN_SSO_SERVER, SSO_POST_URL, TRUE, nexus_got_response_cb, nexus); } static void nexus_got_update_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data) { MsnNexusUpdateData *ud = data; MsnNexus *nexus = ud->nexus; char iv[8] = {0,0,0,0,0,0,0,0}; xmlnode *enckey; char *tmp; char *nonce; gsize len; char *key; GSList *updates; #if 0 char *decrypted_pp; #endif char *decrypted_data; if (resp == NULL) return; purple_debug_info("msn", "Got Update Response for %s.\n", ticket_domains[ud->id][SSO_VALID_TICKET_DOMAIN]); enckey = xmlnode_get_child(resp->xml, "Header/Security/DerivedKeyToken"); while (enckey) { if (g_str_equal(xmlnode_get_attrib(enckey, "Id"), "EncKey")) break; enckey = xmlnode_get_next_twin(enckey); } if (!enckey) { purple_debug_error("msn", "Invalid response in token update.\n"); return; } tmp = xmlnode_get_data(xmlnode_get_child(enckey, "Nonce")); nonce = (char *)purple_base64_decode(tmp, &len); key = rps_create_key(nexus->secret, 24, nonce, len); g_free(tmp); g_free(nonce); #if 0 /* Don't know what this is for yet */ tmp = xmlnode_get_data(xmlnode_get_child(resp->xml, "Header/EncryptedPP/EncryptedData/CipherData/CipherValue")); if (tmp) { decrypted_pp = des3_cbc(key, iv, tmp, len, TRUE); g_free(tmp); purple_debug_info("msn", "Got Response Header EncryptedPP: %s\n", decrypted_pp); g_free(decrypted_pp); } #endif tmp = xmlnode_get_data(xmlnode_get_child(resp->xml, "Body/EncryptedData/CipherData/CipherValue")); if (tmp) { char *unescaped; xmlnode *rstresponse; unescaped = (char *)purple_base64_decode(tmp, &len); g_free(tmp); decrypted_data = des3_cbc(key, iv, unescaped, len, TRUE); g_free(unescaped); purple_debug_info("msn", "Got Response Body EncryptedData: %s\n", decrypted_data); rstresponse = xmlnode_from_str(decrypted_data, -1); if (g_str_equal(rstresponse->name, "RequestSecurityTokenResponse")) nexus_parse_token(nexus, ud->id, rstresponse); else nexus_parse_collection(nexus, ud->id, rstresponse); g_free(decrypted_data); } updates = nexus->tokens[ud->id].updates; nexus->tokens[ud->id].updates = NULL; while (updates != NULL) { MsnNexusUpdateCallback *update = updates->data; if (update->cb) purple_timeout_add(0, update->cb, update->data); g_free(update); updates = g_slist_delete_link(updates, updates); } g_free(ud); } void msn_nexus_update_token(MsnNexus *nexus, int id, GSourceFunc cb, gpointer data) { MsnSession *session = nexus->session; MsnNexusUpdateData *ud; MsnNexusUpdateCallback *update; PurpleCipherContext *sha1; PurpleCipherContext *hmac; char *key; guchar digest[20]; struct tm *tm; time_t now; char *now_str; char *timestamp; char *timestamp_b64; char *domain; char *domain_b64; char *signedinfo; gint32 nonce[6]; int i; char *nonce_b64; char *signature_b64; guchar signature[20]; char *request; MsnSoapMessage *soap; update = g_new0(MsnNexusUpdateCallback, 1); update->cb = cb; update->data = data; if (nexus->tokens[id].updates != NULL) { /* Update already in progress. Just add to list and return. */ purple_debug_info("msn", "Ticket update for user '%s' on domain '%s' in progress. Adding request to queue.\n", purple_account_get_username(session->account), ticket_domains[id][SSO_VALID_TICKET_DOMAIN]); nexus->tokens[id].updates = g_slist_prepend(nexus->tokens[id].updates, update); return; } else { purple_debug_info("msn", "Updating ticket for user '%s' on domain '%s'\n", purple_account_get_username(session->account), ticket_domains[id][SSO_VALID_TICKET_DOMAIN]); nexus->tokens[id].updates = g_slist_prepend(nexus->tokens[id].updates, update); } ud = g_new0(MsnNexusUpdateData, 1); ud->nexus = nexus; ud->id = id; sha1 = purple_cipher_context_new_by_name("sha1", NULL); domain = g_strdup_printf(MSN_SSO_RST_TEMPLATE, id, ticket_domains[id][SSO_VALID_TICKET_DOMAIN], ticket_domains[id][SSO_VALID_TICKET_POLICY] != NULL ? ticket_domains[id][SSO_VALID_TICKET_POLICY] : nexus->policy); purple_cipher_context_append(sha1, (guchar *)domain, strlen(domain)); purple_cipher_context_digest(sha1, 20, digest, NULL); domain_b64 = purple_base64_encode(digest, 20); now = time(NULL); tm = gmtime(&now); now_str = g_strdup(purple_utf8_strftime("%Y-%m-%dT%H:%M:%SZ", tm)); now += 5*60; tm = gmtime(&now); timestamp = g_strdup_printf(MSN_SSO_TIMESTAMP_TEMPLATE, now_str, purple_utf8_strftime("%Y-%m-%dT%H:%M:%SZ", tm)); purple_cipher_context_reset(sha1, NULL); purple_cipher_context_append(sha1, (guchar *)timestamp, strlen(timestamp)); purple_cipher_context_digest(sha1, 20, digest, NULL); timestamp_b64 = purple_base64_encode(digest, 20); g_free(now_str); purple_cipher_context_destroy(sha1); signedinfo = g_strdup_printf(MSN_SSO_SIGNEDINFO_TEMPLATE, id, domain_b64, timestamp_b64); for (i = 0; i < 6; i++) nonce[i] = rand(); nonce_b64 = purple_base64_encode((guchar *)&nonce, sizeof(nonce)); key = rps_create_key(nexus->secret, 24, (char *)nonce, sizeof(nonce)); hmac = purple_cipher_context_new_by_name("hmac", NULL); purple_cipher_context_set_option(hmac, "hash", "sha1"); purple_cipher_context_set_key_with_len(hmac, (guchar *)key, 24); purple_cipher_context_append(hmac, (guchar *)signedinfo, strlen(signedinfo)); purple_cipher_context_digest(hmac, 20, signature, NULL); purple_cipher_context_destroy(hmac); signature_b64 = purple_base64_encode(signature, 20); request = g_strdup_printf(MSN_SSO_TOKEN_UPDATE_TEMPLATE, nexus->cipher, nonce_b64, timestamp, signedinfo, signature_b64, domain); g_free(nonce_b64); g_free(domain_b64); g_free(timestamp_b64); g_free(timestamp); g_free(key); g_free(signature_b64); g_free(signedinfo); g_free(domain); soap = msn_soap_message_new(NULL, xmlnode_from_str(request, -1)); g_free(request); msn_soap_message_send(session, soap, MSN_SSO_SERVER, SSO_POST_URL, TRUE, nexus_got_update_cb, ud); } GHashTable * msn_nexus_get_token(MsnNexus *nexus, MsnAuthDomains id) { g_return_val_if_fail(nexus != NULL, NULL); g_return_val_if_fail(id < nexus->token_len, NULL); return nexus->tokens[id].token; } const char * msn_nexus_get_token_str(MsnNexus *nexus, MsnAuthDomains id) { static char buf[1024]; GHashTable *token = msn_nexus_get_token(nexus, id); const char *msn_t; const char *msn_p; gint ret; g_return_val_if_fail(token != NULL, NULL); msn_t = g_hash_table_lookup(token, "t"); msn_p = g_hash_table_lookup(token, "p"); g_return_val_if_fail(msn_t != NULL, NULL); g_return_val_if_fail(msn_p != NULL, NULL); ret = g_snprintf(buf, sizeof(buf) - 1, "t=%s&p=%s", msn_t, msn_p); g_return_val_if_fail(ret != -1, NULL); return buf; }