diff --git a/xmpp.go b/xmpp.go
index c326130..8b66ad4 100644
--- a/xmpp.go
+++ b/xmpp.go
@@ -52,6 +52,7 @@ const (
nsSASL2 = "urn:xmpp:sasl:2"
nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
nsBind2 = "urn:xmpp:bind:0"
+ nsFast = "urn:xmpp:fast:0"
nsSASLCB = "urn:xmpp:sasl-cb:0"
nsClient = "jabber:client"
nsSession = "urn:ietf:params:xml:ns:xmpp-session"
@@ -75,6 +76,13 @@ func getCookie() Cookie {
return Cookie(binary.LittleEndian.Uint64(buf[:]))
}
+// Fast holds the XEP-0484 fast token, mechanism and expiry date
+type Fast struct {
+ Token string
+ Mechanism string
+ Expiry time.Time
+}
+
// Client holds XMPP connection options
type Client struct {
conn net.Conn // connection to server
@@ -87,6 +95,7 @@ type Client struct {
LimitMaxBytes int // Maximum stanza size (XEP-0478: Stream Limits Advertisement)
LimitIdleSeconds int // Maximum idle seconds (XEP-0478: Stream Limits Advertisement)
Mechanism string
+ Fast Fast // XEP-0484 FAST Token, mechanism and expiry.
}
func (c *Client) JID() string {
@@ -240,7 +249,7 @@ type Options struct {
// XEP-0474: SASL SCRAM Downgrade Protection
SSDP bool
- // XEP-0388: XEP-0388: Extensible SASL Profile
+ // XEP-0388: Extensible SASL Profile
// Value for software
UserAgentSW string
@@ -248,10 +257,18 @@ type Options struct {
// Value for device
UserAgentDev string
- // XEP-0388: XEP-0388: Extensible SASL Profile
+ // XEP-0388: Extensible SASL Profile
// Unique stable identifier for the client installation
// MUST be a valid UUIDv4
UserAgentID string
+
+ // XEP-0484: Fast Authentication Streamlining Tokens
+ // Fast Token
+ FastToken string
+
+ // XEP-0484: Fast Authentication Streamlining Tokens
+ // Fast Mechanism
+ FastMechanism string
}
// NewClient establishes a new Client connection based on a set of Options.
@@ -431,7 +448,7 @@ func (c *Client) init(o *Options) error {
return err
}
var mechanism, channelBinding, clientFirstMessage, clientFinalMessageBare, authMessage string
- var bind2Data, resource, userAgentSW, userAgentDev, userAgentID string
+ var bind2Data, resource, userAgentSW, userAgentDev, userAgentID, fastAuth string
var serverSignature, keyingMaterial []byte
var scramPlus, ok, tlsConnOK, tls13, serverEndPoint, sasl2, bind2 bool
var cbsSlice, mechSlice []string
@@ -604,9 +621,68 @@ func (c *Client) init(o *Options) error {
if o.UserAgentID != "" {
userAgentID = fmt.Sprintf(" id='%s'", o.UserAgentID)
}
+ if f.Authentication.Inline.Fast.Mechanism != nil && o.UserAgentID != "" {
+ var mech string
+ if o.FastToken == "" {
+ m := f.Authentication.Inline.Fast.Mechanism
+ switch {
+ case slices.Contains(m, "HT-SHA-256-EXPR") && tls13:
+ mech = "HT-SHA-256-EXPR"
+ case slices.Contains(m, "HT-SHA-256-UNIQ") && !tls13:
+ mech = "HT-SHA-256-UNIQ"
+ case slices.Contains(m, "HT-SHA-256-ENDP"):
+ mech = "HT-SHA-256-ENDP"
+ case slices.Contains(m, "HT-SHA-256-NONE"):
+ mech = "HT-SHA-256-NONE"
+ default:
+ return fmt.Errorf("fast: unsupported auth mechanism %s", m)
+ }
+ fastAuth = fmt.Sprintf("", nsFast, mech)
+ } else {
+ fastAuth = fmt.Sprintf("", nsFast)
+ tlsState := tlsConn.ConnectionState()
+ mechanism = o.FastMechanism
+ switch mechanism {
+ case "HT-SHA-256-EXPR":
+ keyingMaterial, err = tlsState.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32)
+ if err != nil {
+ return err
+ }
+ case "HT-SHA-256-UNIQ":
+ keyingMaterial = tlsState.TLSUnique
+ case "HT-SHA-256-ENDP":
+ var h hash.Hash
+ switch tlsState.PeerCertificates[0].SignatureAlgorithm {
+ case x509.SHA1WithRSA, x509.SHA256WithRSA, x509.ECDSAWithSHA1,
+ x509.ECDSAWithSHA256, x509.SHA256WithRSAPSS:
+ h = sha256.New()
+ case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS:
+ h = sha512.New384()
+ case x509.SHA512WithRSA, x509.ECDSAWithSHA512, x509.SHA512WithRSAPSS:
+ h = sha512.New()
+ }
+ h.Write(tlsState.PeerCertificates[0].Raw)
+ keyingMaterial = h.Sum(nil)
+ h.Reset()
+ case "HT-SHA-256-NONE":
+ keyingMaterial = []byte("")
+ default:
+ return fmt.Errorf("fast: unsupported auth mechanism %s", mechanism)
+ }
+ h := hmac.New(sha256.New, []byte(o.FastToken))
+ initiator := append([]byte("Initiator")[:], keyingMaterial[:]...)
+ _, err = h.Write(initiator)
+ if err != nil {
+ return err
+ }
+ initiatorHashedToken := h.Sum(nil)
+ user := strings.Split(o.User, "@")[0]
+ clientFirstMessage = user + "\x00" + string(initiatorHashedToken)
+ }
+ }
fmt.Fprintf(c.stanzaWriter,
- "%s%s%s%s\n",
- nsSASL2, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)), userAgentID, userAgentSW, userAgentDev, bind2Data)
+ "%s%s%s%s%s\n",
+ nsSASL2, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)), userAgentID, userAgentSW, userAgentDev, bind2Data, fastAuth)
} else {
fmt.Fprintf(c.stanzaWriter, "%s\n",
nsSASL, mechanism, base64.StdEncoding.EncodeToString([]byte(clientFirstMessage)))
@@ -633,6 +709,55 @@ func (c *Client) init(o *Options) error {
errorMessage = v.Any.Local
}
return errors.New("auth failure: " + errorMessage)
+ case *sasl2Success:
+ if strings.HasPrefix(mechanism, "SCRAM-SHA") {
+ successMsg, err := base64.StdEncoding.DecodeString(v.AdditionalData)
+ if err != nil {
+ return err
+ }
+ if !strings.HasPrefix(string(successMsg), "v=") {
+ return errors.New("server sent unexpected content in SCRAM success message")
+ }
+ c.Mechanism = mechanism
+ }
+ if strings.HasPrefix(mechanism, "HT-SHA") {
+ // TODO: Check whether server implementations already support
+ // https://www.ietf.org/archive/id/draft-schmaus-kitten-sasl-ht-09.html#section-3.3
+ h := hmac.New(sha256.New, []byte(o.FastToken))
+ responder := append([]byte("Responder")[:], keyingMaterial[:]...)
+ _, err = h.Write(responder)
+ if err != nil {
+ return err
+ }
+ responderMsgRcv, err := base64.StdEncoding.DecodeString(v.AdditionalData)
+ if err != nil {
+ return err
+ }
+ responderMsgCalc := h.Sum(nil)
+ if string(responderMsgCalc) != string(responderMsgRcv) {
+ return fmt.Errorf("server sent unexpected content in FAST success message")
+ }
+ c.Mechanism = mechanism
+ }
+ if bind2 {
+ c.jid = v.AuthorizationIdentifier
+ }
+ if v.Token.Token != "" {
+ m := f.Authentication.Inline.Fast.Mechanism
+ switch {
+ case slices.Contains(m, "HT-SHA-256-EXPR") && tls13:
+ c.Fast.Mechanism = "HT-SHA-256-EXPR"
+ case slices.Contains(m, "HT-SHA-256-UNIQ") && !tls13:
+ c.Fast.Mechanism = "HT-SHA-256-UNIQ"
+ case slices.Contains(m, "HT-SHA-256-ENDP"):
+ c.Fast.Mechanism = "HT-SHA-256-ENDP"
+ case slices.Contains(m, "HT-SHA-256-NONE"):
+ c.Fast.Mechanism = "HT-SHA-256-NONE"
+ }
+ c.Fast.Token = v.Token.Token
+ c.Fast.Expiry, _ = time.Parse(time.RFC3339, v.Token.Expiry)
+ }
+ return nil
case *sasl2Challenge:
sfm = v.Text
case *saslChallenge:
@@ -829,6 +954,23 @@ func (c *Client) init(o *Options) error {
if bind2 {
c.jid = v.AuthorizationIdentifier
}
+ if v.Token.Token != "" {
+ if v.Token.Token != "" {
+ m := f.Authentication.Inline.Fast.Mechanism
+ switch {
+ case slices.Contains(m, "HT-SHA-256-EXPR") && tls13:
+ c.Fast.Mechanism = "HT-SHA-256-EXPR"
+ case slices.Contains(m, "HT-SHA-256-UNIQ") && !tls13:
+ c.Fast.Mechanism = "HT-SHA-256-UNIQ"
+ case slices.Contains(m, "HT-SHA-256-ENDP"):
+ c.Fast.Mechanism = "HT-SHA-256-ENDP"
+ case slices.Contains(m, "HT-SHA-256-NONE"):
+ c.Fast.Mechanism = "HT-SHA-256-NONE"
+ }
+ c.Fast.Token = v.Token.Token
+ c.Fast.Expiry, _ = time.Parse(time.RFC3339, v.Token.Expiry)
+ }
+ }
case *saslSuccess:
if strings.HasPrefix(mechanism, "SCRAM-SHA") {
successMsg, err := base64.StdEncoding.DecodeString(v.Text)
@@ -1488,9 +1630,16 @@ type sasl2Authentication struct {
Inline struct {
Text string `xml:",chardata"`
Bind struct {
- Text string `xml:",chardata"`
- Xmlns string `xml:"xmlns,attr"`
+ XMLName xml.Name `xml:"urn:xmpp:bind:0 bind"`
+ Xmlns string `xml:"xmlns,attr"`
+ Text string `xml:",chardata"`
} `xml:"bind"`
+ Fast struct {
+ XMLName xml.Name `xml:"urn:xmpp:fast:0 fast"`
+ Text string `xml:",chardata"`
+ Tls0rtt string `xml:"tls-0rtt,attr"`
+ Mechanism []string `xml:"mechanism"`
+ } `xml:"fast"`
} `xml:"inline"`
}
@@ -1521,8 +1670,14 @@ type sasl2Success struct {
AuthorizationIdentifier string `xml:"authorization-identifier"`
Bound struct {
Text string `xml:",chardata"`
- Xmlns string `xml:"xmlns,attr"`
+ Xmlns string `xml:"urn:xmpp:bind:0,attr"`
} `xml:"bound"`
+ Token struct {
+ Text string `xml:",chardata"`
+ Xmlns string `xml:"urn:xmpp:fast:0,attr"`
+ Expiry string `xml:"expiry,attr"`
+ Token string `xml:"token,attr"`
+ } `xml:"token"`
}
type saslSuccess struct {