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