2016-09-04 11:25:33 +02:00
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
2016-09-06 08:31:59 +02:00
import (
2016-09-07 12:46:01 +02:00
"bytes"
2016-09-06 08:31:59 +02:00
"encoding/base64"
2016-09-07 12:46:01 +02:00
"encoding/json"
"errors"
"fmt"
"strconv"
2016-09-06 08:31:59 +02:00
"strings"
"time"
"github.com/DanielOaks/girc-go/ircmsg"
2016-09-07 12:46:01 +02:00
"github.com/tidwall/buntdb"
2016-09-06 08:31:59 +02:00
)
2016-09-04 11:25:33 +02:00
2016-09-05 14:35:13 +02:00
var (
2016-09-06 08:31:59 +02:00
// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support.
// This can be moved to some other data structure/place if we need to load/unload mechs later.
EnabledSaslMechanisms = map [ string ] func ( * Server , * Client , string , [ ] byte ) bool {
"PLAIN" : authPlainHandler ,
"EXTERNAL" : authExternalHandler ,
}
// NoAccount is a placeholder which means that the user is not logged into an account.
2016-09-05 14:35:13 +02:00
NoAccount = ClientAccount {
Name : "*" , // * is used until actual account name is set
}
2016-09-07 12:46:01 +02:00
// generic sasl fail error
errSaslFail = errors . New ( "SASL failed" )
2016-09-05 14:35:13 +02:00
)
// ClientAccount represents a user account.
type ClientAccount struct {
2016-09-04 11:25:33 +02:00
// Name of the account.
Name string
// RegisteredAt represents the time that the account was registered.
RegisteredAt time . Time
// Clients that are currently logged into this account (useful for notifications).
2016-09-05 14:54:09 +02:00
Clients [ ] * Client
2016-09-04 11:25:33 +02:00
}
2016-09-06 08:31:59 +02:00
2016-09-07 13:32:58 +02:00
// loadAccountCredentials loads an account's credentials from the store.
func loadAccountCredentials ( tx * buntdb . Tx , accountKey string ) ( * AccountCredentials , error ) {
credText , err := tx . Get ( fmt . Sprintf ( keyAccountCredentials , accountKey ) )
if err != nil {
return nil , err
}
var creds AccountCredentials
err = json . Unmarshal ( [ ] byte ( credText ) , & creds )
if err != nil {
return nil , err
}
return & creds , nil
}
// loadAccount loads an account from the store, note that the account must actually exist.
func loadAccount ( server * Server , tx * buntdb . Tx , accountKey string ) * ClientAccount {
name , _ := tx . Get ( fmt . Sprintf ( keyAccountName , accountKey ) )
regTime , _ := tx . Get ( fmt . Sprintf ( keyAccountRegTime , accountKey ) )
regTimeInt , _ := strconv . ParseInt ( regTime , 10 , 64 )
accountInfo := ClientAccount {
Name : name ,
RegisteredAt : time . Unix ( regTimeInt , 0 ) ,
Clients : [ ] * Client { } ,
}
server . accounts [ accountKey ] = & accountInfo
return & accountInfo
}
2016-09-06 08:31:59 +02:00
// authenticateHandler parses the AUTHENTICATE command (for SASL authentication).
func authenticateHandler ( server * Server , client * Client , msg ircmsg . IrcMessage ) bool {
// sasl abort
if len ( msg . Params ) == 1 && msg . Params [ 0 ] == "*" {
if client . saslInProgress {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLABORTED , client . nick , "SASL authentication aborted" )
2016-09-06 08:31:59 +02:00
} else {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed" )
2016-09-06 08:31:59 +02:00
}
client . saslInProgress = false
client . saslMechanism = ""
client . saslValue = ""
return false
}
// start new sasl session
if ! client . saslInProgress {
mechanism := strings . ToUpper ( msg . Params [ 0 ] )
_ , mechanismIsEnabled := EnabledSaslMechanisms [ mechanism ]
if mechanismIsEnabled {
client . saslInProgress = true
client . saslMechanism = mechanism
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , "AUTHENTICATE" , "+" )
2016-09-06 08:31:59 +02:00
} else {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed" )
2016-09-06 08:31:59 +02:00
}
return false
}
// continue existing sasl session
rawData := msg . Params [ 0 ]
if len ( rawData ) > 400 {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLTOOLONG , client . nick , "SASL message too long" )
2016-09-06 08:31:59 +02:00
client . saslInProgress = false
client . saslMechanism = ""
client . saslValue = ""
return false
} else if len ( rawData ) == 400 {
client . saslValue += rawData
// allow 4 'continuation' lines before rejecting for length
if len ( client . saslValue ) > 400 * 4 {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed: Passphrase too long" )
2016-09-06 08:31:59 +02:00
client . saslInProgress = false
client . saslMechanism = ""
client . saslValue = ""
return false
}
return false
2016-09-19 12:52:24 +02:00
}
if rawData != "+" {
2016-09-06 08:31:59 +02:00
client . saslValue += rawData
}
2016-09-07 13:32:58 +02:00
var data [ ] byte
var err error
if client . saslValue != "+" {
data , err = base64 . StdEncoding . DecodeString ( client . saslValue )
if err != nil {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed: Invalid b64 encoding" )
2016-09-07 13:32:58 +02:00
client . saslInProgress = false
client . saslMechanism = ""
client . saslValue = ""
return false
}
2016-09-06 08:31:59 +02:00
}
// call actual handler
handler , handlerExists := EnabledSaslMechanisms [ client . saslMechanism ]
// like 100% not required, but it's good to be safe I guess
if ! handlerExists {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed" )
2016-09-06 08:31:59 +02:00
client . saslInProgress = false
client . saslMechanism = ""
client . saslValue = ""
return false
}
2016-09-07 12:46:01 +02:00
// sasl is being done now by the handler, so we empty the client's vars now
client . saslInProgress = false
client . saslMechanism = ""
client . saslValue = ""
2016-09-06 08:31:59 +02:00
return handler ( server , client , client . saslMechanism , data )
}
// authPlainHandler parses the SASL PLAIN mechanism.
func authPlainHandler ( server * Server , client * Client , mechanism string , value [ ] byte ) bool {
2016-09-07 12:46:01 +02:00
splitValue := bytes . Split ( value , [ ] byte { '\000' } )
if len ( splitValue ) != 3 {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed: Invalid auth blob" )
2016-09-07 12:46:01 +02:00
return false
}
2016-09-07 13:32:58 +02:00
accountKey := string ( splitValue [ 0 ] )
2016-09-07 12:46:01 +02:00
authzid := string ( splitValue [ 1 ] )
2016-09-07 13:32:58 +02:00
if accountKey != authzid {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed: authcid and authzid should be the same" )
2016-09-07 12:46:01 +02:00
return false
}
// keep it the same as in the REG CREATE stage
2016-10-11 15:51:46 +02:00
accountKey , err := CasefoldName ( accountKey )
if err != nil {
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed: Bad account name" )
return false
}
2016-09-07 12:46:01 +02:00
// load and check acct data all in one update to prevent races.
// as noted elsewhere, change to proper locking for Account type later probably
2016-10-11 15:51:46 +02:00
err = server . store . Update ( func ( tx * buntdb . Tx ) error {
2016-09-07 13:32:58 +02:00
creds , err := loadAccountCredentials ( tx , accountKey )
2016-09-07 12:46:01 +02:00
if err != nil {
return err
}
// ensure creds are valid
password := string ( splitValue [ 2 ] )
if len ( creds . PassphraseHash ) < 1 || len ( creds . PassphraseSalt ) < 1 || len ( password ) < 1 {
return errSaslFail
}
err = server . passwords . CompareHashAndPassword ( creds . PassphraseHash , creds . PassphraseSalt , password )
// succeeded, load account info if necessary
2016-09-07 13:32:58 +02:00
account , exists := server . accounts [ accountKey ]
2016-09-07 12:46:01 +02:00
if ! exists {
2016-09-07 13:32:58 +02:00
account = loadAccount ( server , tx , accountKey )
2016-09-07 12:46:01 +02:00
}
account . Clients = append ( account . Clients , client )
client . account = account
return err
} )
if err != nil {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed" )
2016-09-07 12:46:01 +02:00
return false
}
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , RPL_LOGGEDIN , client . nick , client . nickMaskString , client . account . Name , fmt . Sprintf ( "You are now logged in as %s" , client . account . Name ) )
client . Send ( nil , server . name , RPL_SASLSUCCESS , client . nick , "SASL authentication successful" )
2016-09-06 08:31:59 +02:00
return false
}
// authExternalHandler parses the SASL EXTERNAL mechanism.
func authExternalHandler ( server * Server , client * Client , mechanism string , value [ ] byte ) bool {
2016-09-07 13:32:58 +02:00
if client . certfp == "" {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed, you are not connecting with a caertificate" )
2016-09-07 13:32:58 +02:00
return false
}
err := server . store . Update ( func ( tx * buntdb . Tx ) error {
// certfp lookup key
accountKey , err := tx . Get ( fmt . Sprintf ( keyCertToAccount , client . certfp ) )
if err != nil {
return errSaslFail
}
// confirm account exists
_ , err = tx . Get ( fmt . Sprintf ( keyAccountExists , accountKey ) )
if err != nil {
return errSaslFail
}
// confirm the certfp in that account's credentials
creds , err := loadAccountCredentials ( tx , accountKey )
if err != nil || creds . Certificate != client . certfp {
return errSaslFail
}
// succeeded, load account info if necessary
account , exists := server . accounts [ accountKey ]
if ! exists {
account = loadAccount ( server , tx , accountKey )
}
account . Clients = append ( account . Clients , client )
client . account = account
return nil
} )
if err != nil {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed" )
2016-09-07 13:32:58 +02:00
return false
}
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , RPL_LOGGEDIN , client . nick , client . nickMaskString , client . account . Name , fmt . Sprintf ( "You are now logged in as %s" , client . account . Name ) )
client . Send ( nil , server . name , RPL_SASLSUCCESS , client . nick , "SASL authentication successful" )
2016-09-06 08:31:59 +02:00
return false
}