implement SCRAM-SHA-256

This commit is contained in:
Shivaram Lingamneni
2021-07-30 12:20:13 -04:00
parent 3264687803
commit e1401934df
27 changed files with 1545 additions and 10 deletions
+52 -5
View File
@@ -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);
+2
View File
@@ -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
View File
@@ -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)
}
+1
View File
@@ -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
View File
@@ -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