Add SASL authentication support

This hacks together support for IRCv3.1 SASL. Currently only SASL PLAIN
is supported, but it's implemented in a way that adding support for
other types should not require too many changes to the current code.
This commit is contained in:
Taavi Väänänen 2022-10-30 12:01:33 +02:00 committed by Alex Bee
parent bbbcc9aa5b
commit e64b5d47c3
6 changed files with 234 additions and 23 deletions

View File

@ -10,6 +10,7 @@ const (
CONNECTED = "CONNECTED"
DISCONNECTED = "DISCONNECTED"
ACTION = "ACTION"
AUTHENTICATE = "AUTHENTICATE"
AWAY = "AWAY"
CAP = "CAP"
CTCP = "CTCP"
@ -322,3 +323,8 @@ func (conn *Conn) Cap(subcommmand string, capabilities ...string) {
}
}
}
// Authenticate send an AUTHENTICATE command to the server.
func (conn *Conn) Authenticate(message string) {
conn.Raw(AUTHENTICATE + " " + message)
}

View File

@ -13,6 +13,7 @@ import (
"sync"
"time"
"github.com/emersion/go-sasl"
"github.com/fluffle/goirc/logging"
"github.com/fluffle/goirc/state"
"golang.org/x/net/proxy"
@ -51,6 +52,9 @@ type Conn struct {
// Capabilites currently enabled
currCaps *capSet
// SASL internals
saslRemainingData []byte
// CancelFunc and WaitGroup for goroutines
die context.CancelFunc
wg sync.WaitGroup
@ -101,6 +105,9 @@ type Config struct {
// A list of capabilities to request to the server during registration.
Capabilites []string
// SASL configuration to use to authenticate the connection.
Sasl sasl.Client
// Replaceable function to customise the 433 handler's new nick.
// By default an underscore "_" is appended to the current nick.
NewNick func(string) string
@ -216,6 +223,11 @@ func Client(cfg *Config) *Conn {
}
}
if cfg.Sasl != nil && !cfg.EnableCapabilityNegotiation {
logging.Warn("Enabling capability negotiation as it's required for SASL")
cfg.EnableCapabilityNegotiation = true
}
conn := &Conn{
cfg: cfg,
dialer: dialer,
@ -226,6 +238,7 @@ func Client(cfg *Config) *Conn {
lastsent: time.Now(),
supportedCaps: capabilitySet(),
currCaps: capabilitySet(),
saslRemainingData: nil,
}
conn.addIntHandlers()
return conn
@ -248,7 +261,6 @@ func (conn *Conn) Connected() bool {
// conn.Config().Flood = true
// // Send many lines to the IRC server, risking "excess flood"
// conn.Config().Flood = false
//
func (conn *Conn) Config() *Config {
return conn.cfg
}

View File

@ -9,9 +9,13 @@ import (
"sync"
"time"
"encoding/base64"
"github.com/fluffle/goirc/logging"
)
// saslCap is the IRCv3 capability used for SASL authentication.
const saslCap = "sasl"
// sets up the internal event handlers to do essential IRC protocol things
var intHandlers = map[string]HandlerFunc{
REGISTER: (*Conn).h_REGISTER,
@ -22,6 +26,10 @@ var intHandlers = map[string]HandlerFunc{
PING: (*Conn).h_PING,
CAP: (*Conn).h_CAP,
"410": (*Conn).h_410,
AUTHENTICATE: (*Conn).h_AUTHENTICATE,
"903": (*Conn).h_903,
"904": (*Conn).h_904,
"908": (*Conn).h_908,
}
// set up the ircv3 capabilities supported by this client which will be requested by default to the server.
@ -59,6 +67,11 @@ func (conn *Conn) getRequestCapabilities() *capSet {
// add capabilites supported by the client
s.Add(defaultCaps...)
if conn.cfg.Sasl != nil {
// add the SASL cap if enabled
s.Add(saslCap)
}
// add capabilites requested by the user
s.Add(conn.cfg.Capabilites...)
@ -79,11 +92,32 @@ func (conn *Conn) negotiateCapabilities(supportedCaps []string) {
}
func (conn *Conn) handleCapAck(caps []string) {
gotSasl := false
for _, cap := range caps {
conn.currCaps.Add(cap)
if conn.cfg.Sasl != nil && cap == saslCap {
mech, ir, err := conn.cfg.Sasl.Start()
if err != nil {
logging.Warn("SASL authentication failed: %v", err)
continue
}
// TODO: when IRC 3.2 capability negotiation is supported, ensure the
// capability value is used to match the chosen mechanism
gotSasl = true
conn.saslRemainingData = ir
conn.Authenticate(mech)
}
}
if !gotSasl {
conn.Cap(CAP_END)
}
}
func (conn *Conn) handleCapNak(caps []string) {
conn.Cap(CAP_END)
@ -181,6 +215,60 @@ func (conn *Conn) h_CAP(line *Line) {
}
}
// Handler for SASL authentication
func (conn *Conn) h_AUTHENTICATE(line *Line) {
if conn.cfg.Sasl == nil {
return
}
if conn.saslRemainingData != nil {
data := "+" // plus sign representing empty data
if len(conn.saslRemainingData) > 0 {
data = base64.StdEncoding.EncodeToString(conn.saslRemainingData)
}
// TODO: batch data into chunks of 400 bytes per the spec
conn.Authenticate(data)
conn.saslRemainingData = nil
return
}
// TODO: handle data over 400 bytes long (which will be chunked into multiple messages per the spec)
challenge, err := base64.StdEncoding.DecodeString(line.Args[0])
if err != nil {
logging.Error("Failed to decode SASL challenge: %v", err)
return
}
response, err := conn.cfg.Sasl.Next(challenge)
if err != nil {
logging.Error("Failed to generate response for SASL challenge: %v", err)
return
}
// TODO: batch data into chunks of 400 bytes per the spec
data := base64.StdEncoding.EncodeToString(response)
conn.Authenticate(data)
}
// Handler for RPL_SASLSUCCESS.
func (conn *Conn) h_903(line *Line) {
conn.Cap(CAP_END)
}
// Handler for RPL_SASLFAILURE.
func (conn *Conn) h_904(line *Line) {
logging.Warn("SASL authentication failed")
conn.Cap(CAP_END)
}
// Handler for RPL_SASLMECHS.
func (conn *Conn) h_908(line *Line) {
logging.Warn("SASL mechanism not supported, supported mechanisms are: %v", line.Args[1])
conn.Cap(CAP_END)
}
// Handler to trigger a CONNECTED event on receipt of numeric 001
// :<server> 001 <nick> :Welcome message <nick>!<user>@<host>
func (conn *Conn) h_001(line *Line) {

103
client/sasl_test.go Normal file
View File

@ -0,0 +1,103 @@
package client
import (
"github.com/emersion/go-sasl"
"testing"
)
func TestSaslPlainSuccessWorkflow(t *testing.T) {
c, s := setUp(t)
defer s.tearDown()
c.Config().Sasl = sasl.NewPlainClient("", "example", "password")
c.Config().EnableCapabilityNegotiation = true
c.h_REGISTER(&Line{Cmd: REGISTER})
s.nc.Expect("CAP LS")
s.nc.Expect("NICK test")
s.nc.Expect("USER test 12 * :Testing IRC")
s.nc.Send("CAP * LS :sasl foobar")
s.nc.Expect("CAP REQ :sasl")
s.nc.Send("CAP * ACK :sasl")
s.nc.Expect("AUTHENTICATE PLAIN")
s.nc.Send("AUTHENTICATE +")
s.nc.Expect("AUTHENTICATE AGV4YW1wbGUAcGFzc3dvcmQ=")
s.nc.Send("904 test :SASL authentication successful")
s.nc.Expect("CAP END")
}
func TestSaslPlainWrongPassword(t *testing.T) {
c, s := setUp(t)
defer s.tearDown()
c.Config().Sasl = sasl.NewPlainClient("", "example", "password")
c.Config().EnableCapabilityNegotiation = true
c.h_REGISTER(&Line{Cmd: REGISTER})
s.nc.Expect("CAP LS")
s.nc.Expect("NICK test")
s.nc.Expect("USER test 12 * :Testing IRC")
s.nc.Send("CAP * LS :sasl foobar")
s.nc.Expect("CAP REQ :sasl")
s.nc.Send("CAP * ACK :sasl")
s.nc.Expect("AUTHENTICATE PLAIN")
s.nc.Send("AUTHENTICATE +")
s.nc.Expect("AUTHENTICATE AGV4YW1wbGUAcGFzc3dvcmQ=")
s.nc.Send("904 test :SASL authentication failed")
s.nc.Expect("CAP END")
}
func TestSaslExternalSuccessWorkflow(t *testing.T) {
c, s := setUp(t)
defer s.tearDown()
c.Config().Sasl = sasl.NewExternalClient("")
c.Config().EnableCapabilityNegotiation = true
c.h_REGISTER(&Line{Cmd: REGISTER})
s.nc.Expect("CAP LS")
s.nc.Expect("NICK test")
s.nc.Expect("USER test 12 * :Testing IRC")
s.nc.Send("CAP * LS :sasl foobar")
s.nc.Expect("CAP REQ :sasl")
s.nc.Send("CAP * ACK :sasl")
s.nc.Expect("AUTHENTICATE EXTERNAL")
s.nc.Send("AUTHENTICATE +")
s.nc.Expect("AUTHENTICATE +")
s.nc.Send("904 test :SASL authentication successful")
s.nc.Expect("CAP END")
}
func TestSaslNoSaslCap(t *testing.T) {
c, s := setUp(t)
defer s.tearDown()
c.Config().Sasl = sasl.NewPlainClient("", "example", "password")
c.Config().EnableCapabilityNegotiation = true
c.h_REGISTER(&Line{Cmd: REGISTER})
s.nc.Expect("CAP LS")
s.nc.Expect("NICK test")
s.nc.Expect("USER test 12 * :Testing IRC")
s.nc.Send("CAP * LS :foobar")
s.nc.Expect("CAP END")
}
func TestSaslUnsupportedMechanism(t *testing.T) {
c, s := setUp(t)
defer s.tearDown()
c.Config().Sasl = sasl.NewPlainClient("", "example", "password")
c.Config().EnableCapabilityNegotiation = true
c.h_REGISTER(&Line{Cmd: REGISTER})
s.nc.Expect("CAP LS")
s.nc.Expect("NICK test")
s.nc.Expect("USER test 12 * :Testing IRC")
s.nc.Send("CAP * LS :sasl foobar")
s.nc.Expect("CAP REQ :sasl")
s.nc.Send("CAP * ACK :sasl")
s.nc.Expect("AUTHENTICATE PLAIN")
s.nc.Send("908 test external :are available SASL mechanisms")
s.nc.Expect("CAP END")
}

1
go.mod
View File

@ -1,6 +1,7 @@
module github.com/fluffle/goirc
require (
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
github.com/golang/mock v1.5.0
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
)

3
go.sum
View File

@ -1,9 +1,10 @@
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=