xmpp/sasl.go
changeset 143 62166e57800e
child 145 21a390dd3506
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/xmpp/sasl.go	Sun Sep 15 12:00:17 2013 -0600
@@ -0,0 +1,190 @@
+// Deal with SASL authentication.
+
+package xmpp
+
+import (
+	"strings"
+	"encoding/xml"
+	"encoding/base64"
+	"fmt"
+	"math/big"
+	"crypto/rand"
+	"regexp"
+	"crypto/md5"
+)
+
+// BUG(cjyar): Doesn't implement TLS/SASL EXTERNAL.
+func (cl *Client) chooseSasl(fe *Features) {
+	var digestMd5 bool
+	for _, m := range fe.Mechanisms.Mechanism {
+		switch strings.ToLower(m) {
+		case "digest-md5":
+			digestMd5 = true
+		}
+	}
+
+	if digestMd5 {
+		auth := &auth{XMLName: xml.Name{Space: NsSASL, Local: "auth"}, Mechanism: "DIGEST-MD5"}
+		cl.sendXml <- auth
+	}
+}
+
+func (cl *Client) handleSasl(srv *auth) {
+	switch strings.ToLower(srv.XMLName.Local) {
+	case "challenge":
+		b64 := base64.StdEncoding
+		str, err := b64.DecodeString(srv.Chardata)
+		if err != nil {
+			Warn.Logf("SASL challenge decode: %s", err)
+			return
+		}
+		srvMap := parseSasl(string(str))
+
+		if cl.saslExpected == "" {
+			cl.saslDigest1(srvMap)
+		} else {
+			cl.saslDigest2(srvMap)
+		}
+	case "failure":
+		Info.Log("SASL authentication failed")
+	case "success":
+		Info.Log("Sasl authentication succeeded")
+		cl.Features = nil
+		ss := &stream{To: cl.Jid.Domain, Version: XMPPVersion}
+		cl.sendXml <- ss
+	}
+}
+
+func (cl *Client) saslDigest1(srvMap map[string]string) {
+	// Make sure it supports qop=auth
+	var hasAuth bool
+	for _, qop := range strings.Fields(srvMap["qop"]) {
+		if qop == "auth" {
+			hasAuth = true
+		}
+	}
+	if !hasAuth {
+		Warn.Log("Server doesn't support SASL auth")
+		return
+	}
+
+	// Pick a realm.
+	var realm string
+	if srvMap["realm"] != "" {
+		realm = strings.Fields(srvMap["realm"])[0]
+	}
+
+	passwd := cl.password
+	nonce := srvMap["nonce"]
+	digestUri := "xmpp/" + cl.Jid.Domain
+	nonceCount := int32(1)
+	nonceCountStr := fmt.Sprintf("%08x", nonceCount)
+
+	// Begin building the response. Username is
+	// user@domain or just domain.
+	var username string
+	if cl.Jid.Node == "" {
+		username = cl.Jid.Domain
+	} else {
+		username = cl.Jid.Node
+	}
+
+	// Generate our own nonce from random data.
+	randSize := big.NewInt(0)
+	randSize.Lsh(big.NewInt(1), 64)
+	cnonce, err := rand.Int(rand.Reader, randSize)
+	if err != nil {
+		Warn.Logf("SASL rand: %s", err)
+		return
+	}
+	cnonceStr := fmt.Sprintf("%016x", cnonce)
+
+	/* Now encode the actual password response, as well as the
+	 * expected next challenge from the server. */
+	response := saslDigestResponse(username, realm, passwd, nonce,
+		cnonceStr, "AUTHENTICATE", digestUri, nonceCountStr)
+	next := saslDigestResponse(username, realm, passwd, nonce,
+		cnonceStr, "", digestUri, nonceCountStr)
+	cl.saslExpected = next
+
+	// Build the map which will be encoded.
+	clMap := make(map[string]string)
+	clMap["realm"] = `"` + realm + `"`
+	clMap["username"] = `"` + username + `"`
+	clMap["nonce"] = `"` + nonce + `"`
+	clMap["cnonce"] = `"` + cnonceStr + `"`
+	clMap["nc"] = nonceCountStr
+	clMap["qop"] = "auth"
+	clMap["digest-uri"] = `"` + digestUri + `"`
+	clMap["response"] = response
+	if srvMap["charset"] == "utf-8" {
+		clMap["charset"] = "utf-8"
+	}
+
+	// Encode the map and send it.
+	clStr := packSasl(clMap)
+	b64 := base64.StdEncoding
+	clObj := &auth{XMLName: xml.Name{Space: NsSASL, Local: "response"}, Chardata: b64.EncodeToString([]byte(clStr))}
+	cl.sendXml <- clObj
+}
+
+func (cl *Client) saslDigest2(srvMap map[string]string) {
+	if cl.saslExpected == srvMap["rspauth"] {
+		clObj := &auth{XMLName: xml.Name{Space: NsSASL, Local: "response"}}
+		cl.sendXml <- clObj
+	} else {
+		clObj := &auth{XMLName: xml.Name{Space: NsSASL, Local: "failure"}, Any: &Generic{XMLName: xml.Name{Space: NsSASL,
+			Local: "abort"}}}
+		cl.sendXml <- clObj
+	}
+}
+
+// Takes a string like `key1=value1,key2="value2"...` and returns a
+// key/value map.
+func parseSasl(in string) map[string]string {
+	re := regexp.MustCompile(`([^=]+)="?([^",]+)"?,?`)
+	strs := re.FindAllStringSubmatch(in, -1)
+	m := make(map[string]string)
+	for _, pair := range strs {
+		key := strings.ToLower(string(pair[1]))
+		value := string(pair[2])
+		m[key] = value
+	}
+	return m
+}
+
+// Inverse of parseSasl().
+func packSasl(m map[string]string) string {
+	var terms []string
+	for key, value := range m {
+		if key == "" || value == "" || value == `""` {
+			continue
+		}
+		terms = append(terms, key+"="+value)
+	}
+	return strings.Join(terms, ",")
+}
+
+// Computes the response string for digest authentication.
+func saslDigestResponse(username, realm, passwd, nonce, cnonceStr,
+	authenticate, digestUri, nonceCountStr string) string {
+	h := func(text string) []byte {
+		h := md5.New()
+		h.Write([]byte(text))
+		return h.Sum(nil)
+	}
+	hex := func(bytes []byte) string {
+		return fmt.Sprintf("%x", bytes)
+	}
+	kd := func(secret, data string) []byte {
+		return h(secret + ":" + data)
+	}
+
+	a1 := string(h(username+":"+realm+":"+passwd)) + ":" +
+		nonce + ":" + cnonceStr
+	a2 := authenticate + ":" + digestUri
+	response := hex(kd(hex(h(a1)), nonce+":"+
+		nonceCountStr+":"+cnonceStr+":auth:"+
+		hex(h(a2))))
+	return response
+}