use SASL utilities from irc-go

This commit is contained in:
Shivaram Lingamneni 2024-02-06 20:50:48 -05:00
parent 43b9c9a4bd
commit 9c71a96809
6 changed files with 143 additions and 59 deletions

2
go.mod
View File

@ -8,7 +8,7 @@ require (
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/ergochat/confusables v0.0.0-20201108231250-4ab98ab61fb1
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
github.com/ergochat/irc-go v0.4.0
github.com/ergochat/irc-go v0.5.0-rc1
github.com/go-sql-driver/mysql v1.7.0
github.com/go-test/deep v1.0.6 // indirect
github.com/gofrs/flock v0.8.1

2
go.sum
View File

@ -12,6 +12,8 @@ github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881 h1:+J5m88nvybxB5
github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881/go.mod h1:ASYJtQujNitna6cVHsNQTGrfWvMPJ5Sa2lZlmsH65uM=
github.com/ergochat/irc-go v0.4.0 h1:0YibCKfAAtwxQdNjLQd9xpIEPisLcJ45f8FNsMHAuZc=
github.com/ergochat/irc-go v0.4.0/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
github.com/ergochat/irc-go v0.5.0-rc1 h1:kFoIHExoNFQ2CV+iShAVna/H4xrXQB4t4jK5Sep2j9k=
github.com/ergochat/irc-go v0.5.0-rc1/go.mod h1:2vi7KNpIPWnReB5hmLpl92eMywQvuIeIIGdt/FQCph0=
github.com/ergochat/scram v1.0.2-ergo1 h1:2bYXiRFQH636pT0msOG39fmEYl4Eq+OuutcyDsCix/g=
github.com/ergochat/scram v1.0.2-ergo1/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/ergochat/websocket v1.4.2-oragono1 h1:plMUunFBM6UoSCIYCKKclTdy/TkkHfUslhOfJQzfueM=

View File

@ -21,6 +21,7 @@ import (
"github.com/ergochat/irc-go/ircfmt"
"github.com/ergochat/irc-go/ircmsg"
"github.com/ergochat/irc-go/ircreader"
"github.com/ergochat/irc-go/ircutils"
"github.com/xdg-go/scram"
"github.com/ergochat/ergo/irc/caps"
@ -120,13 +121,20 @@ type Client struct {
type saslStatus struct {
mechanism string
value strings.Builder
value ircutils.SASLBuffer
scramConv *scram.ServerConversation
oauthConv *oauth2.OAuthBearerServer
}
func (s *saslStatus) Initialize() {
s.value.Initialize(saslMaxResponseLength)
}
func (s *saslStatus) Clear() {
*s = saslStatus{}
s.mechanism = ""
s.value.Clear()
s.scramConv = nil
s.oauthConv = nil
}
// what stage the client is at w.r.t. the PASS command:
@ -364,6 +372,7 @@ func (server *Server) RunClient(conn IRCConn) {
isTor: wConn.Tor,
hideSTS: wConn.Tor || wConn.HideSTS,
}
session.sasl.Initialize()
client.sessions = []*Session{session}
session.resetFakelag()

View File

@ -8,7 +8,6 @@ package irc
import (
"bytes"
"encoding/base64"
"fmt"
"net"
"os"
@ -180,7 +179,6 @@ func acceptHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
}
const (
saslMaxArgLength = 400 // required by SASL spec
saslMaxResponseLength = 8192 // implementation-defined sanity check, long enough for bearer tokens
)
@ -207,7 +205,7 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
return false
}
// start new sasl session
// start new sasl session: parameter is the authentication mechanism
if session.sasl.mechanism == "" {
throttled, remainingTime := client.loginThrottle.Touch()
if throttled {
@ -246,44 +244,28 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
return false
}
// continue existing sasl session
rawData := msg.Params[0]
// https://ircv3.net/specs/extensions/sasl-3.1:
// "The response is encoded in Base64 (RFC 4648), then split to 400-byte chunks,
// and each chunk is sent as a separate AUTHENTICATE command."
if len(rawData) > saslMaxArgLength {
// continue existing sasl session: parameter is a message chunk
done, value, err := session.sasl.value.Add(msg.Params[0])
if err == nil {
if done {
// call actual handler
handler := EnabledSaslMechanisms[session.sasl.mechanism]
return handler(server, client, session, value, rb)
} else {
return false // wait for continuation line
}
}
// else: error handling
switch err {
case ircutils.ErrSASLTooLong:
rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long"))
session.sasl.Clear()
return false
} else if len(rawData) == saslMaxArgLength {
if session.sasl.value.Len() >= saslMaxResponseLength {
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
session.sasl.Clear()
return false
}
session.sasl.value.WriteString(rawData)
return false
case ircutils.ErrSASLLimitExceeded:
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
default:
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
}
if rawData != "+" {
session.sasl.value.WriteString(rawData)
}
var data []byte
var err error
if session.sasl.value.Len() > 0 {
data, err = base64.StdEncoding.DecodeString(session.sasl.value.String())
session.sasl.value.Reset()
if err != nil {
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
session.sasl.Clear()
return false
}
}
// call actual handler
handler := EnabledSaslMechanisms[session.sasl.mechanism]
return handler(server, client, session, data, rb)
session.sasl.Clear()
return false
}
// AUTHENTICATE PLAIN
@ -495,21 +477,9 @@ func authOauthBearerHandler(server *Server, client *Client, session *Session, va
// helper to b64 a sasl response and chunk it into 400-byte lines
// as per https://ircv3.net/specs/extensions/sasl-3.1
// TODO replace this with ircutils.EncodeSASLResponse
func sendSASLChallenge(server *Server, rb *ResponseBuffer, challenge []byte) {
challengeStr := base64.StdEncoding.EncodeToString(challenge)
lastLen := 0
for len(challengeStr) > 0 {
end := saslMaxArgLength
if end > len(challengeStr) {
end = len(challengeStr)
}
lastLen = end
rb.Add(nil, server.name, "AUTHENTICATE", challengeStr[:end])
challengeStr = challengeStr[end:]
}
if lastLen == saslMaxArgLength {
rb.Add(nil, server.name, "AUTHENTICATE", "+")
for _, chunk := range ircutils.EncodeSASLResponse(challenge) {
rb.Add(nil, server.name, "AUTHENTICATE", chunk)
}
}

105
vendor/github.com/ergochat/irc-go/ircutils/sasl.go generated vendored Normal file
View File

@ -0,0 +1,105 @@
package ircutils
import (
"encoding/base64"
"errors"
"strings"
)
var (
ErrSASLLimitExceeded = errors.New("SASL total response size exceeded configured limit")
ErrSASLTooLong = errors.New("SASL response chunk exceeded 400-byte limit")
)
// EncodeSASLResponse encodes a raw SASL response as parameters to successive
// AUTHENTICATE commands, as described in the IRCv3 SASL specification.
func EncodeSASLResponse(raw []byte) (result []string) {
// https://ircv3.net/specs/extensions/sasl-3.1#the-authenticate-command
// "The response is encoded in Base64 (RFC 4648), then split to 400-byte chunks,
// and each chunk is sent as a separate AUTHENTICATE command. Empty (zero-length)
// responses are sent as AUTHENTICATE +. If the last chunk was exactly 400 bytes
// long, it must also be followed by AUTHENTICATE + to signal end of response."
if len(raw) == 0 {
return []string{"+"}
}
response := base64.StdEncoding.EncodeToString(raw)
lastLen := 0
for len(response) > 0 {
// TODO once we require go 1.21, this can be: lastLen = min(len(response), 400)
lastLen = len(response)
if lastLen > 400 {
lastLen = 400
}
result = append(result, response[:lastLen])
response = response[lastLen:]
}
if lastLen == 400 {
result = append(result, "+")
}
return result
}
// SASLBuffer handles buffering and decoding SASL responses sent as parameters
// to AUTHENTICATE commands, as described in the IRCv3 SASL specification.
// Do not copy a SASLBuffer after first use.
type SASLBuffer struct {
maxLength int
buffer strings.Builder
}
// NewSASLBuffer returns a new SASLBuffer. maxLength is the maximum amount of
// base64'ed data to buffer (0 for no limit).
func NewSASLBuffer(maxLength int) *SASLBuffer {
result := new(SASLBuffer)
result.Initialize(maxLength)
return result
}
// Initialize initializes a SASLBuffer in place.
func (b *SASLBuffer) Initialize(maxLength int) {
b.maxLength = maxLength
}
// Add processes an additional SASL response chunk sent via AUTHENTICATE.
// If the response is complete, it resets the buffer and returns the decoded
// response along with any decoding or protocol errors detected.
func (b *SASLBuffer) Add(value string) (done bool, output []byte, err error) {
if value == "+" {
output, err = b.getAndReset()
return true, output, err
}
if len(value) > 400 {
b.buffer.Reset()
return true, nil, ErrSASLTooLong
}
if b.maxLength != 0 && (b.buffer.Len()+len(value)) > b.maxLength {
b.buffer.Reset()
return true, nil, ErrSASLLimitExceeded
}
b.buffer.WriteString(value)
if len(value) < 400 {
output, err = b.getAndReset()
return true, output, err
} else {
// 400 bytes, wait for continuation line or +
return false, nil, nil
}
}
// Clear resets the buffer state.
func (b *SASLBuffer) Clear() {
b.buffer.Reset()
}
func (b *SASLBuffer) getAndReset() (output []byte, err error) {
output, err = base64.StdEncoding.DecodeString(b.buffer.String())
b.buffer.Reset()
return
}

4
vendor/modules.txt vendored
View File

@ -16,7 +16,7 @@ github.com/ergochat/confusables
# github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
## explicit; go 1.18
github.com/ergochat/go-ident
# github.com/ergochat/irc-go v0.4.0
# github.com/ergochat/irc-go v0.5.0-rc1
## explicit; go 1.15
github.com/ergochat/irc-go/ircfmt
github.com/ergochat/irc-go/ircmsg
@ -30,8 +30,6 @@ github.com/go-sql-driver/mysql
# github.com/gofrs/flock v0.8.1
## explicit
github.com/gofrs/flock
# github.com/golang-jwt/jwt v3.2.2+incompatible
## explicit
# github.com/golang-jwt/jwt/v5 v5.2.0
## explicit; go 1.18
github.com/golang-jwt/jwt/v5