mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-23 03:02:48 +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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "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)
|
os.Remove(path)
|
||||||
db := OpenDB(path)
|
db := OpenDB(path)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
_, err := db.Exec(`
|
_, err = db.Exec(`
|
||||||
CREATE TABLE channel (
|
CREATE TABLE channel (
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
flags TEXT DEFAULT '',
|
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
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -14,6 +16,12 @@ import (
|
|||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyAccountExists = "account %s exists"
|
||||||
|
keyAccountRegTime = "account %s registered.time"
|
||||||
|
keyAccountCredentials = "account %s credentials"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errAccountCreation = errors.New("Account could not be created")
|
errAccountCreation = errors.New("Account could not be created")
|
||||||
)
|
)
|
||||||
@ -25,6 +33,13 @@ type AccountRegistration struct {
|
|||||||
EnabledCredentialTypes []string
|
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.
|
// NewAccountRegistration returns a new AccountRegistration, configured correctly.
|
||||||
func NewAccountRegistration(config AccountRegistrationConfig) (accountReg AccountRegistration) {
|
func NewAccountRegistration(config AccountRegistrationConfig) (accountReg AccountRegistration) {
|
||||||
if config.Enabled {
|
if config.Enabled {
|
||||||
@ -60,6 +75,18 @@ func regHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
|||||||
return false
|
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.
|
// regCreateHandler parses the REG CREATE command.
|
||||||
func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||||
client.Notice("Parsing CREATE")
|
client.Notice("Parsing CREATE")
|
||||||
@ -75,7 +102,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
|
|||||||
// check whether account exists
|
// check whether account exists
|
||||||
// do it all in one write tx to prevent races
|
// do it all in one write tx to prevent races
|
||||||
err := server.store.Update(func(tx *buntdb.Tx) error {
|
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)
|
_, err := tx.Get(accountKey)
|
||||||
if err != buntdb.ErrNotFound {
|
if err != buntdb.ErrNotFound {
|
||||||
@ -84,7 +111,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
|
|||||||
return errAccountCreation
|
return errAccountCreation
|
||||||
}
|
}
|
||||||
|
|
||||||
registeredTimeKey := fmt.Sprintf("account %s registered.time", accountString)
|
registeredTimeKey := fmt.Sprintf(keyAccountRegTime, accountString)
|
||||||
|
|
||||||
tx.Set(accountKey, "1", nil)
|
tx.Set(accountKey, "1", nil)
|
||||||
tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), 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 {
|
if !callbackValid {
|
||||||
client.Send(nil, server.nameString, ERR_REG_INVALID_CALLBACK, client.nickString, msg.Params[1], callbackNamespace, "Callback namespace is not supported")
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +163,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
|
|||||||
credentialValue = msg.Params[3]
|
credentialValue = msg.Params[3]
|
||||||
} else {
|
} else {
|
||||||
client.Send(nil, server.nameString, ERR_NEEDMOREPARAMS, client.nickString, msg.Command, "Not enough parameters")
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,22 +174,61 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
|
|||||||
credentialValid = true
|
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 {
|
if !credentialValid {
|
||||||
client.Send(nil, server.nameString, ERR_REG_INVALID_CRED_TYPE, client.nickString, credentialType, callbackNamespace, "Credential type is not supported")
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispatch callback
|
// 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("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
|
return false
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
@ -40,6 +41,7 @@ type Server struct {
|
|||||||
newConns chan clientConn
|
newConns chan clientConn
|
||||||
operators map[Name][]byte
|
operators map[Name][]byte
|
||||||
password []byte
|
password []byte
|
||||||
|
passwords *PasswordManager
|
||||||
accountRegistration *AccountRegistration
|
accountRegistration *AccountRegistration
|
||||||
signals chan os.Signal
|
signals chan os.Signal
|
||||||
proxyAllowedFrom []string
|
proxyAllowedFrom []string
|
||||||
@ -91,6 +93,26 @@ func NewServer(config *Config) *Server {
|
|||||||
defer db.Close()
|
defer db.Close()
|
||||||
server.store = *db
|
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 != "" {
|
if config.Server.MOTD != "" {
|
||||||
file, err := os.Open(config.Server.MOTD)
|
file, err := os.Open(config.Server.MOTD)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -54,8 +54,8 @@ Options:
|
|||||||
fmt.Print("\n")
|
fmt.Print("\n")
|
||||||
fmt.Println(encoded)
|
fmt.Println(encoded)
|
||||||
} else if arguments["initdb"].(bool) {
|
} else if arguments["initdb"].(bool) {
|
||||||
irc.InitDB(config.Datastore.SQLitePath)
|
irc.InitDB(config.Datastore.Path, config.Datastore.SQLitePath)
|
||||||
log.Println("database initialized: ", config.Datastore.SQLitePath)
|
log.Println("databases initialized: ", config.Datastore.Path, config.Datastore.SQLitePath)
|
||||||
} else if arguments["upgradedb"].(bool) {
|
} else if arguments["upgradedb"].(bool) {
|
||||||
irc.UpgradeDB(config.Datastore.SQLitePath)
|
irc.UpgradeDB(config.Datastore.SQLitePath)
|
||||||
log.Println("database upgraded: ", config.Datastore.SQLitePath)
|
log.Println("database upgraded: ", config.Datastore.SQLitePath)
|
||||||
|
Loading…
Reference in New Issue
Block a user