mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 18:52:41 +01:00
registration: Add new password manager, integrate registration and credential types
This commit is contained in:
parent
68c83a95d4
commit
6e96a175d6
@ -5,18 +5,48 @@ package irc
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
func InitDB(path string) {
|
||||
const (
|
||||
// key for the primary salt used by the ircd
|
||||
keySalt = "crypto.salt"
|
||||
)
|
||||
|
||||
func InitDB(buntpath string, path string) {
|
||||
// prepare kvstore db
|
||||
os.Remove(buntpath)
|
||||
store, err := buntdb.Open(buntpath)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed to open datastore: %s", err.Error()))
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
err = store.Update(func(tx *buntdb.Tx) error {
|
||||
salt, err := NewSalt()
|
||||
encodedSalt := base64.StdEncoding.EncodeToString(salt)
|
||||
if err != nil {
|
||||
log.Fatal("Could not generate cryptographically-secure salt for the user:", err.Error())
|
||||
}
|
||||
tx.Set(keySalt, encodedSalt, nil)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Could not save bunt store:", err.Error())
|
||||
}
|
||||
|
||||
// prepare SQLite db
|
||||
os.Remove(path)
|
||||
db := OpenDB(path)
|
||||
defer db.Close()
|
||||
_, err := db.Exec(`
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE channel (
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
flags TEXT DEFAULT '',
|
||||
|
62
irc/password_new.go
Normal file
62
irc/password_new.go
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const newSaltLen = 30
|
||||
const defaultPasswordCost = 14
|
||||
|
||||
// NewSalt returns a salt for crypto uses.
|
||||
func NewSalt() ([]byte, error) {
|
||||
salt := make([]byte, newSaltLen)
|
||||
_, err := rand.Read(salt)
|
||||
|
||||
if err != nil {
|
||||
var emptySalt []byte
|
||||
return emptySalt, err
|
||||
}
|
||||
|
||||
return salt, nil
|
||||
}
|
||||
|
||||
// PasswordManager supports the hashing and comparing of passwords with the given salt.
|
||||
type PasswordManager struct {
|
||||
salt []byte
|
||||
}
|
||||
|
||||
// NewPasswordManager returns a new PasswordManager with the given salt.
|
||||
func NewPasswordManager(salt []byte) PasswordManager {
|
||||
var pwm PasswordManager
|
||||
pwm.salt = salt
|
||||
return pwm
|
||||
}
|
||||
|
||||
// assemblePassword returns an assembled slice of bytes for the given password details.
|
||||
func (pwm *PasswordManager) assemblePassword(specialSalt []byte, password string) []byte {
|
||||
var assembledPasswordBytes []byte
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, pwm.salt...)
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, '-')
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, specialSalt...)
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, '-')
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, []byte(password)...)
|
||||
return assembledPasswordBytes
|
||||
}
|
||||
|
||||
// GenerateFromPassword encrypts the given password.
|
||||
func (pwm *PasswordManager) GenerateFromPassword(specialSalt []byte, password string) ([]byte, error) {
|
||||
assembledPasswordBytes := pwm.assemblePassword(specialSalt, password)
|
||||
return bcrypt.GenerateFromPassword(assembledPasswordBytes, defaultPasswordCost)
|
||||
}
|
||||
|
||||
// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
|
||||
// Returns nil on success, or an error on failure.
|
||||
func (pwm *PasswordManager) CompareHashAndPassword(hashedPassword []byte, specialSalt []byte, password string) error {
|
||||
assembledPasswordBytes := pwm.assemblePassword(specialSalt, password)
|
||||
return bcrypt.CompareHashAndPassword(hashedPassword, assembledPasswordBytes)
|
||||
}
|
@ -4,8 +4,10 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -14,6 +16,12 @@ import (
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
keyAccountExists = "account %s exists"
|
||||
keyAccountRegTime = "account %s registered.time"
|
||||
keyAccountCredentials = "account %s credentials"
|
||||
)
|
||||
|
||||
var (
|
||||
errAccountCreation = errors.New("Account could not be created")
|
||||
)
|
||||
@ -25,6 +33,13 @@ type AccountRegistration struct {
|
||||
EnabledCredentialTypes []string
|
||||
}
|
||||
|
||||
// AccountCredentials stores the various methods for verifying accounts.
|
||||
type AccountCredentials struct {
|
||||
PassphraseSalt []byte
|
||||
PassphraseHash []byte
|
||||
Certificate string // fingerprint
|
||||
}
|
||||
|
||||
// NewAccountRegistration returns a new AccountRegistration, configured correctly.
|
||||
func NewAccountRegistration(config AccountRegistrationConfig) (accountReg AccountRegistration) {
|
||||
if config.Enabled {
|
||||
@ -60,6 +75,18 @@ func regHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// removeFailedRegCreateData removes the data created by REG CREATE if the account creation fails early.
|
||||
func removeFailedRegCreateData(store buntdb.DB, account string) {
|
||||
// error is ignored here, we can't do much about it anyways
|
||||
store.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Delete(fmt.Sprintf(keyAccountExists, account))
|
||||
tx.Delete(fmt.Sprintf(keyAccountRegTime, account))
|
||||
tx.Delete(fmt.Sprintf(keyAccountCredentials, account))
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// regCreateHandler parses the REG CREATE command.
|
||||
func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
client.Notice("Parsing CREATE")
|
||||
@ -75,7 +102,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
|
||||
// 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("account %s exists", accountString)
|
||||
accountKey := fmt.Sprintf(keyAccountExists, accountString)
|
||||
|
||||
_, err := tx.Get(accountKey)
|
||||
if err != buntdb.ErrNotFound {
|
||||
@ -84,7 +111,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
|
||||
return errAccountCreation
|
||||
}
|
||||
|
||||
registeredTimeKey := fmt.Sprintf("account %s registered.time", accountString)
|
||||
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, accountString)
|
||||
|
||||
tx.Set(accountKey, "1", nil)
|
||||
tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil)
|
||||
@ -121,7 +148,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
|
||||
|
||||
if !callbackValid {
|
||||
client.Send(nil, server.nameString, ERR_REG_INVALID_CALLBACK, client.nickString, msg.Params[1], callbackNamespace, "Callback namespace is not supported")
|
||||
//TODO(dan): close out failed account reg (remove values from db)
|
||||
removeFailedRegCreateData(server.store, accountString)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -136,7 +163,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
|
||||
credentialValue = msg.Params[3]
|
||||
} else {
|
||||
client.Send(nil, server.nameString, ERR_NEEDMOREPARAMS, client.nickString, msg.Command, "Not enough parameters")
|
||||
//TODO(dan): close out failed account reg (remove values from db)
|
||||
removeFailedRegCreateData(server.store, accountString)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -147,22 +174,61 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
|
||||
credentialValid = true
|
||||
}
|
||||
}
|
||||
if credentialType == "certfp" && client.certfp == "" {
|
||||
client.Send(nil, server.nameString, ERR_REG_INVALID_CRED_TYPE, client.nickString, credentialType, callbackNamespace, "You are not using a certificiate")
|
||||
removeFailedRegCreateData(server.store, accountString)
|
||||
return false
|
||||
}
|
||||
|
||||
if !credentialValid {
|
||||
client.Send(nil, server.nameString, ERR_REG_INVALID_CRED_TYPE, client.nickString, credentialType, callbackNamespace, "Credential type is not supported")
|
||||
//TODO(dan): close out failed account reg (remove values from db)
|
||||
removeFailedRegCreateData(server.store, accountString)
|
||||
return false
|
||||
}
|
||||
|
||||
// store details
|
||||
err = server.store.Update(func(tx *buntdb.Tx) error {
|
||||
var creds AccountCredentials
|
||||
|
||||
// always set passphrase salt
|
||||
creds.PassphraseSalt, err = NewSalt()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not create passphrase salt: %s", err.Error())
|
||||
}
|
||||
|
||||
if credentialType == "certfp" {
|
||||
creds.Certificate = client.certfp
|
||||
} else if credentialType == "passphrase" {
|
||||
creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, credentialValue)
|
||||
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(keyAccountCredentials, string(credText), nil)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// details could not be stored and relevant numerics have been dispatched, abort
|
||||
if err != nil {
|
||||
client.Send(nil, server.nameString, ERR_UNKNOWNERROR, client.nickString, "REG", "CREATE", "Could not register")
|
||||
log.Println("Could not save registration creds:", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
// automatically complete registration
|
||||
if callbackNamespace != "*" {
|
||||
client.Notice("Account creation was successful!")
|
||||
removeFailedRegCreateData(server.store, accountString)
|
||||
return false
|
||||
}
|
||||
|
||||
// dispatch callback
|
||||
if callbackNamespace != "*" {
|
||||
client.Notice("Account creation was successful!")
|
||||
//TODO(dan): close out failed account reg (remove values from db)
|
||||
return false
|
||||
}
|
||||
|
||||
client.Notice(fmt.Sprintf("We should dispatch an actual callback here to %s:%s", callbackNamespace, callbackValue))
|
||||
client.Notice(fmt.Sprintf("Primary account credential is with %s:%s", credentialType, credentialValue))
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@ -40,6 +41,7 @@ type Server struct {
|
||||
newConns chan clientConn
|
||||
operators map[Name][]byte
|
||||
password []byte
|
||||
passwords *PasswordManager
|
||||
accountRegistration *AccountRegistration
|
||||
signals chan os.Signal
|
||||
proxyAllowedFrom []string
|
||||
@ -91,6 +93,26 @@ func NewServer(config *Config) *Server {
|
||||
defer db.Close()
|
||||
server.store = *db
|
||||
|
||||
// load password manager
|
||||
err = server.store.View(func(tx *buntdb.Tx) error {
|
||||
saltString, err := tx.Get(keySalt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Could not retrieve salt string: %s", err.Error())
|
||||
}
|
||||
|
||||
salt, err := base64.StdEncoding.DecodeString(saltString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pwm := NewPasswordManager(salt)
|
||||
server.passwords = &pwm
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Could not load salt: %s", err.Error()))
|
||||
}
|
||||
|
||||
if config.Server.MOTD != "" {
|
||||
file, err := os.Open(config.Server.MOTD)
|
||||
if err == nil {
|
||||
|
@ -54,8 +54,8 @@ Options:
|
||||
fmt.Print("\n")
|
||||
fmt.Println(encoded)
|
||||
} else if arguments["initdb"].(bool) {
|
||||
irc.InitDB(config.Datastore.SQLitePath)
|
||||
log.Println("database initialized: ", config.Datastore.SQLitePath)
|
||||
irc.InitDB(config.Datastore.Path, config.Datastore.SQLitePath)
|
||||
log.Println("databases initialized: ", config.Datastore.Path, config.Datastore.SQLitePath)
|
||||
} else if arguments["upgradedb"].(bool) {
|
||||
irc.UpgradeDB(config.Datastore.SQLitePath)
|
||||
log.Println("database upgraded: ", config.Datastore.SQLitePath)
|
||||
|
Loading…
Reference in New Issue
Block a user