2020-05-23 06:05:37 +00:00
|
|
|
package irc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2020-06-04 00:28:41 +00:00
|
|
|
"crypto/sha1"
|
|
|
|
"crypto/sha256"
|
|
|
|
"crypto/sha512"
|
2020-05-23 06:05:37 +00:00
|
|
|
"encoding/base64"
|
2020-06-04 00:28:41 +00:00
|
|
|
"errors"
|
|
|
|
"hash"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/xdg-go/scram"
|
2020-05-23 06:05:37 +00:00
|
|
|
)
|
|
|
|
|
2020-06-04 00:28:41 +00:00
|
|
|
var DefaultSASLMechanisms = []string{
|
|
|
|
"EXTERNAL",
|
|
|
|
//"SCRAM-SHA-512",
|
|
|
|
"SCRAM-SHA-256",
|
|
|
|
//"SCRAM-SHA-1",
|
|
|
|
"PLAIN",
|
|
|
|
}
|
|
|
|
|
2020-05-23 06:05:37 +00:00
|
|
|
type SASL interface {
|
|
|
|
Name() string
|
2020-06-04 00:28:41 +00:00
|
|
|
Step(response string) (string, error)
|
2020-05-23 06:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type SASLPlain struct {
|
|
|
|
Username string
|
|
|
|
Password string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SASLPlain) Name() string {
|
|
|
|
return "PLAIN"
|
|
|
|
}
|
|
|
|
|
2020-06-04 00:28:41 +00:00
|
|
|
func (s *SASLPlain) Step(string) (string, error) {
|
2020-05-23 06:05:37 +00:00
|
|
|
buf := bytes.Buffer{}
|
|
|
|
buf.WriteString(s.Username)
|
|
|
|
buf.WriteByte(0x0)
|
|
|
|
buf.WriteString(s.Username)
|
|
|
|
buf.WriteByte(0x0)
|
|
|
|
buf.WriteString(s.Password)
|
|
|
|
|
2020-06-04 00:28:41 +00:00
|
|
|
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
2020-05-23 06:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type SASLExternal struct{}
|
|
|
|
|
|
|
|
func (s *SASLExternal) Name() string {
|
|
|
|
return "EXTERNAL"
|
|
|
|
}
|
|
|
|
|
2020-06-04 00:28:41 +00:00
|
|
|
func (s *SASLExternal) Step(string) (string, error) {
|
|
|
|
return "+", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
scramHashes = map[string]scram.HashGeneratorFcn{
|
|
|
|
"SHA-512": func() hash.Hash { return sha512.New() },
|
|
|
|
"SHA-256": func() hash.Hash { return sha256.New() },
|
|
|
|
"SHA-1": func() hash.Hash { return sha1.New() },
|
|
|
|
}
|
|
|
|
|
|
|
|
ErrUnsupportedHash = errors.New("unsupported hash algorithm")
|
|
|
|
)
|
|
|
|
|
|
|
|
type SASLScram struct {
|
|
|
|
Username string
|
|
|
|
Password string
|
|
|
|
Hash string
|
|
|
|
conv *scram.ClientConversation
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SASLScram) Name() string {
|
|
|
|
return "SCRAM-" + s.Hash
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *SASLScram) Step(response string) (string, error) {
|
|
|
|
if s.conv == nil {
|
|
|
|
if hash, ok := scramHashes[s.Hash]; ok {
|
|
|
|
client, err := hash.NewClient(s.Username, s.Password, "")
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
s.conv = client.NewConversation()
|
|
|
|
} else {
|
|
|
|
return "", ErrUnsupportedHash
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
challenge := ""
|
|
|
|
if response != "+" {
|
|
|
|
b, err := base64.StdEncoding.DecodeString(response)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
challenge = string(b)
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := s.conv.Step(challenge)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if s.conv.Done() {
|
|
|
|
s.conv = nil
|
|
|
|
return "+", nil
|
|
|
|
} else {
|
|
|
|
return base64.StdEncoding.EncodeToString([]byte(res)), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-04 03:06:59 +00:00
|
|
|
func (c *Client) tryNextSASL() {
|
|
|
|
if len(c.saslMechanisms) > 0 {
|
|
|
|
c.currentSASL, c.saslMechanisms = c.saslMechanisms[0], c.saslMechanisms[1:]
|
|
|
|
c.authenticate(c.currentSASL.Name())
|
|
|
|
} else {
|
|
|
|
c.finishCAP()
|
|
|
|
}
|
2020-06-04 00:28:41 +00:00
|
|
|
}
|
|
|
|
|
2020-06-04 03:06:59 +00:00
|
|
|
func (c *Client) filterSASLMechanisms(supportedMechs []string) {
|
|
|
|
saslMechanisms := []SASL{}
|
|
|
|
|
|
|
|
for _, mech := range c.saslMechanisms {
|
|
|
|
for _, supported := range supportedMechs {
|
|
|
|
if mech.Name() == supported {
|
|
|
|
saslMechanisms = append(saslMechanisms, mech)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2020-06-04 00:28:41 +00:00
|
|
|
}
|
2020-06-04 03:06:59 +00:00
|
|
|
|
|
|
|
c.saslMechanisms = saslMechanisms
|
2020-05-23 06:05:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) handleSASL(msg *Message) {
|
|
|
|
switch msg.Command {
|
|
|
|
case AUTHENTICATE:
|
2020-06-04 03:06:59 +00:00
|
|
|
if c.currentSASL == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: handle 400 chunking on incoming messages
|
|
|
|
auth, err := c.currentSASL.Step(msg.LastParam())
|
2020-06-04 00:28:41 +00:00
|
|
|
if err != nil {
|
2020-06-04 03:06:59 +00:00
|
|
|
c.tryNextSASL()
|
2020-06-04 00:28:41 +00:00
|
|
|
return
|
|
|
|
}
|
2020-05-23 06:05:37 +00:00
|
|
|
|
|
|
|
for len(auth) >= 400 {
|
2020-06-04 00:28:41 +00:00
|
|
|
c.authenticate(auth)
|
2020-05-23 06:05:37 +00:00
|
|
|
auth = auth[400:]
|
|
|
|
}
|
|
|
|
if len(auth) > 0 {
|
2020-06-04 00:28:41 +00:00
|
|
|
c.authenticate(auth)
|
|
|
|
} else {
|
|
|
|
c.authenticate("+")
|
|
|
|
}
|
|
|
|
|
2020-06-04 03:06:59 +00:00
|
|
|
case ERR_SASLFAIL, ERR_SASLTOOLONG, ERR_SASLABORTED:
|
|
|
|
c.tryNextSASL()
|
|
|
|
|
2020-06-04 00:28:41 +00:00
|
|
|
case RPL_SASLMECHS:
|
|
|
|
if len(msg.Params) > 1 {
|
|
|
|
supportedMechs := strings.Split(msg.Params[1], ",")
|
2020-06-04 03:06:59 +00:00
|
|
|
c.filterSASLMechanisms(supportedMechs)
|
2020-06-04 00:28:41 +00:00
|
|
|
}
|
|
|
|
|
2020-06-04 03:06:59 +00:00
|
|
|
if len(c.saslMechanisms) == 0 {
|
2020-06-04 00:28:41 +00:00
|
|
|
c.finishCAP()
|
2020-05-23 06:05:37 +00:00
|
|
|
}
|
|
|
|
|
2020-06-04 03:06:59 +00:00
|
|
|
case RPL_SASLSUCCESS, RPL_LOGGEDIN, ERR_NICKLOCKED:
|
2020-06-04 00:28:41 +00:00
|
|
|
c.finishCAP()
|
2020-05-23 06:05:37 +00:00
|
|
|
}
|
|
|
|
}
|