registration: Add new password manager, integrate registration and credential types

This commit is contained in:
Daniel Oaks 2016-09-05 18:45:42 +10:00
parent 68c83a95d4
commit 6e96a175d6
5 changed files with 196 additions and 16 deletions

View File

@ -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
View 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)
}

View File

@ -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
} }

View File

@ -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 {

View File

@ -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)