view libpurple/protocols/msn/nexus.c @ 24430:ad685dadbaca

Our in-tree prpls probably shouldn't list authors who don't work on them anymore. I think most prpls just use NULL here. I don't think the field actually shows up in our UI anywhere. Also change the website listed for openq to pidgin.im instead of openq.sourceforge.net, which tells people to go to pidgin.im
author Mark Doliner <mark@kingant.net>
date Fri, 14 Nov 2008 18:46:19 +0000
parents 019d5ae3ad72
children 25667ca518d6
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_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;
};

#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;

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

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

	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("msn",
	                  "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, 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;
}