123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- // Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
- // released under the MIT license
-
- package irc
-
- import (
- "encoding/json"
- "fmt"
- "strconv"
- "strings"
- "time"
-
- "github.com/goshuirc/irc-go/ircfmt"
- "github.com/oragono/oragono/irc/passwd"
- "github.com/oragono/oragono/irc/sno"
- "github.com/tidwall/buntdb"
- )
-
- const nickservHelp = `NickServ lets you register and log into a user account.
-
- To register an account:
- /NS REGISTER username [password]
- Leave out [password] if you're registering using your client certificate fingerprint.
-
- To login to an account:
- /NS IDENTIFY [username password]
- Leave out [username password] to use your client certificate fingerprint. Otherwise,
- the given username and password will be used.`
-
- func (server *Server) nickservReceiveNotice(client *Client, message string) {
- // do nothing
- }
-
- // extractParam extracts a parameter from the given string, returning the param and the rest of the string.
- func extractParam(line string) (string, string) {
- rawParams := strings.SplitN(strings.TrimSpace(line), " ", 2)
- param0 := rawParams[0]
- var param1 string
- if 1 < len(rawParams) {
- param1 = strings.TrimSpace(rawParams[1])
- }
- return param0, param1
- }
-
- func (server *Server) nickservReceivePrivmsg(client *Client, message string) {
- command, params := extractParam(message)
- command = strings.ToLower(command)
-
- if command == "help" {
- for _, line := range strings.Split(nickservHelp, "\n") {
- client.Notice(line)
- }
- } else if command == "register" {
- // get params
- username, passphrase := extractParam(params)
-
- // fail out if we need to
- if username == "" {
- client.Notice(client.t("No username supplied"))
- return
- }
-
- certfp := client.certfp
- if passphrase == "" && certfp == "" {
- client.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert"))
- return
- }
-
- if !server.accountRegistration.Enabled {
- client.Notice(client.t("Account registration has been disabled"))
- return
- }
-
- if client.LoggedIntoAccount() {
- if server.accountRegistration.AllowMultiplePerConnection {
- client.LogoutOfAccount()
- } else {
- client.Notice(client.t("You're already logged into an account"))
- return
- }
- }
-
- // get and sanitise account name
- account := strings.TrimSpace(username)
- casefoldedAccount, err := CasefoldName(account)
- // probably don't need explicit check for "*" here... but let's do it anyway just to make sure
- if err != nil || username == "*" {
- client.Notice(client.t("Account name is not valid"))
- return
- }
-
- // check whether account exists
- // do it all in one write tx to prevent races
- err = server.store.Update(func(tx *buntdb.Tx) error {
- accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
-
- _, err := tx.Get(accountKey)
- if err != buntdb.ErrNotFound {
- //TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be
- client.Notice(client.t("Account already exists"))
- return errAccountCreation
- }
-
- registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
-
- tx.Set(accountKey, "1", nil)
- tx.Set(fmt.Sprintf(keyAccountName, casefoldedAccount), account, nil)
- tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil)
- return nil
- })
-
- // account could not be created and relevant numerics have been dispatched, abort
- if err != nil {
- if err != errAccountCreation {
- client.Notice(client.t("Account registration failed"))
- }
- return
- }
-
- // store details
- err = server.store.Update(func(tx *buntdb.Tx) error {
- // certfp special lookup key
- if passphrase == "" {
- assembledKeyCertToAccount := fmt.Sprintf(keyCertToAccount, client.certfp)
-
- // make sure certfp doesn't already exist because that'd be silly
- _, err := tx.Get(assembledKeyCertToAccount)
- if err != buntdb.ErrNotFound {
- return errCertfpAlreadyExists
- }
-
- tx.Set(assembledKeyCertToAccount, casefoldedAccount, nil)
- }
-
- // make creds
- var creds AccountCredentials
-
- // always set passphrase salt
- creds.PassphraseSalt, err = passwd.NewSalt()
- if err != nil {
- return fmt.Errorf("Could not create passphrase salt: %s", err.Error())
- }
-
- if passphrase == "" {
- creds.Certificate = client.certfp
- } else {
- creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase)
- if err != nil {
- return fmt.Errorf("Could not hash password: %s", err)
- }
- }
- credText, err := json.Marshal(creds)
- if err != nil {
- return fmt.Errorf("Could not marshal creds: %s", err)
- }
- tx.Set(fmt.Sprintf(keyAccountCredentials, account), string(credText), nil)
-
- return nil
- })
-
- // details could not be stored and relevant numerics have been dispatched, abort
- if err != nil {
- errMsg := "Could not register"
- if err == errCertfpAlreadyExists {
- errMsg = "An account already exists for your certificate fingerprint"
- }
- client.Notice(errMsg)
- removeFailedAccRegisterData(server.store, casefoldedAccount)
- return
- }
-
- err = server.store.Update(func(tx *buntdb.Tx) error {
- tx.Set(fmt.Sprintf(keyAccountVerified, casefoldedAccount), "1", nil)
-
- // load acct info inside store tx
- account := ClientAccount{
- Name: username,
- RegisteredAt: time.Now(),
- Clients: []*Client{client},
- }
- //TODO(dan): Consider creating ircd-wide account adding/removing/affecting lock for protecting access to these sorts of variables
- server.accounts[casefoldedAccount] = &account
- client.account = &account
-
- client.Notice(client.t("Account created"))
- client.Send(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account.Name, fmt.Sprintf(client.t("You are now logged in as %s"), account.Name))
- client.Send(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
- server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), account.Name, client.nickMaskString))
- return nil
- })
- if err != nil {
- client.Notice(client.t("Account registration failed"))
- removeFailedAccRegisterData(server.store, casefoldedAccount)
- return
- }
-
- } else if command == "identify" {
- // fail out if we need to
- if !server.accountAuthenticationEnabled {
- client.Notice(client.t("Login has been disabled"))
- return
- }
-
- // try passphrase
- username, passphrase := extractParam(params)
- if username != "" && passphrase != "" {
- // keep it the same as in the ACC CREATE stage
- accountKey, err := CasefoldName(username)
- if err != nil {
- client.Notice(client.t("Could not login with your username/password"))
- return
- }
-
- // load and check acct data all in one update to prevent races.
- // as noted elsewhere, change to proper locking for Account type later probably
- var accountName string
- err = server.store.Update(func(tx *buntdb.Tx) error {
- // confirm account is verified
- _, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey))
- if err != nil {
- return errSaslFail
- }
-
- creds, err := loadAccountCredentials(tx, accountKey)
- if err != nil {
- return err
- }
-
- // ensure creds are valid
- if len(creds.PassphraseHash) < 1 || len(creds.PassphraseSalt) < 1 || len(passphrase) < 1 {
- return errSaslFail
- }
- err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, passphrase)
-
- // succeeded, load account info if necessary
- account, exists := server.accounts[accountKey]
- if !exists {
- account = loadAccount(server, tx, accountKey)
- }
-
- client.LoginToAccount(account)
- accountName = account.Name
-
- return err
- })
-
- if err == nil {
- client.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName))
- return
- }
- }
-
- // try certfp
- certfp := client.certfp
- if certfp != "" {
- var accountName string
- err := server.store.Update(func(tx *buntdb.Tx) error {
- // certfp lookup key
- accountKey, err := tx.Get(fmt.Sprintf(keyCertToAccount, certfp))
- if err != nil {
- return errSaslFail
- }
-
- // confirm account exists
- _, err = tx.Get(fmt.Sprintf(keyAccountExists, accountKey))
- if err != nil {
- return errSaslFail
- }
-
- // confirm account is verified
- _, err = tx.Get(fmt.Sprintf(keyAccountVerified, 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)
- }
-
- client.LoginToAccount(account)
- accountName = account.Name
-
- return nil
- })
-
- if err == nil {
- client.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName))
- return
- }
- }
-
- client.Notice(client.t("Could not login with your TLS certificate or supplied username/password"))
- } else {
- client.Notice(client.t("Command not recognised. To see the available commands, run /NS HELP"))
- }
- }
|