// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use // of this source code is governed by the MIT license that can be found in // the LICENSE file. package girc import ( "encoding/base64" "fmt" ) // SASLMech is an representation of what a SASL mechanism should support. // See SASLExternal and SASLPlain for implementations of this. type SASLMech interface { // Method returns the uppercase version of the SASL mechanism name. Method() string // Encode returns the response that the SASL mechanism wants to use. If // the returned string is empty (e.g. the mechanism gives up), the handler // will attempt to panic, as expectation is that if SASL authentication // fails, the client will disconnect. Encode(params []string) (output string) } // SASLExternal implements the "EXTERNAL" SASL type. type SASLExternal struct { // Identity is an optional field which allows the client to specify // pre-authentication identification. This means that EXTERNAL will // supply this in the initial response. This usually isn't needed (e.g. // CertFP). Identity string `json:"identity"` } // Method identifies what type of SASL this implements. func (sasl *SASLExternal) Method() string { return "EXTERNAL" } // Encode for external SALS authentication should really only return a "+", // unless the user has specified pre-authentication or identification data. // See https://tools.ietf.org/html/rfc4422#appendix-A for more info. func (sasl *SASLExternal) Encode(params []string) string { if len(params) != 1 || params[0] != "+" { return "" } if sasl.Identity != "" { return sasl.Identity } return "+" } // SASLPlain contains the user and password needed for PLAIN SASL authentication. type SASLPlain struct { User string `json:"user"` // User is the username for SASL. Pass string `json:"pass"` // Pass is the password for SASL. } // Method identifies what type of SASL this implements. func (sasl *SASLPlain) Method() string { return "PLAIN" } // Encode encodes the plain user+password into a SASL PLAIN implementation. // See https://tools.ietf.org/rfc/rfc4422.txt for more info. func (sasl *SASLPlain) Encode(params []string) string { if len(params) != 1 || params[0] != "+" { return "" } in := []byte(sasl.User) in = append(in, 0x0) in = append(in, []byte(sasl.User)...) in = append(in, 0x0) in = append(in, []byte(sasl.Pass)...) return base64.StdEncoding.EncodeToString(in) } const saslChunkSize = 400 func handleSASL(c *Client, e Event) { if e.Command == RPL_SASLSUCCESS || e.Command == ERR_SASLALREADY { // Let the server know that we're done. c.write(&Event{Command: CAP, Params: []string{CAP_END}}) return } // Assume they want us to handle sending auth. auth := c.Config.SASL.Encode(e.Params) if auth == "" { // Assume the SASL authentication method doesn't want to respond for // some reason. The SASL spec and IRCv3 spec do not define a clear // way to abort a SASL exchange, other than to disconnect, or proceed // with CAP END. c.rx <- &Event{Command: ERROR, Trailing: fmt.Sprintf( "closing connection: SASL %s failed: %s", c.Config.SASL.Method(), e.Trailing, )} return } // Send in "saslChunkSize"-length byte chunks. If the last chuck is // exactly "saslChunkSize" bytes, send a "AUTHENTICATE +" 0-byte // acknowledgement response to let the server know that we're done. for { if len(auth) > saslChunkSize { c.write(&Event{Command: AUTHENTICATE, Params: []string{auth[0 : saslChunkSize-1]}, Sensitive: true}) auth = auth[saslChunkSize:] continue } if len(auth) <= saslChunkSize { c.write(&Event{Command: AUTHENTICATE, Params: []string{auth}, Sensitive: true}) if len(auth) == 400 { c.write(&Event{Command: AUTHENTICATE, Params: []string{"+"}}) } break } } return } func handleSASLError(c *Client, e Event) { if c.Config.SASL == nil { c.write(&Event{Command: CAP, Params: []string{CAP_END}}) return } // Authentication failed. The SASL spec and IRCv3 spec do not define a // clear way to abort a SASL exchange, other than to disconnect, or // proceed with CAP END. c.rx <- &Event{Command: ERROR, Trailing: "closing connection: " + e.Trailing} }