diff libpurple/protocols/msn/nexus.c @ 23489:25899ec348a4

Patch 2 from Qulogic, this one adds SSO authentication committer: Ka-Hing Cheung <khc@hxbc.us>
author Elliott Sales de Andrade <qulogic@pidgin.im>
date Wed, 26 Dec 2007 00:34:12 +0000
parents f5874552b8d5
children 72aa2ccad28d
line wrap: on
line diff
--- a/libpurple/protocols/msn/nexus.c	Wed Dec 26 00:33:39 2007 +0000
+++ b/libpurple/protocols/msn/nexus.c	Wed Dec 26 00:34:12 2007 +0000
@@ -26,7 +26,24 @@
 #include "nexus.h"
 #include "notification.h"
 
-#undef NEXUS_LOGIN_TWN
+
+/**************************************************************************
+ * 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"},        /* Messenger website authentication. */
+	{"contacts.msn.com", "MBI"},             /* Authentication for the Contact server. */
+	{"messengersecure.live.com", "MBI_SSL"}, /* Unknown */
+	{"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
@@ -36,12 +53,17 @@
 msn_nexus_new(MsnSession *session)
 {
 	MsnNexus *nexus;
+	int i;
 
 	nexus = g_new0(MsnNexus, 1);
 	nexus->session = session;
 
-	nexus->challenge_data = g_hash_table_new_full(g_str_hash,
-		g_str_equal, g_free, g_free);
+	nexus->token_len = sizeof(ticket_domains) / sizeof(char *[2]);
+	nexus->tokens = g_malloc(sizeof(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;
 }
@@ -49,47 +71,198 @@
 void
 msn_nexus_destroy(MsnNexus *nexus)
 {
-	if (nexus->challenge_data != NULL)
-		g_hash_table_destroy(nexus->challenge_data);
+	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);
+}
+
+/**************************************************************************
+ * RPS/SSO Authentication
+ **************************************************************************/
+
+static char *
+sha1_hmac(const char *key, int key_len, const char *message, int msg_len)
+{
+	PurpleCipherContext *context;
+	char *result;
+	gboolean ret;
+
+	context = purple_cipher_context_new_by_name("hmac", NULL);
+	purple_cipher_context_set_option(context, "hash", "sha1");
+	purple_cipher_context_set_key_with_len(context, (guchar *)key, key_len);
+
+	purple_cipher_context_append(context, (guchar *)message, msg_len);
+	result = g_malloc(20);
+	ret = purple_cipher_context_digest(context, 20, (guchar *)result, NULL);
+
+	purple_cipher_context_destroy(context);
+
+	return result;
+}
+
+static char *
+rps_create_key(const char *key, int key_len, const char *data, size_t data_len)
+{
+	char *hash1, *hash2, *hash3, *hash4;
+	char *result;
+
+	hash1 = sha1_hmac(key, key_len, data, data_len);
+	hash1 = g_realloc(hash1, 20 + data_len);
+	memcpy(hash1 + 20, data, data_len);
+	hash2 = sha1_hmac(key, key_len, hash1, 20 + data_len);
+
+	hash3 = sha1_hmac(key, key_len, hash1, 20);
+
+	hash3 = g_realloc(hash3, 20 + data_len);
+	memcpy(hash3 + 20, data, data_len);
+	hash4 = sha1_hmac(key, key_len, hash3, 20 + data_len);
+
+	result = g_malloc(24);
+	memcpy(result, hash2, 20);
+	memcpy(result + 20, hash4, 4);
+
+	g_free(hash1);
+	g_free(hash2);
+	g_free(hash3);
+	g_free(hash4);
+
+	return result;
+}
+
+static char *
+des3_cbc(const char *key, const char *iv, const char *data, int len)
+{
+	PurpleCipherContext *des3;
+	char *out;
+	size_t outlen;
 
-	g_free(nexus);
+	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);
+	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 = "WS-SecureConversationSESSION KEY HASH";
+	const char *magic2 = "WS-SecureConversationSESSION KEY ENCRYPTION";
+	size_t len;
+	char *hash;
+	char *key1, *key2, *key3;
+	gsize key1_len;
+	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);
+	len = strlen(magic1);
+	key2 = rps_create_key(key1, key1_len, magic1, len);
+	len = strlen(magic2);
+	key3 = rps_create_key(key1, key1_len, magic2, len);
+
+	usr_key->iv[0] = 0x46; //rand() % 256;
+	usr_key->iv[1] = 0xC4;
+	usr_key->iv[2] = 0x14;
+	usr_key->iv[3] = 0x9F;
+	usr_key->iv[4] = 0xFF;
+	usr_key->iv[5] = 0xFC;
+	usr_key->iv[6] = 0x91;
+	usr_key->iv[7] = 0x61;
+
+	len = strlen(nexus->nonce);
+	hash = sha1_hmac(key2, 24, nexus->nonce, len);
+
+	/* 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);
+	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(hash);
+	g_free(cipher);
+
+	response = purple_base64_encode((guchar *)usr_key, sizeof(MsnUsrKey));
+
+	g_free(usr_key);
+
+	return response;
 }
 
 /**************************************************************************
  * Login
  **************************************************************************/
 
-static void
-nexus_got_response_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data)
+static gboolean
+nexus_parse_response(MsnNexus *nexus, xmlnode *xml)
 {
-	MsnNexus *nexus = data;
-	MsnSession *session = nexus->session;
 	xmlnode *node;
+	gboolean result = FALSE;
 
-	if (resp == NULL) {
-		msn_session_set_error(session, MSN_ERROR_SERVCONN, _("Windows Live ID authentication:Unable to connect"));
-		return;
-	}
+	node = msn_soap_xml_get(xml, "Body/RequestSecurityTokenResponseCollection/RequestSecurityTokenResponse");
 
-	node = msn_soap_xml_get(resp->xml,	"Body/"
-		"RequestSecurityTokenResponseCollection/RequestSecurityTokenResponse");
+	if (node)
+		node = node->next;	/* The first one is not useful */
+	else
+		return FALSE;
 
 	for (; node; node = node->next) {
-		xmlnode *token = msn_soap_xml_get(node,
-			"RequestedSecurityToken/BinarySecurityToken");
+		xmlnode *token = msn_soap_xml_get(node, "RequestedSecurityToken/BinarySecurityToken");
+		xmlnode *secret = msn_soap_xml_get(node, "RequestedProofToken/BinarySecret");
+		xmlnode *expires = msn_soap_xml_get(node, "LifeTime/Expires");
 
 		if (token) {
 			char *token_str = xmlnode_get_data(token);
+			const char *id_str = xmlnode_get_attrib(token, "Id");
 			char **elems, **cur, **tokens;
-			char *msn_twn_t, *msn_twn_p, *cert_str;
+			int id;
 
 			if (token_str == NULL) continue;
+			if (id_str == NULL) continue;
+
+			id = atol(id_str + 7) - 1;	/* 'Compact#' or 'PPToken#' */
+			if (id >= nexus->token_len)
+				continue;	/* Where did this come from? */
 
 			elems = g_strsplit(token_str, "&", 0);
 
 			for (cur = elems; *cur != NULL; cur++){
 				tokens = g_strsplit(*cur, "=", 2);
-				g_hash_table_insert(nexus->challenge_data, tokens[0], tokens[1]);
+				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);
 			}
@@ -97,33 +270,55 @@
 			g_free(token_str);
 			g_strfreev(elems);
 
-			msn_twn_t = g_hash_table_lookup(nexus->challenge_data, "t");
-			msn_twn_p = g_hash_table_lookup(nexus->challenge_data, "p");
-
-			/*setup the t and p parameter for session*/
-			if (session->passport_info.t != NULL){
-				g_free(session->passport_info.t);
-			}
-			session->passport_info.t = g_strdup(msn_twn_t);
+			if (secret)
+				nexus->tokens[id].secret = g_strdup(xmlnode_get_data(secret));
+			else
+				nexus->tokens[id].secret = NULL;
 
-			if (session->passport_info.p != NULL)
-				g_free(session->passport_info.p);
-			session->passport_info.p = g_strdup(msn_twn_p);
-
-			cert_str = g_strdup_printf("t=%s&p=%s",msn_twn_t,msn_twn_p);
-			msn_got_login_params(session, cert_str);
+			/* Yay for MS using ISO-8601 */
+			nexus->tokens[id].expiry = purple_str_to_time(xmlnode_get_data(expires),
+			                                              FALSE, NULL, NULL, NULL);
 
-			purple_debug_info("MSN Nexus","Close nexus connection!\n");
-			g_free(cert_str);
-			msn_nexus_destroy(nexus);
-			session->nexus = NULL;
-
-			return;
+			purple_debug_info("msnp15", "Updated ticket for domain '%s'\n",
+			                  ticket_domains[id][SSO_VALID_TICKET_DOMAIN]);
+			result = TRUE;
 		}
 	}
 
-	/* we must have failed! */
-	msn_session_set_error(session, MSN_ERROR_AUTH, _("Windows Live ID authentication: cannot find authenticate token in server response"));
+	return result;
+}
+
+static void
+nexus_got_response_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data)
+{
+	MsnNexus *nexus = data;
+	MsnSession *session = nexus->session;
+	char *msn_twn_t, *msn_twn_p, *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;
+	}
+
+	/*setup the t and p parameter for session*/
+	msn_twn_t = g_hash_table_lookup(nexus->tokens[MSN_AUTH_MESSENGER].token, "t");
+	msn_twn_p = g_hash_table_lookup(nexus->tokens[MSN_AUTH_MESSENGER].token, "p");
+	g_free(session->passport_info.t);
+	session->passport_info.t = g_strdup(msn_twn_t);
+	g_free(session->passport_info.p);
+	session->passport_info.p = g_strdup(msn_twn_p);
+
+	ticket = g_strdup_printf("t=%s&p=%s", msn_twn_t, msn_twn_p);
+	response = msn_rps_encrypt(nexus);
+	msn_got_login_params(session, ticket, response);
+	g_free(ticket);
+	g_free(response);
 }
 
 /*when connect, do the SOAP Style windows Live ID authentication */
@@ -131,92 +326,113 @@
 msn_nexus_connect(MsnNexus *nexus)
 {
 	MsnSession *session = nexus->session;
-	char *ru,*lc,*id,*tw,*ct,*kpp,*kv,*ver,*rn,*tpf;
-	char *fs0,*fs;
 	char *username, *password;
-	char *tail;
-#ifdef NEXUS_LOGIN_TWN
-	char *challenge_str;
-#else
-	char *rst1_str,*rst2_str,*rst3_str;
-#endif
+	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);
 
-	/*prepare the Windows Live ID authentication token*/
 	username = g_strdup(purple_account_get_username(session->account));
 	password = g_strndup(purple_connection_get_password(session->account->gc), 16);
 
-	lc =	(char *)g_hash_table_lookup(nexus->challenge_data, "lc");
-	id =	(char *)g_hash_table_lookup(nexus->challenge_data, "id");
-	tw =	(char *)g_hash_table_lookup(nexus->challenge_data, "tw");
-	fs0=	(char *)g_hash_table_lookup(nexus->challenge_data, "fs");
-	ru =	(char *)g_hash_table_lookup(nexus->challenge_data, "ru");
-	ct =	(char *)g_hash_table_lookup(nexus->challenge_data, "ct");
-	kpp=	(char *)g_hash_table_lookup(nexus->challenge_data, "kpp");
-	kv =	(char *)g_hash_table_lookup(nexus->challenge_data, "kv");
-	ver=	(char *)g_hash_table_lookup(nexus->challenge_data, "ver");
-	rn =	(char *)g_hash_table_lookup(nexus->challenge_data, "rn");
-	tpf=	(char *)g_hash_table_lookup(nexus->challenge_data, "tpf");
+	purple_debug_info("msnp15", "Logging on %s, with policy '%s', nonce '%s'\n",
+	                  username, nexus->policy, nexus->nonce);
 
-	/*
-	 * add some fail-safe code to avoid windows Purple Crash bug #1540454
-	 * If any of these string is NULL, will return Authentication Fail!
-	 * for when windows g_strdup_printf() implementation get NULL point,It crashed!
-	 */
-	if(!(lc && id && tw && ru && ct && kpp && kv && ver && tpf)){
-		purple_debug_error("MSN Nexus","WLM Authenticate Key Error!\n");
-		msn_session_set_error(session, MSN_ERROR_AUTH, _("Windows Live ID authentication Failed"));
-		g_free(username);
-		g_free(password);
-		msn_nexus_destroy(nexus);
-		session->nexus = NULL;
-		return;
+	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);
 	}
 
-	/*
-	 * in old MSN NS server's "USR TWN S" return,didn't include fs string
-	 * so we use a default "1" for fs.
-	 */
-	if(fs0){
-		fs = g_strdup(fs0);
-	}else{
-		fs = g_strdup("1");
-	}
-
-#ifdef NEXUS_LOGIN_TWN
-	challenge_str = g_strdup_printf(
-		"lc=%s&amp;id=%s&amp;tw=%s&amp;fs=%s&amp;ru=%s&amp;ct=%s&amp;kpp=%s&amp;kv=%s&amp;ver=%s&amp;rn=%s&amp;tpf=%s\r\n",
-		lc,id,tw,fs,ru,ct,kpp,kv,ver,rn,tpf
-		);
+	request = g_strdup_printf(MSN_SSO_TEMPLATE, username, password, domains->str);
+	g_free(username);
+	g_free(password);
+	g_string_free(domains, TRUE);
 
-	/*build the SOAP windows Live ID XML body */
-	tail = g_strdup_printf(TWN_ENVELOP_TEMPLATE, username, password, challenge_str);
-	g_free(challenge_str);
-#else
-	rst1_str = g_strdup_printf(
-		"id=%s&amp;tw=%s&amp;fs=%s&amp;kpp=%s&amp;kv=%s&amp;ver=%s&amp;rn=%s",
-		id,tw,fs,kpp,kv,ver,rn
-		);
-	rst2_str = g_strdup_printf(
-		"fs=%s&amp;id=%s&amp;kv=%s&amp;rn=%s&amp;tw=%s&amp;ver=%s",
-		fs,id,kv,rn,tw,ver
-		);
-	rst3_str = g_strdup_printf("id=%s",id);
-	tail = g_strdup_printf(TWN_LIVE_ENVELOP_TEMPLATE,username,password,rst1_str,rst2_str,rst3_str);
-	g_free(rst1_str);
-	g_free(rst2_str);
-	g_free(rst3_str);
-#endif
-	g_free(fs);
-	g_free(password);
+	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);
+}
 
-	soap = msn_soap_message_new(NULL, xmlnode_from_str(tail, -1));
-	g_free(tail);
-	msn_soap_message_send(nexus->session, soap, MSN_TWN_SERVER, TWN_POST_URL,
-		nexus_got_response_cb, nexus);
+static void
+nexus_got_update_cb(MsnSoapMessage *req, MsnSoapMessage *resp, gpointer data)
+{
+	MsnNexus *nexus = data;
+
+	nexus_parse_response(nexus, resp->xml);
 }
 
+static void
+msn_nexus_update_token(MsnNexus *nexus, int id)
+{
+	MsnSession *session = nexus->session;
+	char *username, *password;
+	char *domain;
+	char *request;
+
+	MsnSoapMessage *soap;
+
+	username = g_strdup(purple_account_get_username(session->account));
+	password = g_strndup(purple_connection_get_password(session->account->gc), 16);
+
+	purple_debug_info("msnp15", "Updating ticket for user '%s' on domain '%s'\n",
+	                  username, ticket_domains[id][SSO_VALID_TICKET_DOMAIN]);
+
+	/* TODO: This really assumes if we send RSTn, the server responds with
+	 Compactn, even if there is no RST(n-1). This needs checking.
+	*/
+	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);
+
+	request = g_strdup_printf(MSN_SSO_TEMPLATE, username, password, domain);
+	g_free(username);
+	g_free(password);
+	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, nexus);
+}
+
+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);
+
+	if (time(NULL) > nexus->tokens[id].expiry)
+		msn_nexus_update_token(nexus, id);
+
+	return g_hash_table_ref(nexus->tokens[id].token);
+}
+
+char *
+msn_nexus_get_token_str(MsnNexus *session, MsnAuthDomains id)
+{
+#if 0
+	GHashTable *token = msn_nexus_get_token(nexus, id);
+	GString *token_str;
+
+	g_return_val_if_fail(token != NULL, NULL);
+
+	token_str = g_string_new(NULL);
+	g_hash_table_foreach(token, GHFunc func, token_str);
+
+	g_hash_table_unref (token);
+#endif
+	return NULL;
+}
+