1// Copyright 2013 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Package xmpp implements the XMPP IM protocol, as specified in RFC 6120 and 6// 6121. 7package xmpp 8 9import ( 10 "encoding/hex" 11 "errors" 12 "fmt" 13 "io" 14 "log" 15 16 "github.com/coyim/coyim/xmpp/data" 17 xe "github.com/coyim/coyim/xmpp/errors" 18 "github.com/coyim/coyim/xmpp/interfaces" 19 20 "github.com/coyim/coyim/sasl" 21 "github.com/coyim/coyim/sasl/digestmd5" 22 "github.com/coyim/coyim/sasl/plain" 23 "github.com/coyim/coyim/sasl/scram" 24) 25 26var ( 27 errUnsupportedSASLMechanism = errors.New("xmpp: server does not support any of the preferred SASL mechanism") 28) 29 30func init() { 31 plain.Register() 32 digestmd5.Register() 33 scram.Register() 34} 35 36// SASL negotiation. RFC 6120, section 6 37func (d *dialer) negotiateSASL(c interfaces.Conn) error { 38 user := d.getJIDLocalpart() 39 password := d.password 40 41 if err := c.Authenticate(user, password); err != nil { 42 return c.AuthenticationFailure() 43 } 44 45 // RFC 6120, section 6.3.2. Restart the stream 46 err := c.SendInitialStreamHeader() 47 if err != nil { 48 return err 49 } 50 51 return c.BindResource() 52} 53 54func (c *conn) AuthenticationFailure() error { 55 if c.isGoogle() { 56 return xe.ErrGoogleAuthenticationFailed 57 } 58 return xe.ErrAuthenticationFailed 59} 60 61func (c *conn) Authenticate(user, password string) error { 62 // TODO: Google accounts with 2-step auth MUST use app-specific passwords: 63 // https://security.google.com/settings/security/apppasswords 64 // An alternative would be implementing the Google authentication mechanisms 65 // - X-OAUTH2: requires app registration on Google, and a server to receive the oauth callback 66 // https://developers.google.com/talk/jep_extensions/oauth?hl=en 67 // - X-GOOGLE-TOKEN: seems to be this https://developers.google.com/identity/protocols/AuthForInstalledApps 68 69 return c.authenticateWithPreferedMethod(user, password) 70} 71 72func (c *conn) isGoogle() bool { 73 for _, m := range c.features.Mechanisms.Mechanism { 74 if "X-GOOGLE-TOKEN" == m { 75 return true 76 } 77 } 78 return false 79} 80 81func (c *conn) authenticateWithPreferedMethod(user, password string) error { 82 //TODO: this should be configurable by the client 83 preferedMechanisms := []string{"SCRAM-SHA-1", "DIGEST-MD5", "PLAIN"} 84 85 log.Println("sasl: server supports mechanisms", c.features.Mechanisms.Mechanism) 86 87 for _, prefered := range preferedMechanisms { 88 if !sasl.ClientSupport(prefered) { 89 continue 90 } 91 92 for _, m := range c.features.Mechanisms.Mechanism { 93 if prefered == m { 94 log.Println("sasl: authenticating via", m) 95 return c.authenticateWith(prefered, user, password) 96 } 97 } 98 } 99 100 return errUnsupportedSASLMechanism 101} 102 103func clientNonce(r io.Reader) (string, error) { 104 //TODO: what is the appropriate size for this? 105 //TODO: what is the appropriate way to generate a cnonce? 106 conceRand := make([]byte, 7) 107 _, err := io.ReadFull(r, conceRand) 108 if err != nil { 109 return "", err 110 } 111 112 return hex.EncodeToString(conceRand), nil 113} 114 115func (c *conn) authenticateWith(mechanism string, user string, password string) error { 116 clientAuth, err := sasl.NewClient(mechanism) 117 if err != nil { 118 return err 119 } 120 121 clientNonce, err := clientNonce(c.Rand()) 122 if err != nil { 123 return err 124 } 125 126 clientAuth.SetProperty(sasl.AuthID, user) 127 clientAuth.SetProperty(sasl.Password, password) 128 129 clientAuth.SetProperty(sasl.Service, "xmpp") 130 clientAuth.SetProperty(sasl.QOP, "auth") 131 132 //TODO: this should come from username if it were a full JID 133 //clientAuth.SetProperty(sasl.Realm, "") 134 135 clientAuth.SetProperty(sasl.ClientNonce, clientNonce) 136 137 t, err := clientAuth.Step(nil) 138 if err != nil { 139 return err 140 } 141 142 fmt.Fprintf(c.rawOut, "<auth xmlns='%s' mechanism='%s'>%s</auth>\n", NsSASL, mechanism, t.Encode()) 143 144 return c.challengeLoop(clientAuth) 145} 146 147func (c *conn) challengeLoop(clientAuth sasl.Session) error { 148 for { 149 t, success, err := c.receiveChallenge() 150 if err != nil { 151 return err 152 } 153 154 t, err = clientAuth.Step(t) 155 if err != nil { 156 return err 157 } 158 159 if success { 160 return nil 161 } 162 163 if !clientAuth.NeedsMore() { 164 break 165 } 166 167 fmt.Fprintf(c.rawOut, "<response xmlns='%s'>%s</response>\n", NsSASL, t.Encode()) 168 } 169 170 return xe.ErrAuthenticationFailed 171} 172 173func (c *conn) receiveChallenge() (t sasl.Token, success bool, err error) { 174 var encodedChallenge []byte 175 176 name, val, _ := next(c) 177 switch v := val.(type) { 178 case *data.SaslFailure: 179 err = errors.New("xmpp: authentication failure: " + v.String()) 180 return 181 case *data.SaslSuccess: 182 encodedChallenge = v.Content 183 success = true 184 case *string: 185 if name.Local != "challenge" || name.Space != NsSASL { 186 err = errors.New("xmpp: unexpected <" + name.Local + "> in " + name.Space) 187 return 188 } 189 190 encodedChallenge = []byte(*v) 191 } 192 193 t, err = sasl.DecodeToken(encodedChallenge) 194 return 195} 196 197// Resource binding. RFC 6120, section 7 198func (c *conn) BindResource() error { 199 // This is mandatory, so a missing features.Bind is a protocol failure 200 fmt.Fprintf(c.out, "<iq type='set' id='bind_1'><bind xmlns='%s'/></iq>", NsBind) 201 var iq data.ClientIQ 202 if err := c.in.DecodeElement(&iq, nil); err != nil { 203 return errors.New("unmarshal <iq>: " + err.Error()) 204 } 205 206 c.jid = iq.Bind.Jid // our local id 207 208 return c.establishSession() 209} 210 211// See RFC 3921, section 3. 212func (c *conn) establishSession() error { 213 if c.features.Session == nil { 214 return nil 215 } 216 217 // The server needs a session to be established. 218 fmt.Fprintf(c.out, "<iq to='%s' type='set' id='sess_1'><session xmlns='%s'/></iq>", c.originDomain, NsSession) 219 var iq data.ClientIQ 220 if err := c.in.DecodeElement(&iq, nil); err != nil { 221 return errors.New("xmpp: unmarshal <iq>: " + err.Error()) 222 } 223 224 if iq.Type != "result" { 225 return errors.New("xmpp: session establishment failed") 226 } 227 228 return nil 229} 230