From f0db02e04b6e61653953adbe0b9adbee3f49a8ba Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 29 Jan 2024 23:19:22 -0500 Subject: [PATCH] fix #102 Implement multiline SASL PLAIN responses; support SASL EXTERNAL --- ircevent/examples/simple.go | 11 +++++++- ircevent/irc.go | 4 +-- ircevent/irc_sasl.go | 54 ++++++++++++++++++++++++++++++++++--- ircevent/irc_sasl_test.go | 26 ++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/ircevent/examples/simple.go b/ircevent/examples/simple.go index 70d9270..19feb3e 100644 --- a/ircevent/examples/simple.go +++ b/ircevent/examples/simple.go @@ -33,10 +33,19 @@ func main() { UseTLS: true, TLSConfig: &tls.Config{InsecureSkipVerify: true}, RequestCaps: []string{"server-time", "message-tags"}, - SASLLogin: saslLogin, // SASL will be enabled automatically if these are set + SASLLogin: saslLogin, // SASL PLAIN will be enabled automatically if these are set SASLPassword: saslPassword, } + if certKeyFile := os.Getenv("IRCEVENT_SASL_CLIENTCERT"); certKeyFile != "" { + clientCert, err := tls.LoadX509KeyPair(certKeyFile, certKeyFile) + if err != nil { + log.Fatalf("could not load client certificate: %v", err) + } + irc.TLSConfig.Certificates = []tls.Certificate{clientCert} + irc.SASLMech = "EXTERNAL" // overrides automatic SASL PLAIN + } + irc.AddConnectCallback(func(e ircmsg.Message) { // attempt to set the BOT mode on ourself: if botMode := irc.ISupport()["BOT"]; botMode != "" { diff --git a/ircevent/irc.go b/ircevent/irc.go index 1744a8b..36938ec 100644 --- a/ircevent/irc.go +++ b/ircevent/irc.go @@ -659,8 +659,8 @@ func (irc *Connection) Connect() (err error) { if irc.SASLMech == "" { irc.SASLMech = "PLAIN" } - if irc.SASLMech != "PLAIN" { - return errors.New("only SASL PLAIN is supported") + if !(irc.SASLMech == "PLAIN" || irc.SASLMech == "EXTERNAL") { + return fmt.Errorf("unsupported SASL mechanism %s", irc.SASLMech) } if irc.MaxLineLen == 0 { irc.MaxLineLen = 512 diff --git a/ircevent/irc_sasl.go b/ircevent/irc_sasl.go index d9903ee..e564aab 100644 --- a/ircevent/irc_sasl.go +++ b/ircevent/irc_sasl.go @@ -1,9 +1,9 @@ package ircevent import ( + "bytes" "encoding/base64" "errors" - "fmt" "github.com/ergochat/irc-go/ircmsg" ) @@ -29,10 +29,58 @@ func (irc *Connection) submitSASLResult(r saslResult) { } } +func splitSaslResponse(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 +} + +func (irc *Connection) composeSaslPlainResponse() []byte { + var buf bytes.Buffer + buf.WriteString(irc.SASLLogin) // optional authzid, included for compatibility + buf.WriteByte('\x00') + buf.WriteString(irc.SASLLogin) // authcid + buf.WriteByte('\x00') + buf.WriteString(irc.SASLPassword) // passwd + return buf.Bytes() +} + func (irc *Connection) setupSASLCallbacks() { irc.AddCallback("AUTHENTICATE", func(e ircmsg.Message) { - str := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s\x00%s\x00%s", irc.SASLLogin, irc.SASLLogin, irc.SASLPassword))) - irc.Send("AUTHENTICATE", str) + switch irc.SASLMech { + case "PLAIN": + for _, resp := range splitSaslResponse(irc.composeSaslPlainResponse()) { + irc.Send("AUTHENTICATE", resp) + } + case "EXTERNAL": + irc.Send("AUTHENTICATE", "+") + default: + // impossible, nothing to do + } }) irc.AddCallback(RPL_LOGGEDOUT, func(e ircmsg.Message) { diff --git a/ircevent/irc_sasl_test.go b/ircevent/irc_sasl_test.go index 500e33e..49a9844 100644 --- a/ircevent/irc_sasl_test.go +++ b/ircevent/irc_sasl_test.go @@ -106,3 +106,29 @@ func TestSASLFail(t *testing.T) { t.Errorf("successfully connected with invalid password") } } + +func TestSplitResponse(t *testing.T) { + assertEqual(splitSaslResponse([]byte{}), []string{"+"}) + assertEqual(splitSaslResponse( + []byte("shivaram\x00shivaram\x00shivarampassphrase")), + []string{"c2hpdmFyYW0Ac2hpdmFyYW0Ac2hpdmFyYW1wYXNzcGhyYXNl"}, + ) + + // from the examples in the spec: + assertEqual( + splitSaslResponse([]byte("\x00emersion\x00Est ut beatae omnis ipsam. Quis fugiat deleniti totam qui. Ipsum quam a dolorum tempora velit laborum odit. Et saepe voluptate sed cumque vel. Voluptas sint ab pariatur libero veritatis corrupti. Vero iure omnis ullam. Vero beatae dolores facere fugiat ipsam. Ea est pariatur minima nobis sunt aut ut. Dolores ut laudantium maiores temporibus voluptates. Reiciendis impedit omnis et unde delectus quas ab. Quae eligendi necessitatibus doloribus molestias tempora magnam assumenda.")), + []string{ + "AGVtZXJzaW9uAEVzdCB1dCBiZWF0YWUgb21uaXMgaXBzYW0uIFF1aXMgZnVnaWF0IGRlbGVuaXRpIHRvdGFtIHF1aS4gSXBzdW0gcXVhbSBhIGRvbG9ydW0gdGVtcG9yYSB2ZWxpdCBsYWJvcnVtIG9kaXQuIEV0IHNhZXBlIHZvbHVwdGF0ZSBzZWQgY3VtcXVlIHZlbC4gVm9sdXB0YXMgc2ludCBhYiBwYXJpYXR1ciBsaWJlcm8gdmVyaXRhdGlzIGNvcnJ1cHRpLiBWZXJvIGl1cmUgb21uaXMgdWxsYW0uIFZlcm8gYmVhdGFlIGRvbG9yZXMgZmFjZXJlIGZ1Z2lhdCBpcHNhbS4gRWEgZXN0IHBhcmlhdHVyIG1pbmltYSBub2JpcyBz", + "dW50IGF1dCB1dC4gRG9sb3JlcyB1dCBsYXVkYW50aXVtIG1haW9yZXMgdGVtcG9yaWJ1cyB2b2x1cHRhdGVzLiBSZWljaWVuZGlzIGltcGVkaXQgb21uaXMgZXQgdW5kZSBkZWxlY3R1cyBxdWFzIGFiLiBRdWFlIGVsaWdlbmRpIG5lY2Vzc2l0YXRpYnVzIGRvbG9yaWJ1cyBtb2xlc3RpYXMgdGVtcG9yYSBtYWduYW0gYXNzdW1lbmRhLg==", + }, + ) + + // 400 byte line must be followed by +: + assertEqual( + splitSaslResponse([]byte("slingamn\x00slingamn\x001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")), + []string{ + "c2xpbmdhbW4Ac2xpbmdhbW4AMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMQ==", + "+", + }, + ) +}