mirror of
https://github.com/jeremyd/ergo.git
synced 2026-07-12 03:08:11 -07:00
implement SCRAM-SHA-256
This commit is contained in:
+52
-5
@@ -5,6 +5,7 @@ package irc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/ergochat/irc-go/ircutils"
|
||||
"github.com/xdg-go/scram"
|
||||
|
||||
"github.com/ergochat/ergo/irc/connection_limits"
|
||||
"github.com/ergochat/ergo/irc/email"
|
||||
@@ -517,8 +519,8 @@ func validatePassphrase(passphrase string) error {
|
||||
}
|
||||
|
||||
// changes the password for an account
|
||||
func (am *AccountManager) setPassword(account string, password string, hasPrivs bool) (err error) {
|
||||
cfAccount, err := CasefoldName(account)
|
||||
func (am *AccountManager) setPassword(accountName string, password string, hasPrivs bool) (err error) {
|
||||
cfAccount, err := CasefoldName(accountName)
|
||||
if err != nil {
|
||||
return errAccountDoesNotExist
|
||||
}
|
||||
@@ -1912,9 +1914,10 @@ func (am *AccountManager) Logout(client *Client) {
|
||||
var (
|
||||
// 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, *ResponseBuffer) bool{
|
||||
"PLAIN": authPlainHandler,
|
||||
"EXTERNAL": authExternalHandler,
|
||||
EnabledSaslMechanisms = map[string]func(*Server, *Client, *Session, []byte, *ResponseBuffer) bool{
|
||||
"PLAIN": authPlainHandler,
|
||||
"EXTERNAL": authExternalHandler,
|
||||
"SCRAM-SHA-256": authScramHandler,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1933,6 +1936,12 @@ type AccountCredentials struct {
|
||||
Version CredentialsVersion
|
||||
PassphraseHash []byte
|
||||
Certfps []string
|
||||
SCRAMCreds struct {
|
||||
Salt []byte
|
||||
Iters int
|
||||
StoredKey []byte
|
||||
ServerKey []byte
|
||||
}
|
||||
}
|
||||
|
||||
func (ac *AccountCredentials) Empty() bool {
|
||||
@@ -1964,9 +1973,47 @@ func (ac *AccountCredentials) SetPassphrase(passphrase string, bcryptCost uint)
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
|
||||
// we can pass an empty account name because it won't actually be incorporated
|
||||
// into the credentials; it's just a quirk of the xdg-go/scram API that the way
|
||||
// to produce server credentials is to call NewClient* and then GetStoredCredentials
|
||||
scramClient, err := scram.SHA256.NewClientUnprepped("", passphrase, "")
|
||||
if err != nil {
|
||||
return errAccountBadPassphrase
|
||||
}
|
||||
salt := make([]byte, 16)
|
||||
rand.Read(salt)
|
||||
// xdg-go/scram says: "Clients have a default minimum PBKDF2 iteration count of 4096."
|
||||
minIters := 4096
|
||||
scramCreds := scramClient.GetStoredCredentials(scram.KeyFactors{Salt: string(salt), Iters: minIters})
|
||||
ac.SCRAMCreds.Salt = salt
|
||||
ac.SCRAMCreds.Iters = minIters
|
||||
ac.SCRAMCreds.StoredKey = scramCreds.StoredKey
|
||||
ac.SCRAMCreds.ServerKey = scramCreds.ServerKey
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *AccountManager) NewScramConversation() *scram.ServerConversation {
|
||||
server, _ := scram.SHA256.NewServer(am.lookupSCRAMCreds)
|
||||
return server.NewConversation()
|
||||
}
|
||||
|
||||
func (am *AccountManager) lookupSCRAMCreds(accountName string) (creds scram.StoredCredentials, err error) {
|
||||
acct, err := am.LoadAccount(accountName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if acct.Credentials.SCRAMCreds.Iters == 0 {
|
||||
err = errNoSCRAMCredentials
|
||||
return
|
||||
}
|
||||
creds.Salt = string(acct.Credentials.SCRAMCreds.Salt)
|
||||
creds.Iters = acct.Credentials.SCRAMCreds.Iters
|
||||
creds.StoredKey = acct.Credentials.SCRAMCreds.StoredKey
|
||||
creds.ServerKey = acct.Credentials.SCRAMCreds.ServerKey
|
||||
return
|
||||
}
|
||||
|
||||
func (ac *AccountCredentials) AddCertfp(certfp string) (err error) {
|
||||
// XXX we require that certfp is already normalized (rather than normalize here
|
||||
// and pass back the normalized version as an additional return parameter);
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/ergochat/irc-go/ircfmt"
|
||||
"github.com/ergochat/irc-go/ircmsg"
|
||||
"github.com/ergochat/irc-go/ircreader"
|
||||
"github.com/xdg-go/scram"
|
||||
|
||||
"github.com/ergochat/ergo/irc/caps"
|
||||
"github.com/ergochat/ergo/irc/connection_limits"
|
||||
@@ -116,6 +117,7 @@ type Client struct {
|
||||
type saslStatus struct {
|
||||
mechanism string
|
||||
value string
|
||||
scramConv *scram.ServerConversation
|
||||
}
|
||||
|
||||
func (s *saslStatus) Clear() {
|
||||
|
||||
+1
-1
@@ -1379,7 +1379,7 @@ func LoadConfig(filename string) (config *Config, err error) {
|
||||
config.Accounts.VHosts.validRegexp = defaultValidVhostRegex
|
||||
}
|
||||
|
||||
config.Server.capValues[caps.SASL] = "PLAIN,EXTERNAL"
|
||||
config.Server.capValues[caps.SASL] = "PLAIN,EXTERNAL,SCRAM-SHA-256"
|
||||
if !config.Accounts.AuthenticationEnabled {
|
||||
config.Server.supportedCaps.Disable(caps.SASL)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ var (
|
||||
errCASFailed = errors.New("Compare-and-swap update of database value failed")
|
||||
errEmptyCredentials = errors.New("No more credentials are approved")
|
||||
errCredsExternallyManaged = errors.New("Credentials are externally managed and cannot be changed here")
|
||||
errNoSCRAMCredentials = errors.New("SCRAM credentials are not initialized for this account; consult the user guide")
|
||||
errInvalidMultilineBatch = errors.New("Invalid multiline batch")
|
||||
errTimedOut = errors.New("Operation timed out")
|
||||
errInvalidUtf8 = errors.New("Message rejected for invalid utf8")
|
||||
|
||||
+66
-4
@@ -211,6 +211,7 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
|
||||
var err error
|
||||
if session.sasl.value != "+" {
|
||||
data, err = base64.StdEncoding.DecodeString(session.sasl.value)
|
||||
session.sasl.value = ""
|
||||
if err != nil {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, details.nick, client.t("SASL authentication failed: Invalid b64 encoding"))
|
||||
session.sasl.Clear()
|
||||
@@ -229,14 +230,15 @@ func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb
|
||||
}
|
||||
|
||||
// let the SASL handler do its thing
|
||||
exiting := handler(server, client, session.sasl.mechanism, data, rb)
|
||||
session.sasl.Clear()
|
||||
exiting := handler(server, client, session, data, rb)
|
||||
|
||||
return exiting
|
||||
}
|
||||
|
||||
// AUTHENTICATE PLAIN
|
||||
func authPlainHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool {
|
||||
func authPlainHandler(server *Server, client *Client, session *Session, value []byte, rb *ResponseBuffer) bool {
|
||||
defer session.sasl.Clear()
|
||||
|
||||
splitValue := bytes.Split(value, []byte{'\000'})
|
||||
|
||||
// PLAIN has separate "authorization ID" (which user you want to become)
|
||||
@@ -301,7 +303,9 @@ func authErrorToMessage(server *Server, err error) (msg string) {
|
||||
}
|
||||
|
||||
// AUTHENTICATE EXTERNAL
|
||||
func authExternalHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool {
|
||||
func authExternalHandler(server *Server, client *Client, session *Session, value []byte, rb *ResponseBuffer) bool {
|
||||
defer session.sasl.Clear()
|
||||
|
||||
if rb.session.certfp == "" {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed, you are not connecting with a certificate"))
|
||||
return false
|
||||
@@ -343,6 +347,64 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
|
||||
return false
|
||||
}
|
||||
|
||||
// AUTHENTICATE SCRAM-SHA-256
|
||||
func authScramHandler(server *Server, client *Client, session *Session, value []byte, rb *ResponseBuffer) bool {
|
||||
continueAuth := true
|
||||
defer func() {
|
||||
if !continueAuth {
|
||||
session.sasl.Clear()
|
||||
}
|
||||
}()
|
||||
|
||||
// first message? if so, initialize the SCRAM conversation
|
||||
if session.sasl.scramConv == nil {
|
||||
throttled, remainingTime := client.loginThrottle.Touch()
|
||||
if throttled {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
|
||||
continueAuth = false
|
||||
return false
|
||||
}
|
||||
session.sasl.scramConv = server.accounts.NewScramConversation()
|
||||
}
|
||||
|
||||
// wait for a final AUTHENTICATE + from the client to conclude authentication
|
||||
if session.sasl.scramConv.Done() {
|
||||
continueAuth = false
|
||||
if session.sasl.scramConv.Valid() {
|
||||
accountName := session.sasl.scramConv.Username()
|
||||
authzid := session.sasl.scramConv.AuthzID()
|
||||
if authzid != "" && authzid != accountName {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
|
||||
return false
|
||||
}
|
||||
account, err := server.accounts.LoadAccount(accountName)
|
||||
if err == nil {
|
||||
server.accounts.Login(client, account)
|
||||
if fixupNickEqualsAccount(client, rb, server.Config(), "") {
|
||||
sendSuccessfulAccountAuth(nil, client, rb, true)
|
||||
}
|
||||
} else {
|
||||
server.logger.Error("internal", "SCRAM succeeded but couldn't load account", accountName, err.Error())
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed"))
|
||||
}
|
||||
} else {
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed"))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
response, err := session.sasl.scramConv.Step(string(value))
|
||||
if err == nil {
|
||||
rb.Add(nil, server.name, "AUTHENTICATE", base64.StdEncoding.EncodeToString([]byte(response)))
|
||||
} else {
|
||||
continueAuth = false
|
||||
rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AWAY [<message>]
|
||||
func awayHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
|
||||
var isAway bool
|
||||
|
||||
Reference in New Issue
Block a user