diff --git a/irc/database.go b/irc/database.go index 6a84901d..407a2a79 100644 --- a/irc/database.go +++ b/irc/database.go @@ -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 '', diff --git a/irc/password_new.go b/irc/password_new.go new file mode 100644 index 00000000..a3957315 --- /dev/null +++ b/irc/password_new.go @@ -0,0 +1,62 @@ +// Copyright (c) 2016- Daniel Oaks +// 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) +} diff --git a/irc/registration.go b/irc/registration.go index 5197048b..0424cb8b 100644 --- a/irc/registration.go +++ b/irc/registration.go @@ -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 } diff --git a/irc/server.go b/irc/server.go index 5f851da1..5536060e 100644 --- a/irc/server.go +++ b/irc/server.go @@ -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 { diff --git a/oragono.go b/oragono.go index 5ce0f8e2..362d2ab2 100644 --- a/oragono.go +++ b/oragono.go @@ -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)