view libpurple/protocols/msn/nexus.c @ 23476:9fdf0accd4aa

Finally got MSN token updating working (I think). So it seems after creating the signature, the xmlnode_to_str must have added some xmlns attributes which were sent to the server. I thought that CanonicalizationMethod stuff the XML specified meant the server would normalize everything nicely, but apparently not. I added the xmlns to the XML string before creating the signature and it looks like things work now. It just needs a full 8-hour test to be certain. Did a bit of re-factoring to the token response parsing, as well. It should now fail the first time something shows up that isn't expected, or if something is missing, instead of blindly going forward with half the tokens we requested. References #4875.
author Elliott Sales de Andrade <qulogic@pidgin.im>
date Sun, 15 Jun 2008 06:22:25 +0000
parents 1b98e2090a71
children c5891c8d0c28
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 "soap2.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_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;
	GSourceFunc cb;
	gpointer data;
};

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;

	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("msnp15", "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_response(MsnNexus *nexus, xmlnode *xml)
{
	xmlnode *node;
	xmlnode *cipher;
	xmlnode *secret;
	char *data;
	gboolean result;

	node = xmlnode_get_child(xml, "Body/RequestSecurityTokenResponseCollection/RequestSecurityTokenResponse");

	if (!node)
		return FALSE;

	/* The first node contains the stuff for updating tokens. */
	cipher = xmlnode_get_child(node, "RequestedSecurityToken/EncryptedData/CipherData/CipherValue");
	nexus->cipher = xmlnode_get_data(cipher);
	secret = xmlnode_get_child(node, "RequestedProofToken/BinarySecret");
	data = xmlnode_get_data(secret);
	nexus->secret = (char *)purple_base64_decode(data, NULL);
	g_free(data);

	result = TRUE;
	for (node = node->next; node && result; node = node->next)
		result = nexus_parse_token(nexus, -1, node);

	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_response(nexus, resp->xml)) {
		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;
	char *password;
	GString *domains;
	char *request;
	int i;

	MsnSoapMessage *soap;

	purple_debug_info("MSN Nexus","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 = g_strndup(purple_connection_get_password(session->account->gc), 16);

	purple_debug_info("msnp15", "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, domains->str);
	g_free(password);
	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,
	                      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;

#if 0
	char *decrypted_pp;
#endif
	char *decrypted_data;

	purple_debug_info("msnp15", "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_info("msnp15", "Invalid Response.\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("msnp15", "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("msnp15", "Got Response Body EncryptedData: %s\n", decrypted_data);

		rstresponse = xmlnode_from_str(decrypted_data, -1);
		g_hash_table_remove_all(nexus->tokens[ud->id].token);
		nexus_parse_token(nexus, ud->id, rstresponse);
		g_free(decrypted_data);
	}

	if (ud->cb)
		purple_timeout_add(0, ud->cb, ud->data);

	g_free(ud);
}

void
msn_nexus_update_token(MsnNexus *nexus, int id, GSourceFunc cb, gpointer data)
{
	MsnSession *session = nexus->session;
	MsnNexusUpdateData *ud;
	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;

	purple_debug_info("msnp15",
	                  "Updating ticket for user '%s' on domain '%s'\n",
	                  purple_account_get_username(session->account),
	                  ticket_domains[id][SSO_VALID_TICKET_DOMAIN]);

	ud = g_new0(MsnNexusUpdateData, 1);
	ud->nexus = nexus;
	ud->id = id;
	ud->cb = cb;
	ud->data = data;

	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,
	                      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;
}