xmpp/sasl.go
changeset 143 62166e57800e
child 145 21a390dd3506
equal deleted inserted replaced
142:0ff033eed887 143:62166e57800e
       
     1 // Deal with SASL authentication.
       
     2 
       
     3 package xmpp
       
     4 
       
     5 import (
       
     6 	"strings"
       
     7 	"encoding/xml"
       
     8 	"encoding/base64"
       
     9 	"fmt"
       
    10 	"math/big"
       
    11 	"crypto/rand"
       
    12 	"regexp"
       
    13 	"crypto/md5"
       
    14 )
       
    15 
       
    16 // BUG(cjyar): Doesn't implement TLS/SASL EXTERNAL.
       
    17 func (cl *Client) chooseSasl(fe *Features) {
       
    18 	var digestMd5 bool
       
    19 	for _, m := range fe.Mechanisms.Mechanism {
       
    20 		switch strings.ToLower(m) {
       
    21 		case "digest-md5":
       
    22 			digestMd5 = true
       
    23 		}
       
    24 	}
       
    25 
       
    26 	if digestMd5 {
       
    27 		auth := &auth{XMLName: xml.Name{Space: NsSASL, Local: "auth"}, Mechanism: "DIGEST-MD5"}
       
    28 		cl.sendXml <- auth
       
    29 	}
       
    30 }
       
    31 
       
    32 func (cl *Client) handleSasl(srv *auth) {
       
    33 	switch strings.ToLower(srv.XMLName.Local) {
       
    34 	case "challenge":
       
    35 		b64 := base64.StdEncoding
       
    36 		str, err := b64.DecodeString(srv.Chardata)
       
    37 		if err != nil {
       
    38 			Warn.Logf("SASL challenge decode: %s", err)
       
    39 			return
       
    40 		}
       
    41 		srvMap := parseSasl(string(str))
       
    42 
       
    43 		if cl.saslExpected == "" {
       
    44 			cl.saslDigest1(srvMap)
       
    45 		} else {
       
    46 			cl.saslDigest2(srvMap)
       
    47 		}
       
    48 	case "failure":
       
    49 		Info.Log("SASL authentication failed")
       
    50 	case "success":
       
    51 		Info.Log("Sasl authentication succeeded")
       
    52 		cl.Features = nil
       
    53 		ss := &stream{To: cl.Jid.Domain, Version: XMPPVersion}
       
    54 		cl.sendXml <- ss
       
    55 	}
       
    56 }
       
    57 
       
    58 func (cl *Client) saslDigest1(srvMap map[string]string) {
       
    59 	// Make sure it supports qop=auth
       
    60 	var hasAuth bool
       
    61 	for _, qop := range strings.Fields(srvMap["qop"]) {
       
    62 		if qop == "auth" {
       
    63 			hasAuth = true
       
    64 		}
       
    65 	}
       
    66 	if !hasAuth {
       
    67 		Warn.Log("Server doesn't support SASL auth")
       
    68 		return
       
    69 	}
       
    70 
       
    71 	// Pick a realm.
       
    72 	var realm string
       
    73 	if srvMap["realm"] != "" {
       
    74 		realm = strings.Fields(srvMap["realm"])[0]
       
    75 	}
       
    76 
       
    77 	passwd := cl.password
       
    78 	nonce := srvMap["nonce"]
       
    79 	digestUri := "xmpp/" + cl.Jid.Domain
       
    80 	nonceCount := int32(1)
       
    81 	nonceCountStr := fmt.Sprintf("%08x", nonceCount)
       
    82 
       
    83 	// Begin building the response. Username is
       
    84 	// user@domain or just domain.
       
    85 	var username string
       
    86 	if cl.Jid.Node == "" {
       
    87 		username = cl.Jid.Domain
       
    88 	} else {
       
    89 		username = cl.Jid.Node
       
    90 	}
       
    91 
       
    92 	// Generate our own nonce from random data.
       
    93 	randSize := big.NewInt(0)
       
    94 	randSize.Lsh(big.NewInt(1), 64)
       
    95 	cnonce, err := rand.Int(rand.Reader, randSize)
       
    96 	if err != nil {
       
    97 		Warn.Logf("SASL rand: %s", err)
       
    98 		return
       
    99 	}
       
   100 	cnonceStr := fmt.Sprintf("%016x", cnonce)
       
   101 
       
   102 	/* Now encode the actual password response, as well as the
       
   103 	 * expected next challenge from the server. */
       
   104 	response := saslDigestResponse(username, realm, passwd, nonce,
       
   105 		cnonceStr, "AUTHENTICATE", digestUri, nonceCountStr)
       
   106 	next := saslDigestResponse(username, realm, passwd, nonce,
       
   107 		cnonceStr, "", digestUri, nonceCountStr)
       
   108 	cl.saslExpected = next
       
   109 
       
   110 	// Build the map which will be encoded.
       
   111 	clMap := make(map[string]string)
       
   112 	clMap["realm"] = `"` + realm + `"`
       
   113 	clMap["username"] = `"` + username + `"`
       
   114 	clMap["nonce"] = `"` + nonce + `"`
       
   115 	clMap["cnonce"] = `"` + cnonceStr + `"`
       
   116 	clMap["nc"] = nonceCountStr
       
   117 	clMap["qop"] = "auth"
       
   118 	clMap["digest-uri"] = `"` + digestUri + `"`
       
   119 	clMap["response"] = response
       
   120 	if srvMap["charset"] == "utf-8" {
       
   121 		clMap["charset"] = "utf-8"
       
   122 	}
       
   123 
       
   124 	// Encode the map and send it.
       
   125 	clStr := packSasl(clMap)
       
   126 	b64 := base64.StdEncoding
       
   127 	clObj := &auth{XMLName: xml.Name{Space: NsSASL, Local: "response"}, Chardata: b64.EncodeToString([]byte(clStr))}
       
   128 	cl.sendXml <- clObj
       
   129 }
       
   130 
       
   131 func (cl *Client) saslDigest2(srvMap map[string]string) {
       
   132 	if cl.saslExpected == srvMap["rspauth"] {
       
   133 		clObj := &auth{XMLName: xml.Name{Space: NsSASL, Local: "response"}}
       
   134 		cl.sendXml <- clObj
       
   135 	} else {
       
   136 		clObj := &auth{XMLName: xml.Name{Space: NsSASL, Local: "failure"}, Any: &Generic{XMLName: xml.Name{Space: NsSASL,
       
   137 			Local: "abort"}}}
       
   138 		cl.sendXml <- clObj
       
   139 	}
       
   140 }
       
   141 
       
   142 // Takes a string like `key1=value1,key2="value2"...` and returns a
       
   143 // key/value map.
       
   144 func parseSasl(in string) map[string]string {
       
   145 	re := regexp.MustCompile(`([^=]+)="?([^",]+)"?,?`)
       
   146 	strs := re.FindAllStringSubmatch(in, -1)
       
   147 	m := make(map[string]string)
       
   148 	for _, pair := range strs {
       
   149 		key := strings.ToLower(string(pair[1]))
       
   150 		value := string(pair[2])
       
   151 		m[key] = value
       
   152 	}
       
   153 	return m
       
   154 }
       
   155 
       
   156 // Inverse of parseSasl().
       
   157 func packSasl(m map[string]string) string {
       
   158 	var terms []string
       
   159 	for key, value := range m {
       
   160 		if key == "" || value == "" || value == `""` {
       
   161 			continue
       
   162 		}
       
   163 		terms = append(terms, key+"="+value)
       
   164 	}
       
   165 	return strings.Join(terms, ",")
       
   166 }
       
   167 
       
   168 // Computes the response string for digest authentication.
       
   169 func saslDigestResponse(username, realm, passwd, nonce, cnonceStr,
       
   170 	authenticate, digestUri, nonceCountStr string) string {
       
   171 	h := func(text string) []byte {
       
   172 		h := md5.New()
       
   173 		h.Write([]byte(text))
       
   174 		return h.Sum(nil)
       
   175 	}
       
   176 	hex := func(bytes []byte) string {
       
   177 		return fmt.Sprintf("%x", bytes)
       
   178 	}
       
   179 	kd := func(secret, data string) []byte {
       
   180 		return h(secret + ":" + data)
       
   181 	}
       
   182 
       
   183 	a1 := string(h(username+":"+realm+":"+passwd)) + ":" +
       
   184 		nonce + ":" + cnonceStr
       
   185 	a2 := authenticate + ":" + digestUri
       
   186 	response := hex(kd(hex(h(a1)), nonce+":"+
       
   187 		nonceCountStr+":"+cnonceStr+":auth:"+
       
   188 		hex(h(a2))))
       
   189 	return response
       
   190 }