Browse Source

use SASL utilities from irc-go

pull/2122/head
Shivaram Lingamneni 3 months ago
parent
commit
9c71a96809
6 changed files with 142 additions and 58 deletions
  1. 1
    1
      go.mod
  2. 2
    0
      go.sum
  3. 11
    2
      irc/client.go
  4. 22
    52
      irc/handlers.go
  5. 105
    0
      vendor/github.com/ergochat/irc-go/ircutils/sasl.go
  6. 1
    3
      vendor/modules.txt

+ 1
- 1
go.mod View File

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

+ 2
- 0
go.sum View File

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

+ 11
- 2
irc/client.go View File

@@ -21,6 +21,7 @@ import (
21 21
 	"github.com/ergochat/irc-go/ircfmt"
22 22
 	"github.com/ergochat/irc-go/ircmsg"
23 23
 	"github.com/ergochat/irc-go/ircreader"
24
+	"github.com/ergochat/irc-go/ircutils"
24 25
 	"github.com/xdg-go/scram"
25 26
 
26 27
 	"github.com/ergochat/ergo/irc/caps"
@@ -120,13 +121,20 @@ type Client struct {
120 121
 
121 122
 type saslStatus struct {
122 123
 	mechanism string
123
-	value     strings.Builder
124
+	value     ircutils.SASLBuffer
124 125
 	scramConv *scram.ServerConversation
125 126
 	oauthConv *oauth2.OAuthBearerServer
126 127
 }
127 128
 
129
+func (s *saslStatus) Initialize() {
130
+	s.value.Initialize(saslMaxResponseLength)
131
+}
132
+
128 133
 func (s *saslStatus) Clear() {
129
-	*s = saslStatus{}
134
+	s.mechanism = ""
135
+	s.value.Clear()
136
+	s.scramConv = nil
137
+	s.oauthConv = nil
130 138
 }
131 139
 
132 140
 // what stage the client is at w.r.t. the PASS command:
@@ -364,6 +372,7 @@ func (server *Server) RunClient(conn IRCConn) {
364 372
 		isTor:      wConn.Tor,
365 373
 		hideSTS:    wConn.Tor || wConn.HideSTS,
366 374
 	}
375
+	session.sasl.Initialize()
367 376
 	client.sessions = []*Session{session}
368 377
 
369 378
 	session.resetFakelag()

+ 22
- 52
irc/handlers.go View File

@@ -8,7 +8,6 @@ package irc
8 8
 
9 9
 import (
10 10
 	"bytes"
11
-	"encoding/base64"
12 11
 	"fmt"
13 12
 	"net"
14 13
 	"os"
@@ -180,7 +179,6 @@ func acceptHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respo
180 179
 }
181 180
 
182 181
 const (
183
-	saslMaxArgLength      = 400  // required by SASL spec
184 182
 	saslMaxResponseLength = 8192 // implementation-defined sanity check, long enough for bearer tokens
185 183
 )
186 184
 
@@ -207,7 +205,7 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
207 205
 		return false
208 206
 	}
209 207
 
210
-	// start new sasl session
208
+	// start new sasl session: parameter is the authentication mechanism
211 209
 	if session.sasl.mechanism == "" {
212 210
 		throttled, remainingTime := client.loginThrottle.Touch()
213 211
 		if throttled {
@@ -246,44 +244,28 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
246 244
 		return false
247 245
 	}
248 246
 
249
-	// continue existing sasl session
250
-	rawData := msg.Params[0]
251
-
252
-	// https://ircv3.net/specs/extensions/sasl-3.1:
253
-	// "The response is encoded in Base64 (RFC 4648), then split to 400-byte chunks,
254
-	// and each chunk is sent as a separate AUTHENTICATE command."
255
-	if len(rawData) > saslMaxArgLength {
256
-		rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long"))
257
-		session.sasl.Clear()
258
-		return false
259
-	} else if len(rawData) == saslMaxArgLength {
260
-		if session.sasl.value.Len() >= saslMaxResponseLength {
261
-			rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
262
-			session.sasl.Clear()
263
-			return false
247
+	// continue existing sasl session: parameter is a message chunk
248
+	done, value, err := session.sasl.value.Add(msg.Params[0])
249
+	if err == nil {
250
+		if done {
251
+			// call actual handler
252
+			handler := EnabledSaslMechanisms[session.sasl.mechanism]
253
+			return handler(server, client, session, value, rb)
254
+		} else {
255
+			return false // wait for continuation line
264 256
 		}
265
-		session.sasl.value.WriteString(rawData)
266
-		return false
267 257
 	}
268
-	if rawData != "+" {
269
-		session.sasl.value.WriteString(rawData)
270
-	}
271
-
272
-	var data []byte
273
-	var err error
274
-	if session.sasl.value.Len() > 0 {
275
-		data, err = base64.StdEncoding.DecodeString(session.sasl.value.String())
276
-		session.sasl.value.Reset()
277
-		if err != nil {
278
-			rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
279
-			session.sasl.Clear()
280
-			return false
281
-		}
258
+	// else: error handling
259
+	switch err {
260
+	case ircutils.ErrSASLTooLong:
261
+		rb.Add(nil, server.name, ERR_SASLTOOLONG, details.nick, client.t("SASL message too long"))
262
+	case ircutils.ErrSASLLimitExceeded:
263
+		rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Passphrase too long"))
264
+	default:
265
+		rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
282 266
 	}
283
-
284
-	// call actual handler
285
-	handler := EnabledSaslMechanisms[session.sasl.mechanism]
286
-	return handler(server, client, session, data, rb)
267
+	session.sasl.Clear()
268
+	return false
287 269
 }
288 270
 
289 271
 // AUTHENTICATE PLAIN
@@ -495,21 +477,9 @@ func authOauthBearerHandler(server *Server, client *Client, session *Session, va
495 477
 
496 478
 // helper to b64 a sasl response and chunk it into 400-byte lines
497 479
 // as per https://ircv3.net/specs/extensions/sasl-3.1
498
-// TODO replace this with ircutils.EncodeSASLResponse
499 480
 func sendSASLChallenge(server *Server, rb *ResponseBuffer, challenge []byte) {
500
-	challengeStr := base64.StdEncoding.EncodeToString(challenge)
501
-	lastLen := 0
502
-	for len(challengeStr) > 0 {
503
-		end := saslMaxArgLength
504
-		if end > len(challengeStr) {
505
-			end = len(challengeStr)
506
-		}
507
-		lastLen = end
508
-		rb.Add(nil, server.name, "AUTHENTICATE", challengeStr[:end])
509
-		challengeStr = challengeStr[end:]
510
-	}
511
-	if lastLen == saslMaxArgLength {
512
-		rb.Add(nil, server.name, "AUTHENTICATE", "+")
481
+	for _, chunk := range ircutils.EncodeSASLResponse(challenge) {
482
+		rb.Add(nil, server.name, "AUTHENTICATE", chunk)
513 483
 	}
514 484
 }
515 485
 

+ 105
- 0
vendor/github.com/ergochat/irc-go/ircutils/sasl.go View File

@@ -0,0 +1,105 @@
1
+package ircutils
2
+
3
+import (
4
+	"encoding/base64"
5
+	"errors"
6
+	"strings"
7
+)
8
+
9
+var (
10
+	ErrSASLLimitExceeded = errors.New("SASL total response size exceeded configured limit")
11
+	ErrSASLTooLong       = errors.New("SASL response chunk exceeded 400-byte limit")
12
+)
13
+
14
+// EncodeSASLResponse encodes a raw SASL response as parameters to successive
15
+// AUTHENTICATE commands, as described in the IRCv3 SASL specification.
16
+func EncodeSASLResponse(raw []byte) (result []string) {
17
+	// https://ircv3.net/specs/extensions/sasl-3.1#the-authenticate-command
18
+	// "The response is encoded in Base64 (RFC 4648), then split to 400-byte chunks,
19
+	// and each chunk is sent as a separate AUTHENTICATE command. Empty (zero-length)
20
+	// responses are sent as AUTHENTICATE +. If the last chunk was exactly 400 bytes
21
+	// long, it must also be followed by AUTHENTICATE + to signal end of response."
22
+
23
+	if len(raw) == 0 {
24
+		return []string{"+"}
25
+	}
26
+
27
+	response := base64.StdEncoding.EncodeToString(raw)
28
+	lastLen := 0
29
+	for len(response) > 0 {
30
+		// TODO once we require go 1.21, this can be: lastLen = min(len(response), 400)
31
+		lastLen = len(response)
32
+		if lastLen > 400 {
33
+			lastLen = 400
34
+		}
35
+		result = append(result, response[:lastLen])
36
+		response = response[lastLen:]
37
+	}
38
+
39
+	if lastLen == 400 {
40
+		result = append(result, "+")
41
+	}
42
+
43
+	return result
44
+}
45
+
46
+// SASLBuffer handles buffering and decoding SASL responses sent as parameters
47
+// to AUTHENTICATE commands, as described in the IRCv3 SASL specification.
48
+// Do not copy a SASLBuffer after first use.
49
+type SASLBuffer struct {
50
+	maxLength int
51
+	buffer    strings.Builder
52
+}
53
+
54
+// NewSASLBuffer returns a new SASLBuffer. maxLength is the maximum amount of
55
+// base64'ed data to buffer (0 for no limit).
56
+func NewSASLBuffer(maxLength int) *SASLBuffer {
57
+	result := new(SASLBuffer)
58
+	result.Initialize(maxLength)
59
+	return result
60
+}
61
+
62
+// Initialize initializes a SASLBuffer in place.
63
+func (b *SASLBuffer) Initialize(maxLength int) {
64
+	b.maxLength = maxLength
65
+}
66
+
67
+// Add processes an additional SASL response chunk sent via AUTHENTICATE.
68
+// If the response is complete, it resets the buffer and returns the decoded
69
+// response along with any decoding or protocol errors detected.
70
+func (b *SASLBuffer) Add(value string) (done bool, output []byte, err error) {
71
+	if value == "+" {
72
+		output, err = b.getAndReset()
73
+		return true, output, err
74
+	}
75
+
76
+	if len(value) > 400 {
77
+		b.buffer.Reset()
78
+		return true, nil, ErrSASLTooLong
79
+	}
80
+
81
+	if b.maxLength != 0 && (b.buffer.Len()+len(value)) > b.maxLength {
82
+		b.buffer.Reset()
83
+		return true, nil, ErrSASLLimitExceeded
84
+	}
85
+
86
+	b.buffer.WriteString(value)
87
+	if len(value) < 400 {
88
+		output, err = b.getAndReset()
89
+		return true, output, err
90
+	} else {
91
+		// 400 bytes, wait for continuation line or +
92
+		return false, nil, nil
93
+	}
94
+}
95
+
96
+// Clear resets the buffer state.
97
+func (b *SASLBuffer) Clear() {
98
+	b.buffer.Reset()
99
+}
100
+
101
+func (b *SASLBuffer) getAndReset() (output []byte, err error) {
102
+	output, err = base64.StdEncoding.DecodeString(b.buffer.String())
103
+	b.buffer.Reset()
104
+	return
105
+}

+ 1
- 3
vendor/modules.txt View File

@@ -16,7 +16,7 @@ github.com/ergochat/confusables
16 16
 # github.com/ergochat/go-ident v0.0.0-20230911071154-8c30606d6881
17 17
 ## explicit; go 1.18
18 18
 github.com/ergochat/go-ident
19
-# github.com/ergochat/irc-go v0.4.0
19
+# github.com/ergochat/irc-go v0.5.0-rc1
20 20
 ## explicit; go 1.15
21 21
 github.com/ergochat/irc-go/ircfmt
22 22
 github.com/ergochat/irc-go/ircmsg
@@ -30,8 +30,6 @@ github.com/go-sql-driver/mysql
30 30
 # github.com/gofrs/flock v0.8.1
31 31
 ## explicit
32 32
 github.com/gofrs/flock
33
-# github.com/golang-jwt/jwt v3.2.2+incompatible
34
-## explicit
35 33
 # github.com/golang-jwt/jwt/v5 v5.2.0
36 34
 ## explicit; go 1.18
37 35
 github.com/golang-jwt/jwt/v5

Loading…
Cancel
Save