2017-03-27 14:15:02 +02:00
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
2016-09-04 11:25:33 +02:00
// 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"
2017-06-15 18:14:19 +02:00
"github.com/goshuirc/irc-go/ircfmt"
"github.com/goshuirc/irc-go/ircmsg"
2017-06-14 20:00:53 +02:00
"github.com/oragono/oragono/irc/sno"
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
2017-03-11 13:01:40 +01:00
const (
keyAccountExists = "account.exists %s"
keyAccountVerified = "account.verified %s"
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
keyAccountRegTime = "account.registered.time %s"
keyAccountCredentials = "account.credentials %s"
keyCertToAccount = "account.creds.certfp %s"
)
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
2017-03-06 00:43:52 +01:00
if ! server . accountAuthenticationEnabled || len ( msg . Params ) == 1 && msg . Params [ 0 ] == "*" {
2017-06-30 03:06:10 +02:00
client . Send ( nil , server . name , ERR_SASLABORTED , client . nick , "SASL authentication aborted" )
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-10-16 06:01:40 +02:00
// let the SASL handler do its thing
exiting := handler ( server , client , client . saslMechanism , data )
// wait 'til SASL is done before emptying the sasl vars
2016-09-07 12:46:01 +02:00
client . saslInProgress = false
client . saslMechanism = ""
client . saslValue = ""
2016-10-16 06:01:40 +02:00
return exiting
2016-09-06 08:31:59 +02:00
}
// 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' } )
2016-12-01 09:48:11 +01:00
var accountKey , authzid string
2016-09-07 12:46:01 +02:00
2016-12-01 09:48:11 +01:00
if len ( splitValue ) == 3 {
accountKey = string ( splitValue [ 0 ] )
authzid = string ( splitValue [ 1 ] )
2016-09-07 12:46:01 +02:00
2016-12-01 09:48:11 +01:00
if accountKey == "" {
accountKey = authzid
} else if accountKey != authzid {
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed: authcid and authzid should be the same" )
return false
}
} else {
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
}
// 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 {
2017-01-10 16:02:26 +01:00
// confirm account is verified
_ , err = tx . Get ( fmt . Sprintf ( keyAccountVerified , accountKey ) )
if err != nil {
return errSaslFail
}
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
}
2017-03-08 12:50:12 +01:00
client . LoginToAccount ( account )
2016-09-07 12:46:01 +02:00
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-13 10:18:00 +02:00
client . successfulSaslAuth ( )
2016-09-06 08:31:59 +02:00
return false
}
2017-03-08 12:50:12 +01:00
// LoginToAccount logs the client into the given account.
func ( client * Client ) LoginToAccount ( account * ClientAccount ) {
if client . account == account {
// already logged into this acct, no changing necessary
return
2017-09-28 07:49:01 +02:00
} else if client . LoggedIntoAccount ( ) {
2017-03-08 12:50:12 +01:00
// logout of existing acct
var newClientAccounts [ ] * Client
for _ , c := range account . Clients {
if c != client {
newClientAccounts = append ( newClientAccounts , c )
}
}
account . Clients = newClientAccounts
}
account . Clients = append ( account . Clients , client )
client . account = account
2017-05-28 20:43:09 +02:00
client . server . snomasks . Send ( sno . LocalAccounts , fmt . Sprintf ( ircfmt . Unescape ( "Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]" ) , client . nickMaskString , account . Name ) )
2017-09-11 01:16:13 +02:00
//TODO(dan): This should output the AccountNotify message instead of the sasl accepted function below.
}
// LogoutOfAccount logs the client out of their current account.
func ( client * Client ) LogoutOfAccount ( ) {
account := client . account
if account == nil {
// already logged out
return
}
// logout of existing acct
var newClientAccounts [ ] * Client
for _ , c := range account . Clients {
if c != client {
newClientAccounts = append ( newClientAccounts , c )
}
}
account . Clients = newClientAccounts
client . account = nil
// dispatch account-notify
for friend := range client . Friends ( AccountNotify ) {
friend . Send ( nil , client . nickMaskString , "ACCOUNT" , "*" )
}
2017-03-08 12:50:12 +01:00
}
2016-09-06 08:31:59 +02:00
// 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 == "" {
2017-09-28 07:49:01 +02:00
client . Send ( nil , server . name , ERR_SASLFAIL , client . nick , "SASL authentication failed, you are not connecting with a certificate" )
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
}
2017-01-10 16:02:26 +01:00
// confirm account is verified
_ , err = tx . Get ( fmt . Sprintf ( keyAccountVerified , accountKey ) )
if err != nil {
return errSaslFail
}
2016-09-07 13:32:58 +02:00
// 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 )
}
2017-03-08 12:50:12 +01:00
client . LoginToAccount ( account )
2016-09-07 13:32:58 +02:00
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-13 10:18:00 +02:00
client . successfulSaslAuth ( )
2016-09-06 08:31:59 +02:00
return false
}
2016-10-13 10:18:00 +02:00
// successfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages.
2017-04-16 03:31:33 +02:00
func ( client * Client ) successfulSaslAuth ( ) {
client . Send ( nil , client . 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 , client . server . name , RPL_SASLSUCCESS , client . nick , "SASL authentication successful" )
2016-10-13 10:18:00 +02:00
// dispatch account-notify
2017-04-16 03:31:33 +02:00
for friend := range client . Friends ( AccountNotify ) {
friend . Send ( nil , client . nickMaskString , "ACCOUNT" , client . account . Name )
2016-10-13 10:18:00 +02:00
}
}