Browse Source

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

tags/v0.1.0
Daniel Oaks 7 years ago
parent
commit
6e96a175d6
5 changed files with 192 additions and 12 deletions
  1. 32
    2
      irc/database.go
  2. 62
    0
      irc/password_new.go
  3. 74
    8
      irc/registration.go
  4. 22
    0
      irc/server.go
  5. 2
    2
      oragono.go

+ 32
- 2
irc/database.go View File

@@ -5,18 +5,48 @@ package irc
5 5
 
6 6
 import (
7 7
 	"database/sql"
8
+	"encoding/base64"
8 9
 	"fmt"
9 10
 	"log"
10 11
 	"os"
11 12
 
12 13
 	_ "github.com/mattn/go-sqlite3"
14
+	"github.com/tidwall/buntdb"
13 15
 )
14 16
 
15
-func InitDB(path string) {
17
+const (
18
+	// key for the primary salt used by the ircd
19
+	keySalt = "crypto.salt"
20
+)
21
+
22
+func InitDB(buntpath string, path string) {
23
+	// prepare kvstore db
24
+	os.Remove(buntpath)
25
+	store, err := buntdb.Open(buntpath)
26
+	if err != nil {
27
+		log.Fatal(fmt.Sprintf("Failed to open datastore: %s", err.Error()))
28
+	}
29
+	defer store.Close()
30
+
31
+	err = store.Update(func(tx *buntdb.Tx) error {
32
+		salt, err := NewSalt()
33
+		encodedSalt := base64.StdEncoding.EncodeToString(salt)
34
+		if err != nil {
35
+			log.Fatal("Could not generate cryptographically-secure salt for the user:", err.Error())
36
+		}
37
+		tx.Set(keySalt, encodedSalt, nil)
38
+		return nil
39
+	})
40
+
41
+	if err != nil {
42
+		log.Fatal("Could not save bunt store:", err.Error())
43
+	}
44
+
45
+	// prepare SQLite db
16 46
 	os.Remove(path)
17 47
 	db := OpenDB(path)
18 48
 	defer db.Close()
19
-	_, err := db.Exec(`
49
+	_, err = db.Exec(`
20 50
         CREATE TABLE channel (
21 51
           name TEXT NOT NULL UNIQUE,
22 52
           flags TEXT DEFAULT '',

+ 62
- 0
irc/password_new.go View File

@@ -0,0 +1,62 @@
1
+// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"crypto/rand"
8
+
9
+	"golang.org/x/crypto/bcrypt"
10
+)
11
+
12
+const newSaltLen = 30
13
+const defaultPasswordCost = 14
14
+
15
+// NewSalt returns a salt for crypto uses.
16
+func NewSalt() ([]byte, error) {
17
+	salt := make([]byte, newSaltLen)
18
+	_, err := rand.Read(salt)
19
+
20
+	if err != nil {
21
+		var emptySalt []byte
22
+		return emptySalt, err
23
+	}
24
+
25
+	return salt, nil
26
+}
27
+
28
+// PasswordManager supports the hashing and comparing of passwords with the given salt.
29
+type PasswordManager struct {
30
+	salt []byte
31
+}
32
+
33
+// NewPasswordManager returns a new PasswordManager with the given salt.
34
+func NewPasswordManager(salt []byte) PasswordManager {
35
+	var pwm PasswordManager
36
+	pwm.salt = salt
37
+	return pwm
38
+}
39
+
40
+// assemblePassword returns an assembled slice of bytes for the given password details.
41
+func (pwm *PasswordManager) assemblePassword(specialSalt []byte, password string) []byte {
42
+	var assembledPasswordBytes []byte
43
+	assembledPasswordBytes = append(assembledPasswordBytes, pwm.salt...)
44
+	assembledPasswordBytes = append(assembledPasswordBytes, '-')
45
+	assembledPasswordBytes = append(assembledPasswordBytes, specialSalt...)
46
+	assembledPasswordBytes = append(assembledPasswordBytes, '-')
47
+	assembledPasswordBytes = append(assembledPasswordBytes, []byte(password)...)
48
+	return assembledPasswordBytes
49
+}
50
+
51
+// GenerateFromPassword encrypts the given password.
52
+func (pwm *PasswordManager) GenerateFromPassword(specialSalt []byte, password string) ([]byte, error) {
53
+	assembledPasswordBytes := pwm.assemblePassword(specialSalt, password)
54
+	return bcrypt.GenerateFromPassword(assembledPasswordBytes, defaultPasswordCost)
55
+}
56
+
57
+// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
58
+// Returns nil on success, or an error on failure.
59
+func (pwm *PasswordManager) CompareHashAndPassword(hashedPassword []byte, specialSalt []byte, password string) error {
60
+	assembledPasswordBytes := pwm.assemblePassword(specialSalt, password)
61
+	return bcrypt.CompareHashAndPassword(hashedPassword, assembledPasswordBytes)
62
+}

+ 74
- 8
irc/registration.go View File

@@ -4,8 +4,10 @@
4 4
 package irc
5 5
 
6 6
 import (
7
+	"encoding/json"
7 8
 	"errors"
8 9
 	"fmt"
10
+	"log"
9 11
 	"strconv"
10 12
 	"strings"
11 13
 	"time"
@@ -14,6 +16,12 @@ import (
14 16
 	"github.com/tidwall/buntdb"
15 17
 )
16 18
 
19
+const (
20
+	keyAccountExists      = "account %s exists"
21
+	keyAccountRegTime     = "account %s registered.time"
22
+	keyAccountCredentials = "account %s credentials"
23
+)
24
+
17 25
 var (
18 26
 	errAccountCreation = errors.New("Account could not be created")
19 27
 )
@@ -25,6 +33,13 @@ type AccountRegistration struct {
25 33
 	EnabledCredentialTypes []string
26 34
 }
27 35
 
36
+// AccountCredentials stores the various methods for verifying accounts.
37
+type AccountCredentials struct {
38
+	PassphraseSalt []byte
39
+	PassphraseHash []byte
40
+	Certificate    string // fingerprint
41
+}
42
+
28 43
 // NewAccountRegistration returns a new AccountRegistration, configured correctly.
29 44
 func NewAccountRegistration(config AccountRegistrationConfig) (accountReg AccountRegistration) {
30 45
 	if config.Enabled {
@@ -60,6 +75,18 @@ func regHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
60 75
 	return false
61 76
 }
62 77
 
78
+// removeFailedRegCreateData removes the data created by REG CREATE if the account creation fails early.
79
+func removeFailedRegCreateData(store buntdb.DB, account string) {
80
+	// error is ignored here, we can't do much about it anyways
81
+	store.Update(func(tx *buntdb.Tx) error {
82
+		tx.Delete(fmt.Sprintf(keyAccountExists, account))
83
+		tx.Delete(fmt.Sprintf(keyAccountRegTime, account))
84
+		tx.Delete(fmt.Sprintf(keyAccountCredentials, account))
85
+
86
+		return nil
87
+	})
88
+}
89
+
63 90
 // regCreateHandler parses the REG CREATE command.
64 91
 func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
65 92
 	client.Notice("Parsing CREATE")
@@ -75,7 +102,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
75 102
 	// check whether account exists
76 103
 	// do it all in one write tx to prevent races
77 104
 	err := server.store.Update(func(tx *buntdb.Tx) error {
78
-		accountKey := fmt.Sprintf("account %s exists", accountString)
105
+		accountKey := fmt.Sprintf(keyAccountExists, accountString)
79 106
 
80 107
 		_, err := tx.Get(accountKey)
81 108
 		if err != buntdb.ErrNotFound {
@@ -84,7 +111,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
84 111
 			return errAccountCreation
85 112
 		}
86 113
 
87
-		registeredTimeKey := fmt.Sprintf("account %s registered.time", accountString)
114
+		registeredTimeKey := fmt.Sprintf(keyAccountRegTime, accountString)
88 115
 
89 116
 		tx.Set(accountKey, "1", nil)
90 117
 		tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil)
@@ -121,7 +148,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
121 148
 
122 149
 	if !callbackValid {
123 150
 		client.Send(nil, server.nameString, ERR_REG_INVALID_CALLBACK, client.nickString, msg.Params[1], callbackNamespace, "Callback namespace is not supported")
124
-		//TODO(dan): close out failed account reg (remove values from db)
151
+		removeFailedRegCreateData(server.store, accountString)
125 152
 		return false
126 153
 	}
127 154
 
@@ -136,7 +163,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
136 163
 		credentialValue = msg.Params[3]
137 164
 	} else {
138 165
 		client.Send(nil, server.nameString, ERR_NEEDMOREPARAMS, client.nickString, msg.Command, "Not enough parameters")
139
-		//TODO(dan): close out failed account reg (remove values from db)
166
+		removeFailedRegCreateData(server.store, accountString)
140 167
 		return false
141 168
 	}
142 169
 
@@ -147,22 +174,61 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo
147 174
 			credentialValid = true
148 175
 		}
149 176
 	}
177
+	if credentialType == "certfp" && client.certfp == "" {
178
+		client.Send(nil, server.nameString, ERR_REG_INVALID_CRED_TYPE, client.nickString, credentialType, callbackNamespace, "You are not using a certificiate")
179
+		removeFailedRegCreateData(server.store, accountString)
180
+		return false
181
+	}
150 182
 
151 183
 	if !credentialValid {
152 184
 		client.Send(nil, server.nameString, ERR_REG_INVALID_CRED_TYPE, client.nickString, credentialType, callbackNamespace, "Credential type is not supported")
153
-		//TODO(dan): close out failed account reg (remove values from db)
185
+		removeFailedRegCreateData(server.store, accountString)
154 186
 		return false
155 187
 	}
156 188
 
157
-	// dispatch callback
189
+	// store details
190
+	err = server.store.Update(func(tx *buntdb.Tx) error {
191
+		var creds AccountCredentials
192
+
193
+		// always set passphrase salt
194
+		creds.PassphraseSalt, err = NewSalt()
195
+		if err != nil {
196
+			return fmt.Errorf("Could not create passphrase salt: %s", err.Error())
197
+		}
198
+
199
+		if credentialType == "certfp" {
200
+			creds.Certificate = client.certfp
201
+		} else if credentialType == "passphrase" {
202
+			creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, credentialValue)
203
+			if err != nil {
204
+				return fmt.Errorf("Could not hash password: %s", err)
205
+			}
206
+		}
207
+		credText, err := json.Marshal(creds)
208
+		if err != nil {
209
+			return fmt.Errorf("Could not marshal creds: %s", err)
210
+		}
211
+		tx.Set(keyAccountCredentials, string(credText), nil)
212
+
213
+		return nil
214
+	})
215
+
216
+	// details could not be stored and relevant numerics have been dispatched, abort
217
+	if err != nil {
218
+		client.Send(nil, server.nameString, ERR_UNKNOWNERROR, client.nickString, "REG", "CREATE", "Could not register")
219
+		log.Println("Could not save registration creds:", err.Error())
220
+		return false
221
+	}
222
+
223
+	// automatically complete registration
158 224
 	if callbackNamespace != "*" {
159 225
 		client.Notice("Account creation was successful!")
160
-		//TODO(dan): close out failed account reg (remove values from db)
226
+		removeFailedRegCreateData(server.store, accountString)
161 227
 		return false
162 228
 	}
163 229
 
230
+	// dispatch callback
164 231
 	client.Notice(fmt.Sprintf("We should dispatch an actual callback here to %s:%s", callbackNamespace, callbackValue))
165
-	client.Notice(fmt.Sprintf("Primary account credential is with %s:%s", credentialType, credentialValue))
166 232
 
167 233
 	return false
168 234
 }

+ 22
- 0
irc/server.go View File

@@ -9,6 +9,7 @@ import (
9 9
 	"bufio"
10 10
 	"crypto/tls"
11 11
 	"database/sql"
12
+	"encoding/base64"
12 13
 	"fmt"
13 14
 	"log"
14 15
 	"net"
@@ -40,6 +41,7 @@ type Server struct {
40 41
 	newConns            chan clientConn
41 42
 	operators           map[Name][]byte
42 43
 	password            []byte
44
+	passwords           *PasswordManager
43 45
 	accountRegistration *AccountRegistration
44 46
 	signals             chan os.Signal
45 47
 	proxyAllowedFrom    []string
@@ -91,6 +93,26 @@ func NewServer(config *Config) *Server {
91 93
 	defer db.Close()
92 94
 	server.store = *db
93 95
 
96
+	// load password manager
97
+	err = server.store.View(func(tx *buntdb.Tx) error {
98
+		saltString, err := tx.Get(keySalt)
99
+		if err != nil {
100
+			return fmt.Errorf("Could not retrieve salt string: %s", err.Error())
101
+		}
102
+
103
+		salt, err := base64.StdEncoding.DecodeString(saltString)
104
+		if err != nil {
105
+			return err
106
+		}
107
+
108
+		pwm := NewPasswordManager(salt)
109
+		server.passwords = &pwm
110
+		return nil
111
+	})
112
+	if err != nil {
113
+		log.Fatal(fmt.Sprintf("Could not load salt: %s", err.Error()))
114
+	}
115
+
94 116
 	if config.Server.MOTD != "" {
95 117
 		file, err := os.Open(config.Server.MOTD)
96 118
 		if err == nil {

+ 2
- 2
oragono.go View File

@@ -54,8 +54,8 @@ Options:
54 54
 		fmt.Print("\n")
55 55
 		fmt.Println(encoded)
56 56
 	} else if arguments["initdb"].(bool) {
57
-		irc.InitDB(config.Datastore.SQLitePath)
58
-		log.Println("database initialized: ", config.Datastore.SQLitePath)
57
+		irc.InitDB(config.Datastore.Path, config.Datastore.SQLitePath)
58
+		log.Println("databases initialized: ", config.Datastore.Path, config.Datastore.SQLitePath)
59 59
 	} else if arguments["upgradedb"].(bool) {
60 60
 		irc.UpgradeDB(config.Datastore.SQLitePath)
61 61
 		log.Println("database upgraded: ", config.Datastore.SQLitePath)

Loading…
Cancel
Save