diff -r 0ff033eed887 -r 62166e57800e xmpp/sasl.go --- /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 +}