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 {