|
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 } |