mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-22 03:49:27 +01:00
bf04dc24f9
Previously, we generated and prepended a long salt before generating password hashes. This resulted in the hash verification cutting off long before it should do. This form of salting is also not necessary with bcrypt as it's provided by the password hashing and verification functions themselves, so totally rip it out. This commit also adds the functionality for the server to automagically upgrade users to use the new hashing system, which means better security and more assurance that people can't bruteforce passwords. No need to apply a database upgrade to do this, whoo! \o/
772 lines
23 KiB
Go
772 lines
23 KiB
Go
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
|
// released under the MIT license
|
|
|
|
package irc
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/smtp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/oragono/oragono/irc/caps"
|
|
"github.com/oragono/oragono/irc/passwd"
|
|
"github.com/tidwall/buntdb"
|
|
)
|
|
|
|
const (
|
|
keyAccountExists = "account.exists %s"
|
|
keyAccountVerified = "account.verified %s"
|
|
keyAccountCallback = "account.callback %s"
|
|
keyAccountVerificationCode = "account.verificationcode %s"
|
|
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
|
|
keyAccountRegTime = "account.registered.time %s"
|
|
keyAccountCredentials = "account.credentials %s"
|
|
keyAccountAdditionalNicks = "account.additionalnicks %s"
|
|
keyCertToAccount = "account.creds.certfp %s"
|
|
)
|
|
|
|
// everything about accounts is persistent; therefore, the database is the authoritative
|
|
// source of truth for all account information. anything on the heap is just a cache
|
|
type AccountManager struct {
|
|
sync.RWMutex // tier 2
|
|
serialCacheUpdateMutex sync.Mutex // tier 3
|
|
|
|
server *Server
|
|
// track clients logged in to accounts
|
|
accountToClients map[string][]*Client
|
|
nickToAccount map[string]string
|
|
}
|
|
|
|
func NewAccountManager(server *Server) *AccountManager {
|
|
am := AccountManager{
|
|
accountToClients: make(map[string][]*Client),
|
|
nickToAccount: make(map[string]string),
|
|
server: server,
|
|
}
|
|
|
|
am.buildNickToAccountIndex()
|
|
return &am
|
|
}
|
|
|
|
func (am *AccountManager) buildNickToAccountIndex() {
|
|
if !am.server.AccountConfig().NickReservation.Enabled {
|
|
return
|
|
}
|
|
|
|
result := make(map[string]string)
|
|
existsPrefix := fmt.Sprintf(keyAccountExists, "")
|
|
|
|
am.serialCacheUpdateMutex.Lock()
|
|
defer am.serialCacheUpdateMutex.Unlock()
|
|
|
|
err := am.server.store.View(func(tx *buntdb.Tx) error {
|
|
err := tx.AscendGreaterOrEqual("", existsPrefix, func(key, value string) bool {
|
|
if !strings.HasPrefix(key, existsPrefix) {
|
|
return false
|
|
}
|
|
accountName := strings.TrimPrefix(key, existsPrefix)
|
|
if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil {
|
|
result[accountName] = accountName
|
|
}
|
|
if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, accountName)); err == nil {
|
|
additionalNicks := unmarshalReservedNicks(rawNicks)
|
|
for _, nick := range additionalNicks {
|
|
result[nick] = accountName
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return err
|
|
})
|
|
|
|
if err != nil {
|
|
am.server.logger.Error("internal", fmt.Sprintf("couldn't read reserved nicks: %v", err))
|
|
} else {
|
|
am.Lock()
|
|
am.nickToAccount = result
|
|
am.Unlock()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (am *AccountManager) NickToAccount(nick string) string {
|
|
cfnick, err := CasefoldName(nick)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
am.RLock()
|
|
defer am.RUnlock()
|
|
return am.nickToAccount[cfnick]
|
|
}
|
|
|
|
func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
|
|
casefoldedAccount, err := CasefoldName(account)
|
|
if err != nil || account == "" || account == "*" {
|
|
return errAccountCreation
|
|
}
|
|
|
|
// can't register a guest nickname
|
|
renamePrefix := strings.ToLower(am.server.AccountConfig().NickReservation.RenamePrefix)
|
|
if renamePrefix != "" && strings.HasPrefix(casefoldedAccount, renamePrefix) {
|
|
return errAccountAlreadyRegistered
|
|
}
|
|
|
|
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
|
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
|
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
|
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
|
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
|
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
|
certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
|
|
|
|
var creds AccountCredentials
|
|
// it's fine if this is empty, that just means no certificate is authorized
|
|
creds.Certificate = certfp
|
|
if passphrase != "" {
|
|
creds.PassphraseHash, err = passwd.GenerateEncodedPasswordBytes(passphrase)
|
|
creds.PassphraseIsV2 = true
|
|
if err != nil {
|
|
am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
|
|
return errAccountCreation
|
|
}
|
|
}
|
|
|
|
credText, err := json.Marshal(creds)
|
|
if err != nil {
|
|
am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err))
|
|
return errAccountCreation
|
|
}
|
|
credStr := string(credText)
|
|
|
|
registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
|
|
callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
|
|
|
|
var setOptions *buntdb.SetOptions
|
|
ttl := am.server.AccountConfig().Registration.VerifyTimeout
|
|
if ttl != 0 {
|
|
setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
|
|
}
|
|
|
|
err = func() error {
|
|
am.serialCacheUpdateMutex.Lock()
|
|
defer am.serialCacheUpdateMutex.Unlock()
|
|
|
|
// can't register an account with the same name as a registered nick
|
|
if am.NickToAccount(casefoldedAccount) != "" {
|
|
return errAccountAlreadyRegistered
|
|
}
|
|
|
|
return am.server.store.Update(func(tx *buntdb.Tx) error {
|
|
_, err := am.loadRawAccount(tx, casefoldedAccount)
|
|
if err != errAccountDoesNotExist {
|
|
return errAccountAlreadyRegistered
|
|
}
|
|
|
|
if certfp != "" {
|
|
// make sure certfp doesn't already exist because that'd be silly
|
|
_, err := tx.Get(certFPKey)
|
|
if err != buntdb.ErrNotFound {
|
|
return errCertfpAlreadyExists
|
|
}
|
|
}
|
|
|
|
tx.Set(accountKey, "1", setOptions)
|
|
tx.Set(accountNameKey, account, setOptions)
|
|
tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
|
|
tx.Set(credentialsKey, credStr, setOptions)
|
|
tx.Set(callbackKey, callbackSpec, setOptions)
|
|
if certfp != "" {
|
|
tx.Set(certFPKey, casefoldedAccount, setOptions)
|
|
}
|
|
return nil
|
|
})
|
|
}()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue)
|
|
if err != nil {
|
|
am.Unregister(casefoldedAccount)
|
|
return errCallbackFailed
|
|
} else {
|
|
return am.server.store.Update(func(tx *buntdb.Tx) error {
|
|
_, _, err = tx.Set(verificationCodeKey, code, setOptions)
|
|
return err
|
|
})
|
|
}
|
|
}
|
|
|
|
func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
|
|
if callbackNamespace == "*" || callbackNamespace == "none" {
|
|
return "", nil
|
|
} else if callbackNamespace == "mailto" {
|
|
return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue)
|
|
} else {
|
|
return "", errors.New(fmt.Sprintf("Callback not implemented: %s", callbackNamespace))
|
|
}
|
|
}
|
|
|
|
func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
|
|
config := am.server.AccountConfig().Registration.Callbacks.Mailto
|
|
buf := make([]byte, 16)
|
|
rand.Read(buf)
|
|
code = hex.EncodeToString(buf)
|
|
|
|
subject := config.VerifyMessageSubject
|
|
if subject == "" {
|
|
subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
|
|
}
|
|
messageStrings := []string{
|
|
fmt.Sprintf("From: %s\r\n", config.Sender),
|
|
fmt.Sprintf("To: %s\r\n", callbackValue),
|
|
fmt.Sprintf("Subject: %s\r\n", subject),
|
|
"\r\n", // end headers, begin message body
|
|
fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n",
|
|
fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
|
|
"\r\n",
|
|
client.t("To verify your account, issue one of these commands:") + "\r\n",
|
|
fmt.Sprintf("/ACC VERIFY %s %s", casefoldedAccount, code) + "\r\n",
|
|
fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
|
|
}
|
|
|
|
var message []byte
|
|
for i := 0; i < len(messageStrings); i++ {
|
|
message = append(message, []byte(messageStrings[i])...)
|
|
}
|
|
addr := fmt.Sprintf("%s:%d", config.Server, config.Port)
|
|
var auth smtp.Auth
|
|
if config.Username != "" && config.Password != "" {
|
|
auth = smtp.PlainAuth("", config.Username, config.Password, config.Server)
|
|
}
|
|
|
|
// TODO: this will never send the password in plaintext over a nonlocal link,
|
|
// but it might send the email in plaintext, regardless of the value of
|
|
// config.TLS.InsecureSkipVerify
|
|
err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
|
|
if err != nil {
|
|
am.server.logger.Error("internal", fmt.Sprintf("Failed to dispatch e-mail: %v", err))
|
|
}
|
|
return
|
|
}
|
|
|
|
func (am *AccountManager) Verify(client *Client, account string, code string) error {
|
|
casefoldedAccount, err := CasefoldName(account)
|
|
if err != nil || account == "" || account == "*" {
|
|
return errAccountVerificationFailed
|
|
}
|
|
|
|
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
|
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
|
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
|
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
|
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
|
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
|
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
|
|
|
var raw rawClientAccount
|
|
|
|
func() {
|
|
am.serialCacheUpdateMutex.Lock()
|
|
defer am.serialCacheUpdateMutex.Unlock()
|
|
|
|
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
|
raw, err = am.loadRawAccount(tx, casefoldedAccount)
|
|
if err == errAccountDoesNotExist {
|
|
return errAccountDoesNotExist
|
|
} else if err != nil {
|
|
return errAccountVerificationFailed
|
|
} else if raw.Verified {
|
|
return errAccountAlreadyVerified
|
|
}
|
|
|
|
// actually verify the code
|
|
// a stored code of "" means a none callback / no code required
|
|
success := false
|
|
storedCode, err := tx.Get(verificationCodeKey)
|
|
if err == nil {
|
|
// this is probably unnecessary
|
|
if storedCode == "" || subtle.ConstantTimeCompare([]byte(code), []byte(storedCode)) == 1 {
|
|
success = true
|
|
}
|
|
}
|
|
if !success {
|
|
return errAccountVerificationInvalidCode
|
|
}
|
|
|
|
// verify the account
|
|
tx.Set(verifiedKey, "1", nil)
|
|
// don't need the code anymore
|
|
tx.Delete(verificationCodeKey)
|
|
// re-set all other keys, removing the TTL
|
|
tx.Set(accountKey, "1", nil)
|
|
tx.Set(accountNameKey, raw.Name, nil)
|
|
tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
|
|
tx.Set(callbackKey, raw.Callback, nil)
|
|
tx.Set(credentialsKey, raw.Credentials, nil)
|
|
|
|
var creds AccountCredentials
|
|
// XXX we shouldn't do (de)serialization inside the txn,
|
|
// but this is like 2 usec on my system
|
|
json.Unmarshal([]byte(raw.Credentials), &creds)
|
|
if creds.Certificate != "" {
|
|
certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
|
|
tx.Set(certFPKey, casefoldedAccount, nil)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err == nil {
|
|
am.Lock()
|
|
am.nickToAccount[casefoldedAccount] = casefoldedAccount
|
|
am.Unlock()
|
|
}
|
|
}()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
am.Login(client, raw.Name)
|
|
return nil
|
|
}
|
|
|
|
func marshalReservedNicks(nicks []string) string {
|
|
return strings.Join(nicks, ",")
|
|
}
|
|
|
|
func unmarshalReservedNicks(nicks string) (result []string) {
|
|
if nicks == "" {
|
|
return
|
|
}
|
|
return strings.Split(nicks, ",")
|
|
}
|
|
|
|
func (am *AccountManager) SetNickReserved(client *Client, nick string, saUnreserve bool, reserve bool) error {
|
|
cfnick, err := CasefoldName(nick)
|
|
// garbage nick, or garbage options, or disabled
|
|
nrconfig := am.server.AccountConfig().NickReservation
|
|
if err != nil || cfnick == "" || (reserve && saUnreserve) || !nrconfig.Enabled {
|
|
return errAccountNickReservationFailed
|
|
}
|
|
|
|
// the cache is in sync with the DB while we hold serialCacheUpdateMutex
|
|
am.serialCacheUpdateMutex.Lock()
|
|
defer am.serialCacheUpdateMutex.Unlock()
|
|
|
|
// find the affected account, which is usually the client's:
|
|
account := client.Account()
|
|
if saUnreserve {
|
|
// unless this is a sadrop:
|
|
account = am.NickToAccount(cfnick)
|
|
if account == "" {
|
|
// nothing to do
|
|
return nil
|
|
}
|
|
}
|
|
if account == "" {
|
|
return errAccountNotLoggedIn
|
|
}
|
|
|
|
accountForNick := am.NickToAccount(cfnick)
|
|
if reserve && accountForNick != "" {
|
|
return errNicknameReserved
|
|
} else if !reserve && !saUnreserve && accountForNick != account {
|
|
return errNicknameReserved
|
|
} else if !reserve && cfnick == account {
|
|
return errAccountCantDropPrimaryNick
|
|
}
|
|
|
|
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, account)
|
|
unverifiedAccountKey := fmt.Sprintf(keyAccountExists, cfnick)
|
|
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
|
if reserve {
|
|
// unverified accounts don't show up in NickToAccount yet (which is intentional),
|
|
// however you shouldn't be able to reserve a nick out from under them
|
|
_, err := tx.Get(unverifiedAccountKey)
|
|
if err == nil {
|
|
return errNicknameReserved
|
|
}
|
|
}
|
|
|
|
rawNicks, err := tx.Get(nicksKey)
|
|
if err != nil && err != buntdb.ErrNotFound {
|
|
return err
|
|
}
|
|
|
|
nicks := unmarshalReservedNicks(rawNicks)
|
|
|
|
if reserve {
|
|
if len(nicks) >= nrconfig.AdditionalNickLimit {
|
|
return errAccountTooManyNicks
|
|
}
|
|
nicks = append(nicks, cfnick)
|
|
} else {
|
|
var newNicks []string
|
|
for _, reservedNick := range nicks {
|
|
if reservedNick != cfnick {
|
|
newNicks = append(newNicks, reservedNick)
|
|
}
|
|
}
|
|
nicks = newNicks
|
|
}
|
|
|
|
marshaledNicks := marshalReservedNicks(nicks)
|
|
_, _, err = tx.Set(nicksKey, string(marshaledNicks), nil)
|
|
return err
|
|
})
|
|
|
|
if err == errAccountTooManyNicks || err == errNicknameReserved {
|
|
return err
|
|
} else if err != nil {
|
|
return errAccountNickReservationFailed
|
|
}
|
|
|
|
// success
|
|
am.Lock()
|
|
defer am.Unlock()
|
|
if reserve {
|
|
am.nickToAccount[cfnick] = account
|
|
} else {
|
|
delete(am.nickToAccount, cfnick)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error {
|
|
account, err := am.LoadAccount(accountName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !account.Verified {
|
|
return errAccountUnverified
|
|
}
|
|
|
|
if account.Credentials.PassphraseIsV2 {
|
|
err = passwd.ComparePassword(account.Credentials.PassphraseHash, []byte(passphrase))
|
|
} else {
|
|
// compare using legacy method
|
|
err = am.server.passwords.CompareHashAndPassword(account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase)
|
|
if err == nil {
|
|
// passphrase worked! silently upgrade them to use v2 hashing going forward.
|
|
//TODO(dan): in future, replace this with an am.updatePassphrase(blah) function, which we can reuse in /ns update pass?
|
|
err = am.server.store.Update(func(tx *buntdb.Tx) error {
|
|
var creds AccountCredentials
|
|
creds.Certificate = account.Credentials.Certificate
|
|
creds.PassphraseHash, err = passwd.GenerateEncodedPasswordBytes(passphrase)
|
|
creds.PassphraseIsV2 = true
|
|
if err != nil {
|
|
am.server.logger.Error("internal", fmt.Sprintf("could not hash password (updating existing hash version): %v", err))
|
|
return errAccountCredUpdate
|
|
}
|
|
|
|
credText, err := json.Marshal(creds)
|
|
if err != nil {
|
|
am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials (updating existing hash version): %v", err))
|
|
return errAccountCredUpdate
|
|
}
|
|
credStr := string(credText)
|
|
|
|
// we know the account name is valid if this line is reached, otherwise the
|
|
// above would have failed. as such, chuck out and ignore err on casefolding
|
|
casefoldedAccountName, _ := CasefoldName(accountName)
|
|
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccountName)
|
|
|
|
//TODO(dan): sling, can you please checkout this mutex usage, see if it
|
|
// makes sense or not? bleh
|
|
am.serialCacheUpdateMutex.Lock()
|
|
defer am.serialCacheUpdateMutex.Unlock()
|
|
|
|
tx.Set(credentialsKey, credStr, nil)
|
|
|
|
return nil
|
|
})
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err != nil {
|
|
return errAccountInvalidCredentials
|
|
}
|
|
|
|
am.Login(client, account.Name)
|
|
return nil
|
|
}
|
|
|
|
func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {
|
|
casefoldedAccount, err := CasefoldName(accountName)
|
|
if err != nil {
|
|
err = errAccountDoesNotExist
|
|
return
|
|
}
|
|
|
|
var raw rawClientAccount
|
|
am.server.store.View(func(tx *buntdb.Tx) error {
|
|
raw, err = am.loadRawAccount(tx, casefoldedAccount)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
result.Name = raw.Name
|
|
regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
|
|
result.RegisteredAt = time.Unix(regTimeInt, 0)
|
|
e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
|
|
if e != nil {
|
|
am.server.logger.Error("internal", fmt.Sprintf("could not unmarshal credentials: %v", e))
|
|
err = errAccountDoesNotExist
|
|
return
|
|
}
|
|
result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
|
|
result.Verified = raw.Verified
|
|
return
|
|
}
|
|
|
|
func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string) (result rawClientAccount, err error) {
|
|
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
|
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
|
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
|
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
|
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
|
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
|
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
|
|
|
_, e := tx.Get(accountKey)
|
|
if e == buntdb.ErrNotFound {
|
|
err = errAccountDoesNotExist
|
|
return
|
|
}
|
|
|
|
result.Name, _ = tx.Get(accountNameKey)
|
|
result.RegisteredAt, _ = tx.Get(registeredTimeKey)
|
|
result.Credentials, _ = tx.Get(credentialsKey)
|
|
result.Callback, _ = tx.Get(callbackKey)
|
|
result.AdditionalNicks, _ = tx.Get(nicksKey)
|
|
|
|
if _, e = tx.Get(verifiedKey); e == nil {
|
|
result.Verified = true
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (am *AccountManager) Unregister(account string) error {
|
|
casefoldedAccount, err := CasefoldName(account)
|
|
if err != nil {
|
|
return errAccountDoesNotExist
|
|
}
|
|
|
|
accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
|
|
accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
|
|
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
|
|
credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
|
|
callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
|
|
verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
|
|
verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
|
|
nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
|
|
|
|
var clients []*Client
|
|
|
|
var credText string
|
|
var rawNicks string
|
|
|
|
am.serialCacheUpdateMutex.Lock()
|
|
defer am.serialCacheUpdateMutex.Unlock()
|
|
|
|
am.server.store.Update(func(tx *buntdb.Tx) error {
|
|
tx.Delete(accountKey)
|
|
tx.Delete(accountNameKey)
|
|
tx.Delete(verifiedKey)
|
|
tx.Delete(registeredTimeKey)
|
|
tx.Delete(callbackKey)
|
|
tx.Delete(verificationCodeKey)
|
|
rawNicks, _ = tx.Get(nicksKey)
|
|
tx.Delete(nicksKey)
|
|
credText, err = tx.Get(credentialsKey)
|
|
tx.Delete(credentialsKey)
|
|
return nil
|
|
})
|
|
|
|
if err == nil {
|
|
var creds AccountCredentials
|
|
if err = json.Unmarshal([]byte(credText), &creds); err == nil && creds.Certificate != "" {
|
|
certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
|
|
am.server.store.Update(func(tx *buntdb.Tx) error {
|
|
if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
|
|
tx.Delete(certFPKey)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
additionalNicks := unmarshalReservedNicks(rawNicks)
|
|
|
|
am.Lock()
|
|
defer am.Unlock()
|
|
|
|
clients = am.accountToClients[casefoldedAccount]
|
|
delete(am.accountToClients, casefoldedAccount)
|
|
delete(am.nickToAccount, casefoldedAccount)
|
|
for _, nick := range additionalNicks {
|
|
delete(am.nickToAccount, nick)
|
|
}
|
|
for _, client := range clients {
|
|
am.logoutOfAccount(client)
|
|
}
|
|
|
|
if err != nil {
|
|
return errAccountDoesNotExist
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
|
|
if client.certfp == "" {
|
|
return errAccountInvalidCredentials
|
|
}
|
|
|
|
var account string
|
|
var rawAccount rawClientAccount
|
|
certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)
|
|
|
|
err := am.server.store.Update(func(tx *buntdb.Tx) error {
|
|
var err error
|
|
account, _ = tx.Get(certFPKey)
|
|
if account == "" {
|
|
return errAccountInvalidCredentials
|
|
}
|
|
rawAccount, err = am.loadRawAccount(tx, account)
|
|
if err != nil || !rawAccount.Verified {
|
|
return errAccountUnverified
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// ok, we found an account corresponding to their certificate
|
|
|
|
am.Login(client, rawAccount.Name)
|
|
return nil
|
|
}
|
|
|
|
func (am *AccountManager) Login(client *Client, account string) {
|
|
am.Lock()
|
|
defer am.Unlock()
|
|
|
|
am.loginToAccount(client, account)
|
|
casefoldedAccount := client.Account()
|
|
am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
|
|
}
|
|
|
|
func (am *AccountManager) Logout(client *Client) {
|
|
am.Lock()
|
|
defer am.Unlock()
|
|
|
|
casefoldedAccount := client.Account()
|
|
if casefoldedAccount == "" {
|
|
return
|
|
}
|
|
|
|
am.logoutOfAccount(client)
|
|
|
|
clients := am.accountToClients[casefoldedAccount]
|
|
if len(clients) <= 1 {
|
|
delete(am.accountToClients, casefoldedAccount)
|
|
return
|
|
}
|
|
remainingClients := make([]*Client, len(clients)-1)
|
|
remainingPos := 0
|
|
for currentPos := 0; currentPos < len(clients); currentPos++ {
|
|
if clients[currentPos] != client {
|
|
remainingClients[remainingPos] = clients[currentPos]
|
|
remainingPos++
|
|
}
|
|
}
|
|
am.accountToClients[casefoldedAccount] = remainingClients
|
|
return
|
|
}
|
|
|
|
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,
|
|
}
|
|
)
|
|
|
|
// AccountCredentials stores the various methods for verifying accounts.
|
|
type AccountCredentials struct {
|
|
PassphraseSalt []byte
|
|
PassphraseHash []byte
|
|
PassphraseIsV2 bool `json:"passphrase-is-v2"`
|
|
Certificate string // fingerprint
|
|
}
|
|
|
|
// ClientAccount represents a user account.
|
|
type ClientAccount struct {
|
|
// Name of the account.
|
|
Name string
|
|
// RegisteredAt represents the time that the account was registered.
|
|
RegisteredAt time.Time
|
|
Credentials AccountCredentials
|
|
Verified bool
|
|
AdditionalNicks []string
|
|
}
|
|
|
|
// convenience for passing around raw serialized account data
|
|
type rawClientAccount struct {
|
|
Name string
|
|
RegisteredAt string
|
|
Credentials string
|
|
Callback string
|
|
Verified bool
|
|
AdditionalNicks string
|
|
}
|
|
|
|
// loginToAccount logs the client into the given account.
|
|
func (am *AccountManager) loginToAccount(client *Client, account string) {
|
|
changed := client.SetAccountName(account)
|
|
if changed {
|
|
go client.nickTimer.Touch()
|
|
}
|
|
}
|
|
|
|
// logoutOfAccount logs the client out of their current account.
|
|
func (am *AccountManager) logoutOfAccount(client *Client) {
|
|
if client.Account() == "" {
|
|
// already logged out
|
|
return
|
|
}
|
|
|
|
client.SetAccountName("")
|
|
go client.nickTimer.Touch()
|
|
|
|
// dispatch account-notify
|
|
// TODO: doing the I/O here is kind of a kludge, let's move this somewhere else
|
|
go func() {
|
|
for friend := range client.Friends(caps.AccountNotify) {
|
|
friend.Send(nil, client.NickMaskString(), "ACCOUNT", "*")
|
|
}
|
|
}()
|
|
}
|