shithub: xmpp

ref: 7b3151f0a13823fccc39b2609155ffd1e5fd806f
dir: /conn.c/

View raw version
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <libsec.h>
#include <ndb.h>
#include "xml.h"
#include "xmpp.h"

enum
{
	Ascramsha1 = 1<<0,
	Adigestmd5 = 1<<1,
	Aplain     = 1<<2,
};

static Xelem*
expect(Biobuf *b, int flags, char *name)
{
	Xelem *x;
	int err;

	x = xmlread(b, flags, &err);
	if(x != nil && strcmp(x->n, name) != 0){
		werrstr("expected %q, got %q", name, x->n);
		xmlprint(x, 2);
		xmlfree(x);
		x = nil;
	}
	return x;
}

static int
authscramsha1(int fd, Biobuf *b, char *user, char *passwd)
{
	uchar cnonce[33], h[3][SHA1dlen], cp[SHA1dlen], svsig[SHA1dlen];
	char cnonce64[45], *snonce64, *salt64, *salt, *s, *ni, *svfst;
	int i, j, numiter, pwdlen, slen;
	Xelem *x;

	svfst = nil;
	genrandom(cnonce, sizeof(cnonce));
	if(enc64(cnonce64, sizeof(cnonce64), cnonce, sizeof(cnonce)) < 0)
		return -1;

	/* client first message */
	s = smprint("n,,n=%s,r=%s", user, cnonce64);
	fprint(fd,
		"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'"
		" mechanism='SCRAM-SHA-1'>%.*[</auth>",
		(int)strlen(s), s);
	free(s);
	s = nil;

	/* server first message */
	if((x = expect(b, 0, "challenge")) == nil || x->v == nil)
		goto error;
	slen = strlen(x->v);
	svfst = malloc(slen);
	if((slen = dec64((uchar*)svfst, slen, x->v, slen)) < 0)
		goto error;
	svfst[slen] = 0;
	s = strdup(svfst);
	if(s[0] != 'r' || s[1] != '=' || strncmp(cnonce64, s+2, strlen(cnonce64)) != 0)
		goto error;
	snonce64 = s + 2;
	salt64 = strstr(snonce64, ",s=");
	ni = salt64 != nil ? strstr(salt64+3, ",i=") : nil;
	if(!snonce64[0] || salt64 == nil || ni == nil)
		goto error;
	*salt64 = 0;
	salt64 += 3;
	*ni = 0;
	ni += 3;
	numiter = atoi(ni);
	if(!salt64[0] || numiter < 1)
		goto error;

	/* decode salt */
	slen = strlen(salt64);
	salt = malloc(slen+4);
	if((slen = dec64((uchar*)salt, slen, salt64, slen)) < 0){
		free(salt);
		goto error;
	}

	/* calc salted password in h[2] */
	salt[slen+0] = 0;
	salt[slen+1] = 0;
	salt[slen+2] = 0;
	salt[slen+3] = 1;
	pwdlen = strlen(passwd);
	hmac_sha1((uchar*)salt, slen+4, (uchar*)passwd, pwdlen, h[0], nil);
	free(salt);
	memcpy(h[2], h[0], SHA1dlen);
	for(i = 1; i < numiter; i++){
		hmac_sha1(h[0], SHA1dlen, (uchar*)passwd, pwdlen, h[1], nil);
		for(j = 0; j < SHA1dlen; j++)
			h[2][j] ^= h[1][j];
		memcpy(h[0], h[1], SHA1dlen);
	}

	/* client (h[0]), server (h[1]) and stored (h[2]) keys */
	hmac_sha1((uchar*)"Client Key", 10, h[2], SHA1dlen, h[0], nil);
	hmac_sha1((uchar*)"Server Key", 10, h[2], SHA1dlen, h[1], nil);
	sha1(h[0], SHA1dlen, h[2], nil);

	/* auth message */
	snonce64 = strdup(snonce64);
	free(s);
	s = smprint("n=%s,r=%s,%s,c=biws,r=%s", user, cnonce64, svfst, snonce64);
	free(svfst);
	svfst = nil;
	xmlfree(x);

	/* client and server signatures */
	hmac_sha1((uchar*)s, strlen(s), h[2], SHA1dlen, cp, nil);
	hmac_sha1((uchar*)s, strlen(s), h[1], SHA1dlen, svsig, nil);

	/* client proof */
	for(i = 0; i < SHA1dlen; i++)
		cp[i] = h[0][i] ^ cp[i];

	free(s);
	s = smprint("c=biws,r=%s,p=%.*[", snonce64, SHA1dlen, cp);
	fprint(fd,
		"<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"
		"%.*[</response>",
		(int)strlen(s), s);
	free(s);
	s = nil;
	free(snonce64);

	if((x = expect(b, 0, "success")) == nil || x->v == nil)
		goto error;
	slen = strlen(x->v);
	s = malloc(slen);
	if((slen = dec64((uchar*)s, slen, x->v, slen)) < 0)
		goto error;
	s[slen] = 0;
	svfst = smprint("v=%.*[", SHA1dlen, (uchar*)svsig);
	if(strcmp(s, svfst) != 0){
		werrstr("server signature doesn't match");
		goto error;
	}
	xmlfree(x);
	free(s);
	free(svfst);
	return 0;

error:
	werrstr("authscramsha1: %r");
	free(s);
	free(svfst);
	xmlfree(x);
	return -1;
}

static DigestState*
md5fmt(char *out, DigestState *st, char *fmt, ...)
{
	va_list arg;
	char *s;

	va_start(arg, fmt);
	s = vsmprint(fmt, arg);
	st = md5((uchar*)s, strlen(s), (uchar*)out, st);
	free(s);
	return st;
}

static int
authmd5(int fd, Biobuf *b, char *user, char *passwd)
{
	Xelem *x;
	int chsz;
	char *ch, *realm, *nonce, *s;
	char ha1[MD5dlen], ha2[MD5dlen], res[MD5dlen], cnonce[MD5dlen];
	DigestState *dgst;

	fprint(fd,
		"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'"
		" mechanism='DIGEST-MD5'/>");
	if((x = expect(b, 0, "challenge")) == nil || x->v == nil)
		return -1;
	chsz = strlen(x->v)/4*3;
	ch = malloc(chsz + 1);
	chsz = dec64((uchar*)ch, chsz, x->v, strlen(x->v));
	xmlfree(x);
	if(chsz < 0)
		return -1;

	realm = strstr(ch, "realm=");
	nonce = strstr(ch, "nonce=");
	if(realm != nil && (s = strchr(realm+7, '"')) != nil){
		*s = 0;
		realm += 7;
	}
	if(nonce != nil && (s = strchr(nonce+7, '"')) != nil){
		*s = 0;
		nonce += 7;
	}else if(nonce == nil){
		werrstr("nil nonce");
		free(ch);
		return -1;
	}

	genrandom((uchar*)cnonce, MD5dlen);

	if(realm == nil)
		realm = mydomain;

	/* ha1 = md5(md5(user:realm:passwd):nonce:cnonce:jid) */
	md5fmt(ha1, nil, "%s:%s:%s", user, realm, passwd);
	dgst = md5((uchar*)ha1, MD5dlen, nil, nil);
	md5fmt(ha1, dgst, ":%s:%.*lH:%s", nonce, MD5dlen, cnonce, myjid);

	/* ha2 = md5(method:digesturi) */
	md5fmt(ha2, nil, "AUTHENTICATE:xmpp/%s", mydomain);

	/* response = md5(ha1:nonce:nc:cnonce:qop:ha2) */
	md5fmt(res, nil, "%.*lH:%s:00000001:%.*lH:auth:%.*lH",
		MD5dlen, ha1, nonce, MD5dlen, cnonce, MD5dlen, ha2);

	s = smprint("username=\"%s\",realm=\"%s\",nonce=\"%s\","
		"cnonce=\"%.*lH\",nc=00000001,qop=auth,digest-uri=\"xmpp/%s\","
		"response=%.*lH,charset=utf-8,authzid=\"%s\"",
		user, realm, nonce, MD5dlen, cnonce, mydomain, MD5dlen, res, myjid);
	fprint(fd,
		"<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>"
		"%.*[</response>",
		(int)strlen(s), s);
	free(s);
	free(ch);

	if((x = expect(b, 0, "challenge")) != nil){
		xmlfree(x);
		fprint(fd, "<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>");
		if((x = expect(b, 0, "success")) != nil){
			xmlfree(x);
			return 0;
		}
	}
	return -1;
}

static int
authplain(int fd, Biobuf *b, char *user, char *passwd)
{
	int len;
	char *p;
	Xelem *x;

	p = smprint("%c%s%c%s", 0, user, 0, passwd);
	len = 1+strlen(user)+1+strlen(passwd);
	fprint(fd,
		"<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl'"
		" mechanism='PLAIN'>"
		"%.*[</auth>",
		len, p);
	free(p);
	if((x = expect(b, 0, "success")) != nil){
		xmlfree(x);
		return 0;
	}
	return -1;
}

static int
streamstart(int fd)
{
	return fprint(fd,
		"<?xml version='1.0'?>"
		"<stream:stream"
		" to='%Ӽ'"
		" xmlns='jabber:client'"
		" xmlns:stream='http://etherx.jabber.org/streams'"
		" version='1.0'>",
		mydomain);
}

static int
login(int fd, Biobuf *b, char *user, char *passwd)
{
	Xelem *x, *y;
	Thumbprint *th;
	TLSconn tls;
	int auth, r, oldfd, err;
	uchar hash[SHA1dlen];

	x = nil;
	if((th = initThumbprints("/sys/lib/tls/xmpp", nil, "x509")) == nil)
		return -1;
	if(Binit(b, fd, OREAD) != 0)
		return -1;
	streamstart(fd);
	xmlfree(xmlread(b, Xmlstartonly, &err));
	if(err != 0 || (x = expect(b, 0, "stream:features")) == nil)
		goto error;
	if(debug > 1)
		xmlprint(x, 2);

	/* require TLS */
	if(xmlget(x->ch, "starttls") == nil){
		werrstr("tls not supported");
		goto error;
	}
	xmlfree(x);
	fprint(fd, "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>");
	if((x = expect(b, 0, "proceed")) == nil)
		goto error;
	xmlfree(x);
	x = nil;
	Bterm(b);
	memset(&tls, 0, sizeof(tls));
	oldfd = fd;
	fd = tlsClient(fd, &tls);
	close(oldfd);
	if(th != nil){
		if(tls.cert == nil || tls.certlen < 1){
			werrstr("no server cert");
			goto error;
		}
		sha1(tls.cert, tls.certlen, hash, nil);
		if(!okThumbprint(hash, SHA1dlen, th)){
			werrstr("unknown thumbprint: %.*H", SHA1dlen, hash);
			goto error;
		}
		freeThumbprints(th);
	}
	free(tls.sessionID);
	free(tls.cert);
	if(Binit(b, fd, OREAD) != 0)
		return -1;
	if(fd < 0)
		goto error;

	streamstart(fd);
	xmlfree(xmlread(b, Xmlstartonly, &err));
	if(err != 0 || (x = expect(b, 0, "stream:features")) == nil)
		goto error;
	if(debug > 1)
		xmlprint(x, 2);
	auth = 0;
	if((y = xmlget(x->ch, "mechanisms")) != nil){
		if(debug > 0)
			fprint(2, "auth methods:");
		for(y = y->ch; y != nil; y = y->next){
			if(debug > 0)
				fprint(2, " %s", y->v);
			if(strcmp(y->v, "SCRAM-SHA-1") == 0)
				auth |= Ascramsha1;
			else if(strcmp(y->v, "DIGEST-MD5") == 0)
				auth |= Adigestmd5;
			else if(strcmp(y->v, "PLAIN") == 0 && plainallow)
				auth |= Aplain;
		}
		if(debug > 0)
			fprint(2, "\n");
	}
	xmlfree(x);
	x = nil;

	if(auth & Ascramsha1)
		r = authscramsha1(fd, b, user, passwd);
	else if(auth & Adigestmd5)
		r = authmd5(fd, b, user, passwd);
	else if(auth & Aplain)
		r = authplain(fd, b, user, passwd);
	else{
		werrstr("no supported auth methods");
		goto error;
	}

	if(r != 0)
		goto error;

	streamstart(fd);
	xmlfree(xmlread(b, Xmlstartonly, &err));
	if(err != 0)
		goto error;
	xmlfree(xmlread(b, Xmlstartonly, &err));
	if(err != 0)
		goto error;
	xmlfree(xmlread(b, 0, &err));
	if(err != 0)
		goto error;

	if(myresource == nil){
		fprint(fd,
			"<iq type='set' id='xml sucks'>"
			"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>");
	}else{
		fprint(fd,
			"<iq type='set' id='xml sucks'>"
			"<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'>"
			"<resource>%Ӽ</resource>"
			"</bind></iq>",
			myresource);
	}
	xmlfree(xmlread(b, 0, &err));
	if(err != 0)
		goto error;

	fprint(fd,
		"<iq type='set' id='xmpp sucks'>"
		"<session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>");
	xmlfree(xmlread(b, 0, &err));
	if(err == 0)
		return fd;

error:
	werrstr("login: %r");
	xmlfree(x);
	Bterm(b);
	return -1;
}

int
connect(Biobuf *b, char *user, char *passwd)
{
	int fd, clfd;
	Ndbtuple *srv, *s, *targ, *port;
	char *p;

	p = smprint("_xmpp-client._tcp.%s", server);
	srv = dnsquery(nil, p, "srv");
	free(p);
	for(s = srv, fd = -1; s != nil && fd < 0; s = s->entry){
		if(strcmp(s->attr, "dom") != 0)
			continue;

		targ = ndbfindattr(s, s->line, "target");
		port = ndbfindattr(s, s->line, "port");
		if(targ == nil || port == nil)
			continue;

		fd = dial(netmkaddr(targ->val, "tcp", port->val), nil, nil, &clfd);
	}
	ndbfree(srv);

	if(fd < 0){
		fd = dial(netmkaddr(server, "tcp", "5222"), nil, nil, &clfd);
		if(fd < 0)
			return -1;
	}
	write(clfd, "keepalive", 9);
	close(clfd);
	return login(fd, b, user, passwd);
}