From 7a6413ea2c1291583dac3d4ec3df7113c096d731 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Fri, 2 Oct 2020 16:48:37 -0400 Subject: [PATCH 1/3] first draft of atheme migration code --- Makefile | 1 + distrib/atheme/atheme2json.py | 111 +++++++++++ go.mod | 1 + go.sum | 2 + irc/accounts.go | 33 +++- irc/channelreg.go | 12 +- irc/database.go | 132 ++++++++++++- irc/import.go | 155 +++++++++++++++ irc/legacy.go | 45 ----- irc/migrations/legacy.go | 20 ++ irc/migrations/passwords.go | 183 ++++++++++++++++++ irc/migrations/passwords_test.go | 72 +++++++ oragono.go | 11 +- vendor/github.com/GehirnInc/crypt/.travis.yml | 7 + vendor/github.com/GehirnInc/crypt/AUTHORS.md | 8 + vendor/github.com/GehirnInc/crypt/LICENSE | 26 +++ vendor/github.com/GehirnInc/crypt/README.rst | 61 ++++++ .../GehirnInc/crypt/common/base64.go | 59 ++++++ .../github.com/GehirnInc/crypt/common/doc.go | 10 + .../github.com/GehirnInc/crypt/common/salt.go | 148 ++++++++++++++ vendor/github.com/GehirnInc/crypt/crypt.go | 121 ++++++++++++ .../GehirnInc/crypt/internal/utils.go | 41 ++++ .../GehirnInc/crypt/md5_crypt/md5_crypt.go | 143 ++++++++++++++ vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go | 77 ++++++++ vendor/modules.txt | 7 + 25 files changed, 1423 insertions(+), 63 deletions(-) create mode 100644 distrib/atheme/atheme2json.py create mode 100644 irc/import.go create mode 100644 irc/migrations/legacy.go create mode 100644 irc/migrations/passwords.go create mode 100644 irc/migrations/passwords_test.go create mode 100644 vendor/github.com/GehirnInc/crypt/.travis.yml create mode 100644 vendor/github.com/GehirnInc/crypt/AUTHORS.md create mode 100644 vendor/github.com/GehirnInc/crypt/LICENSE create mode 100644 vendor/github.com/GehirnInc/crypt/README.rst create mode 100644 vendor/github.com/GehirnInc/crypt/common/base64.go create mode 100644 vendor/github.com/GehirnInc/crypt/common/doc.go create mode 100644 vendor/github.com/GehirnInc/crypt/common/salt.go create mode 100644 vendor/github.com/GehirnInc/crypt/crypt.go create mode 100644 vendor/github.com/GehirnInc/crypt/internal/utils.go create mode 100644 vendor/github.com/GehirnInc/crypt/md5_crypt/md5_crypt.go create mode 100644 vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go diff --git a/Makefile b/Makefile index 5cccb9a0..8659506c 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,7 @@ test: cd irc/email && go test . && go vet . cd irc/history && go test . && go vet . cd irc/isupport && go test . && go vet . + cd irc/migrations && go test . && go vet . cd irc/modes && go test . && go vet . cd irc/mysql && go test . && go vet . cd irc/passwd && go test . && go vet . diff --git a/distrib/atheme/atheme2json.py b/distrib/atheme/atheme2json.py new file mode 100644 index 00000000..05a588bb --- /dev/null +++ b/distrib/atheme/atheme2json.py @@ -0,0 +1,111 @@ +import json +import logging +import sys +from collections import defaultdict + +def to_unixnano(timestamp): + return int(timestamp) * (10**9) + +def convert(infile): + out = { + 'version': 1, + 'source': 'atheme', + 'users': defaultdict(dict), + 'channels': defaultdict(dict), + } + + channel_to_founder = defaultdict(lambda: (None, None)) + + for line in infile: + line = line.strip() + parts = line.split() + category = parts[0] + if category == 'MU': + # user account + # MU AAAAAAAAB shivaram $1$hcspif$nCm4r3S14Me9ifsOPGuJT. user@example.com 1600134392 1600467343 +sC default + name = parts[2] + user = {'name': name, 'hash': parts[3], 'email': parts[4], 'registeredAt': to_unixnano(parts[5])} + out['users'][name].update(user) + pass + elif category == 'MN': + # grouped nick + # MN shivaram slingamn 1600218831 1600467343 + username, groupednick = parts[1], parts[2] + if username != groupednick: + user = out['users'][username] + if 'additionalNicks' not in user: + user['additionalNicks'] = [] + user['additionalNicks'].append(groupednick) + elif category == 'MDU': + if parts[2] == 'private:usercloak': + username = parts[1] + out['users'][username]['vhost'] = parts[3] + elif category == 'MC': + # channel registration + # MC #mychannel 1600134478 1600467343 +v 272 0 0 + chname = parts[1] + out['channels'][chname].update({'name': chname, 'registeredAt': to_unixnano(parts[2])}) + elif category == 'MDC': + # auxiliary data for a channel registration + # MDC #mychannel private:topic:setter s + # MDC #mychannel private:topic:text hi again + # MDC #mychannel private:topic:ts 1600135864 + chname = parts[1] + category = parts[2] + if category == 'private:topic:text': + out['channels'][chname]['topic'] = parts[3] + elif category == 'private:topic:setter': + out['channels'][chname]['topicSetBy'] = parts[3] + elif category == 'private:topic:ts': + out['channels'][chname]['topicSetAt'] = to_unixnano(parts[3]) + elif category == 'CA': + # channel access lists + # CA #mychannel shivaram +AFORafhioqrstv 1600134478 shivaram + chname, username, flags, set_at = parts[1], parts[2], parts[3], int(parts[4]) + chname = parts[1] + chdata = out['channels'][chname] + flags = parts[3] + set_at = int(parts[4]) + if 'amode' not in chdata: + chdata['amode'] = {} + if 'q' in flags: + # there can only be one founder + preexisting_founder, preexisting_set_at = channel_to_founder[chname] + if preexisting_founder is None or set_at < preexisting_set_at: + chdata['founder'] = username + channel_to_founder[chname] = (username, set_at) + # but multiple people can receive the 'q' amode + chdata['amode'][username] = ord('q') + elif 'a' in flags: + chdata['amode'][username] = ord('a') + elif 'o' in flags: + chdata['amode'][username] = ord('o') + elif 'h' in flags: + chdata['amode'][username] = ord('h') + elif 'v' in flags: + chdata['amode'][username] = ord('v') + else: + pass + + # do some basic integrity checks + for chname, chdata in out['channels'].items(): + founder = chdata.get('founder') + if founder not in out['users']: + raise ValueError("no user corresponding to channel founder", chname, chdata.get('founder')) + if 'registeredChannels' not in out['users'][founder]: + out['users'][founder]['registeredChannels'] = [] + out['users'][founder]['registeredChannels'].append(chname) + + return out + +def main(): + if len(sys.argv) != 3: + raise Exception("Usage: atheme2json.py atheme_db output.json") + with open(sys.argv[1]) as infile: + output = convert(infile) + with open(sys.argv[2], 'w') as outfile: + json.dump(output, outfile) + +if __name__ == '__main__': + logging.basicConfig() + sys.exit(main()) diff --git a/go.mod b/go.mod index 3f596fe4..ccd1a431 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 + github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 github.com/go-sql-driver/mysql v1.5.0 diff --git a/go.sum b/go.sum index b127dc14..3e112983 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9xClkGV/t0rmHrdhX4+trQUcBqjwc9xE= code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc= github.com/DanielOaks/go-idn v0.0.0-20160120021903-76db0e10dc65/go.mod h1:GYIaL2hleNQvfMUBTes1Zd/lDTyI/p2hv3kYB4jssyU= +github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw= +github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= diff --git a/irc/accounts.go b/irc/accounts.go index fb35c1de..2e7345fc 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -18,6 +18,7 @@ import ( "github.com/oragono/oragono/irc/connection_limits" "github.com/oragono/oragono/irc/email" + "github.com/oragono/oragono/irc/migrations" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/utils" @@ -1047,17 +1048,35 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou switch account.Credentials.Version { case 0: - err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase) + err = am.checkLegacyPassphrase(migrations.CheckOragonoPassphraseV0, accountName, account.Credentials.PassphraseHash, passphrase) case 1: if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil { err = errAccountInvalidCredentials } + case -1: + err = am.checkLegacyPassphrase(migrations.CheckAthemePassphrase, accountName, account.Credentials.PassphraseHash, passphrase) default: err = errAccountInvalidCredentials } return } +func (am *AccountManager) checkLegacyPassphrase(check migrations.PassphraseCheck, account string, hash []byte, passphrase string) (err error) { + err = check(hash, []byte(passphrase)) + if err != nil { + if err == migrations.ErrHashInvalid { + am.server.logger.Error("internal", "invalid legacy credentials for account", account) + } + return errAccountInvalidCredentials + } + // re-hash the passphrase with the latest algorithm + err = am.setPassword(account, passphrase, true) + if err != nil { + am.server.logger.Error("internal", "could not upgrade user password", err.Error()) + } + return nil +} + func (am *AccountManager) loadWithAutocreation(accountName string, autocreate bool) (account ClientAccount, err error) { account, err = am.LoadAccount(accountName) if err == errAccountDoesNotExist && autocreate { @@ -1872,10 +1891,18 @@ var ( } ) +type CredentialsVersion int + +const ( + CredentialsLegacy CredentialsVersion = 0 + CredentialsSHA3Bcrypt CredentialsVersion = 1 + // negative numbers for migration + CredentialsAtheme = -1 +) + // AccountCredentials stores the various methods for verifying accounts. type AccountCredentials struct { - Version uint - PassphraseSalt []byte // legacy field, not used by v1 and later + Version CredentialsVersion PassphraseHash []byte Certfps []string } diff --git a/irc/channelreg.go b/irc/channelreg.go index 47931c03..5a5a0335 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -233,11 +233,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC info = RegisteredChannel{ Name: name, NameCasefolded: nameCasefolded, - RegisteredAt: time.Unix(regTimeInt, 0).UTC(), + RegisteredAt: time.Unix(0, regTimeInt).UTC(), Founder: founder, Topic: topic, TopicSetBy: topicSetBy, - TopicSetTime: time.Unix(topicSetTimeInt, 0).UTC(), + TopicSetTime: time.Unix(0, topicSetTimeInt).UTC(), Key: password, Modes: modeSlice, Bans: banlist, @@ -273,11 +273,11 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist if err == nil { regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key)) regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) - registeredAt := time.Unix(regTimeInt, 0).UTC() + registeredAt := time.Unix(0, regTimeInt).UTC() founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key)) // to see if we're deleting the right channel, confirm the founder and the registration time - if founder == info.Founder && registeredAt.Unix() == info.RegisteredAt.Unix() { + if founder == info.Founder && registeredAt == info.RegisteredAt { for _, keyFmt := range channelKeyStrings { tx.Delete(fmt.Sprintf(keyFmt, key)) } @@ -339,13 +339,13 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha if includeFlags&IncludeInitial != 0 { tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil) tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil) - tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil) + tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.UnixNano(), 10), nil) tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil) } if includeFlags&IncludeTopic != 0 { tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil) - tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.Unix(), 10), nil) + tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10), nil) tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil) } diff --git a/irc/database.go b/irc/database.go index bd4c1e7c..2608127b 100644 --- a/irc/database.go +++ b/irc/database.go @@ -5,6 +5,7 @@ package irc import ( + "encoding/base64" "encoding/json" "fmt" "log" @@ -23,7 +24,7 @@ const ( // 'version' of the database schema keySchemaVersion = "db.version" // latest schema of the db - latestDbSchema = "12" + latestDbSchema = "14" keyCloakSecret = "crypto.cloak_secret" ) @@ -39,19 +40,26 @@ type SchemaChange struct { // maps an initial version to a schema change capable of upgrading it var schemaChanges map[string]SchemaChange -// InitDB creates the database, implementing the `oragono initdb` command. -func InitDB(path string) { +func checkDBReadyForInit(path string) error { _, err := os.Stat(path) if err == nil { - log.Fatal("Datastore already exists (delete it manually to continue): ", path) + return fmt.Errorf("Datastore already exists (delete it manually to continue): %s", path) } else if !os.IsNotExist(err) { - log.Fatal("Datastore path is inaccessible: ", err.Error()) + return fmt.Errorf("Datastore path %s is inaccessible: %w", path, err) + } + return nil +} + +// InitDB creates the database, implementing the `oragono initdb` command. +func InitDB(path string) error { + if err := checkDBReadyForInit(path); err != nil { + return err } - err = initializeDB(path) - if err != nil { - log.Fatal("Could not save datastore: ", err.Error()) + if err := initializeDB(path); err != nil { + return fmt.Errorf("Could not save datastore: %w", err) } + return nil } // internal database initialization code @@ -686,6 +694,104 @@ func schemaChangeV11ToV12(config *Config, tx *buntdb.Tx) error { return nil } +type accountCredsLegacyV13 struct { + Version CredentialsVersion + PassphraseHash []byte + Certfps []string +} + +// see #212 / #284. this packs the legacy salts into a single passphrase hash, +// allowing legacy passphrases to be verified using the new API `checkLegacyPassphrase`. +func schemaChangeV12ToV13(config *Config, tx *buntdb.Tx) error { + salt, err := tx.Get("crypto.salt") + if err != nil { + return nil // no change required + } + tx.Delete("crypto.salt") + rawSalt, err := base64.StdEncoding.DecodeString(salt) + if err != nil { + return nil // just throw away the creds at this point + } + prefix := "account.credentials " + var accounts []string + var credentials []accountCredsLegacyV13 + tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + account := strings.TrimPrefix(key, prefix) + + var credsOld accountCredsLegacyV9 + err = json.Unmarshal([]byte(value), &credsOld) + if err != nil { + return true + } + // skip if these aren't legacy creds! + if credsOld.Version != 0 { + return true + } + + var credsNew accountCredsLegacyV13 + credsNew.Version = 0 // mark hash for migration + credsNew.Certfps = credsOld.Certfps + credsNew.PassphraseHash = append(credsNew.PassphraseHash, rawSalt...) + credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseSalt...) + credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseHash...) + + accounts = append(accounts, account) + credentials = append(credentials, credsNew) + return true + }) + + for i, account := range accounts { + bytesOut, err := json.Marshal(credentials[i]) + if err != nil { + return err + } + _, _, err = tx.Set(prefix+account, string(bytesOut), nil) + if err != nil { + return err + } + } + + return nil +} + +// channel registration time and topic set time at nanosecond resolution +func schemaChangeV13ToV14(config *Config, tx *buntdb.Tx) error { + prefix := "channel.registered.time " + var channels, times []string + tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + channel := strings.TrimPrefix(key, prefix) + channels = append(channels, channel) + times = append(times, value) + return true + }) + + billion := int64(time.Second) + for i, channel := range channels { + regTime, err := strconv.ParseInt(times[i], 10, 64) + if err != nil { + log.Printf("corrupt registration time entry for %s: %v\n", channel, err) + continue + } + regTime = regTime * billion + tx.Set(prefix+channel, strconv.FormatInt(regTime, 10), nil) + + topicTimeKey := "channel.topic.settime " + channel + topicSetAt, err := tx.Get(topicTimeKey) + if err == nil { + if setTime, err := strconv.ParseInt(topicSetAt, 10, 64); err == nil { + tx.Set(topicTimeKey, strconv.FormatInt(setTime*billion, 10), nil) + } + } + } + return nil +} + func init() { allChanges := []SchemaChange{ { @@ -743,6 +849,16 @@ func init() { TargetVersion: "12", Changer: schemaChangeV11ToV12, }, + { + InitialVersion: "12", + TargetVersion: "13", + Changer: schemaChangeV12ToV13, + }, + { + InitialVersion: "13", + TargetVersion: "14", + Changer: schemaChangeV13ToV14, + }, } // build the index diff --git a/irc/import.go b/irc/import.go new file mode 100644 index 00000000..6893c7fc --- /dev/null +++ b/irc/import.go @@ -0,0 +1,155 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package irc + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "strconv" + "strings" + + "github.com/tidwall/buntdb" + + "github.com/oragono/oragono/irc/utils" +) + +type userImport struct { + Name string + Hash string + Email string + RegisteredAt int64 `json:"registeredAt"` + Vhost string + AdditionalNicks []string `json:"additionalNicks"` + RegisteredChannels []string +} + +type channelImport struct { + Name string + Founder string + RegisteredAt int64 `json:"registeredAt"` + Topic string + TopicSetBy string `json:"topicSetBy"` + TopicSetAt int64 `json:"topicSetAt"` + Amode map[string]int +} + +type databaseImport struct { + Version int + Source string + Users map[string]userImport + Channels map[string]channelImport +} + +func doImportAthemeDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err error) { + requiredVersion := 1 + if dbImport.Version != requiredVersion { + return fmt.Errorf("unsupported version of the db for import: version %d is required", requiredVersion) + } + + // produce a hardcoded version of the database schema + // XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal + // (to ensure that no matter what code changes happen elsewhere, we're still producing a + // version 14 db) + tx.Set(keySchemaVersion, "14", nil) + tx.Set(keyCloakSecret, utils.GenerateSecretKey(), nil) + + for username, userInfo := range dbImport.Users { + cfUsername, err := CasefoldName(username) + if err != nil { + log.Printf("invalid username %s: %v", username, err) + continue + } + credentials := AccountCredentials{ + Version: CredentialsAtheme, + PassphraseHash: []byte(userInfo.Hash), + } + marshaledCredentials, err := json.Marshal(&credentials) + if err != nil { + log.Printf("invalid credentials for %s: %v", username, err) + continue + } + tx.Set(fmt.Sprintf(keyAccountExists, cfUsername), "1", nil) + tx.Set(fmt.Sprintf(keyAccountVerified, cfUsername), "1", nil) + tx.Set(fmt.Sprintf(keyAccountName, cfUsername), userInfo.Name, nil) + tx.Set(fmt.Sprintf(keyAccountCallback, cfUsername), "mailto:"+userInfo.Email, nil) + tx.Set(fmt.Sprintf(keyAccountCredentials, cfUsername), string(marshaledCredentials), nil) + tx.Set(fmt.Sprintf(keyAccountRegTime, cfUsername), strconv.FormatInt(userInfo.RegisteredAt, 10), nil) + if userInfo.Vhost != "" { + tx.Set(fmt.Sprintf(keyAccountVHost, cfUsername), userInfo.Vhost, nil) + } + if len(userInfo.AdditionalNicks) != 0 { + tx.Set(fmt.Sprintf(keyAccountAdditionalNicks, cfUsername), marshalReservedNicks(userInfo.AdditionalNicks), nil) + } + if len(userInfo.RegisteredChannels) != 0 { + tx.Set(fmt.Sprintf(keyAccountChannels, cfUsername), strings.Join(userInfo.RegisteredChannels, ","), nil) + } + } + + for chname, chInfo := range dbImport.Channels { + cfchname, err := CasefoldChannel(chname) + if err != nil { + log.Printf("invalid channel name %s: %v", chname, err) + continue + } + tx.Set(fmt.Sprintf(keyChannelExists, cfchname), "1", nil) + tx.Set(fmt.Sprintf(keyChannelName, cfchname), chname, nil) + tx.Set(fmt.Sprintf(keyChannelRegTime, cfchname), strconv.FormatInt(chInfo.RegisteredAt, 10), nil) + tx.Set(fmt.Sprintf(keyChannelFounder, cfchname), chInfo.Founder, nil) + if chInfo.Topic != "" { + tx.Set(fmt.Sprintf(keyChannelTopic, cfchname), chInfo.Topic, nil) + tx.Set(fmt.Sprintf(keyChannelTopicSetTime, cfchname), strconv.FormatInt(chInfo.TopicSetAt, 10), nil) + tx.Set(fmt.Sprintf(keyChannelTopicSetBy, cfchname), chInfo.TopicSetBy, nil) + } + if len(chInfo.Amode) != 0 { + m, err := json.Marshal(chInfo.Amode) + if err == nil { + tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil) + } else { + log.Printf("couldn't serialize amodes for %s: %v", chname, err) + } + } + } + + return nil +} + +func doImportDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err error) { + switch dbImport.Source { + case "atheme": + return doImportAthemeDB(config, dbImport, tx) + default: + return fmt.Errorf("only imports from atheme are currently supported") + } +} + +func ImportDB(config *Config, infile string) (err error) { + data, err := ioutil.ReadFile(infile) + if err != nil { + return + } + + var dbImport databaseImport + err = json.Unmarshal(data, &dbImport) + if err != nil { + return err + } + + err = checkDBReadyForInit(config.Datastore.Path) + if err != nil { + return err + } + + db, err := buntdb.Open(config.Datastore.Path) + if err != nil { + return err + } + + performImport := func(tx *buntdb.Tx) (err error) { + return doImportDB(config, dbImport, tx) + } + + return db.Update(performImport) +} diff --git a/irc/legacy.go b/irc/legacy.go index 2a1d8b6b..0a55d3ca 100644 --- a/irc/legacy.go +++ b/irc/legacy.go @@ -5,10 +5,6 @@ package irc import ( "encoding/base64" "errors" - "fmt" - - "github.com/tidwall/buntdb" - "golang.org/x/crypto/bcrypt" ) var ( @@ -29,44 +25,3 @@ func decodeLegacyPasswordHash(hash string) ([]byte, error) { return nil, errInvalidPasswordHash } } - -// helper to check a version 0 password hash, with global and per-passphrase salts -func checkLegacyPasswordV0(hashedPassword, globalSalt, passphraseSalt []byte, passphrase string) error { - var assembledPasswordBytes []byte - assembledPasswordBytes = append(assembledPasswordBytes, globalSalt...) - assembledPasswordBytes = append(assembledPasswordBytes, '-') - assembledPasswordBytes = append(assembledPasswordBytes, passphraseSalt...) - assembledPasswordBytes = append(assembledPasswordBytes, '-') - assembledPasswordBytes = append(assembledPasswordBytes, []byte(passphrase)...) - return bcrypt.CompareHashAndPassword(hashedPassword, assembledPasswordBytes) -} - -// checks a version 0 password hash; if successful, upgrades the database entry to version 1 -func handleLegacyPasswordV0(server *Server, account string, credentials AccountCredentials, passphrase string) (err error) { - var globalSaltString string - err = server.store.View(func(tx *buntdb.Tx) (err error) { - globalSaltString, err = tx.Get("crypto.salt") - return err - }) - if err != nil { - return err - } - globalSalt, err := base64.StdEncoding.DecodeString(globalSaltString) - if err != nil { - return err - } - - err = checkLegacyPasswordV0(credentials.PassphraseHash, globalSalt, credentials.PassphraseSalt, passphrase) - if err != nil { - // invalid password - return err - } - - // upgrade credentials - err = server.accounts.setPassword(account, passphrase, true) - if err != nil { - server.logger.Error("internal", fmt.Sprintf("could not upgrade user password: %v", err)) - } - - return nil -} diff --git a/irc/migrations/legacy.go b/irc/migrations/legacy.go new file mode 100644 index 00000000..49e4c1e8 --- /dev/null +++ b/irc/migrations/legacy.go @@ -0,0 +1,20 @@ +package migrations + +import ( + "golang.org/x/crypto/bcrypt" +) + +// See the v12-to-v13 schema change. The format of this hash is: +// 30 bytes of global salt, 30 bytes of per-passphrase salt, then the bcrypt hash +func CheckOragonoPassphraseV0(hash, passphrase []byte) error { + globalSalt := hash[:30] + passphraseSalt := hash[30:60] + bcryptHash := hash[60:] + assembledPasswordBytes := make([]byte, 0, 60+len(passphrase)+2) + assembledPasswordBytes = append(assembledPasswordBytes, globalSalt...) + assembledPasswordBytes = append(assembledPasswordBytes, '-') + assembledPasswordBytes = append(assembledPasswordBytes, passphraseSalt...) + assembledPasswordBytes = append(assembledPasswordBytes, '-') + assembledPasswordBytes = append(assembledPasswordBytes, passphrase...) + return bcrypt.CompareHashAndPassword(bcryptHash, assembledPasswordBytes) +} diff --git a/irc/migrations/passwords.go b/irc/migrations/passwords.go new file mode 100644 index 00000000..70fc7d7a --- /dev/null +++ b/irc/migrations/passwords.go @@ -0,0 +1,183 @@ +package migrations + +import ( + "bytes" + "crypto/hmac" + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "crypto/subtle" + "encoding/base64" + "encoding/hex" + "errors" + "hash" + "strconv" + + "github.com/GehirnInc/crypt/md5_crypt" + "golang.org/x/crypto/pbkdf2" +) + +var ( + ErrHashInvalid = errors.New("password hash invalid for algorithm") + ErrHashCheckFailed = errors.New("passphrase did not match stored hash") + + hmacServerKeyText = []byte("Server Key") + athemePBKDF2V2Prefix = []byte("$z") +) + +type PassphraseCheck func(hash, passphrase []byte) (err error) + +func CheckAthemePassphrase(hash, passphrase []byte) (err error) { + if len(hash) < 60 { + return checkAthemePosixCrypt(hash, passphrase) + } else if bytes.HasPrefix(hash, athemePBKDF2V2Prefix) { + return checkAthemePBKDF2V2(hash, passphrase) + } else { + return checkAthemePBKDF2(hash, passphrase) + } +} + +func checkAthemePosixCrypt(hash, passphrase []byte) (err error) { + // crypto/posix: the platform's crypt(3) function + // MD5 on linux, DES on MacOS: forget MacOS + md5crypt := md5_crypt.New() + return md5crypt.Verify(string(hash), []byte(passphrase)) +} + +type pbkdf2v2Algo struct { + Hash func() hash.Hash + OutputSize int + SCRAM bool + SaltB64 bool +} + +func athemePBKDF2V2ParseAlgo(algo string) (result pbkdf2v2Algo, err error) { + // https://github.com/atheme/atheme/blob/a11e85efc67d86fc4738e3e2a4f220bfa69153f0/include/atheme/pbkdf2.h#L34-L52 + algoInt, err := strconv.Atoi(algo) + if err != nil { + return result, ErrHashInvalid + } + hashCode := algoInt % 10 + algoCode := algoInt - hashCode + + switch algoCode { + case 0: + // e.g., #define PBKDF2_PRF_HMAC_MD5 3U + // no SCRAM, no SHA256 + case 20: + // e.g., #define PBKDF2_PRF_HMAC_MD5_S64 23U + // no SCRAM, base64 + result.SaltB64 = true + case 40: + // e.g., #define PBKDF2_PRF_SCRAM_MD5 43U + // SCRAM, no base64 + result.SCRAM = true + case 60: + // e.g., #define PBKDF2_PRF_SCRAM_MD5_S64 63U + result.SaltB64 = true + result.SCRAM = true + default: + return result, ErrHashInvalid + } + + switch hashCode { + case 3: + result.Hash, result.OutputSize = md5.New, (128 / 8) + case 4: + result.Hash, result.OutputSize = sha1.New, (160 / 8) + case 5: + result.Hash, result.OutputSize = sha256.New, (256 / 8) + case 6: + result.Hash, result.OutputSize = sha512.New, (512 / 8) + default: + return result, ErrHashInvalid + } + + return result, nil +} + +func checkAthemePBKDF2V2(hash, passphrase []byte) (err error) { + // crypto/pbkdf2v2, the default as of september 2020: + // "the format for pbkdf2v2 is $z$alg$iter$salt$digest + // where the z is literal, + // the alg is one from https://github.com/atheme/atheme/blob/master/include/atheme/pbkdf2.h#L34-L52 + // iter is the iteration count. + // if the alg ends in _S64 then the salt is base64-encoded, otherwise taken literally + // (an ASCII salt, inherited from the pbkdf2 module). + // if alg is a SCRAM one, then digest is actually serverkey$storedkey (see RFC 5802). + // digest, serverkey and storedkey are base64-encoded." + parts := bytes.Split(hash, []byte{'$'}) + if len(parts) < 6 { + return ErrHashInvalid + } + algo, err := athemePBKDF2V2ParseAlgo(string(parts[2])) + if err != nil { + return err + } + + iter, err := strconv.Atoi(string(parts[3])) + if err != nil { + return ErrHashInvalid + } + + salt := parts[4] + if algo.SaltB64 { + salt, err = base64.StdEncoding.DecodeString(string(salt)) + if err != nil { + return err + } + } + + // if SCRAM, parts[5] is ServerKey; otherwise it's the actual PBKDF2 output + // either way, it's what we'll test against + expected, err := base64.StdEncoding.DecodeString(string(parts[5])) + if err != nil { + return err + } + + var key []byte + if algo.SCRAM { + if len(parts) != 7 { + return ErrHashInvalid + } + stretch := pbkdf2.Key(passphrase, salt, iter, algo.OutputSize, algo.Hash) + mac := hmac.New(algo.Hash, stretch) + mac.Write(hmacServerKeyText) + key = mac.Sum(nil) + } else { + if len(parts) != 6 { + return ErrHashInvalid + } + key = pbkdf2.Key(passphrase, salt, iter, len(expected), algo.Hash) + } + + if subtle.ConstantTimeCompare(key, expected) == 1 { + return nil + } else { + return ErrHashCheckFailed + } +} + +func checkAthemePBKDF2(hash, passphrase []byte) (err error) { + // crypto/pbkdf2: + // "SHA2-512, 128000 iterations, 16-ASCII-character salt, hexadecimal encoding of digest, + // digest appended directly to salt, for a single string consisting of only 144 characters" + if len(hash) != 144 { + return ErrHashInvalid + } + + salt := hash[:16] + digest := make([]byte, 64) + cnt, err := hex.Decode(digest, hash[16:]) + if err != nil || cnt != 64 { + return ErrHashCheckFailed + } + + key := pbkdf2.Key(passphrase, salt, 128000, 64, sha512.New) + if subtle.ConstantTimeCompare(key, digest) == 1 { + return nil + } else { + return ErrHashCheckFailed + } +} diff --git a/irc/migrations/passwords_test.go b/irc/migrations/passwords_test.go new file mode 100644 index 00000000..06fe2978 --- /dev/null +++ b/irc/migrations/passwords_test.go @@ -0,0 +1,72 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package migrations + +import ( + "encoding/base64" + "testing" +) + +func TestAthemePassphrases(t *testing.T) { + var err error + + err = CheckAthemePassphrase([]byte("$1$hcspif$nCm4r3S14Me9ifsOPGuJT."), []byte("shivarampassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + + err = CheckAthemePassphrase([]byte("$1$hcspif$nCm4r3S14Me9ifsOPGuJT."), []byte("sh1varampassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } + + err = CheckAthemePassphrase([]byte("khMlbBBIFya2ihyN42abc3e768663e2c4fd0e0020e46292bf9fdf44e9a51d2a2e69509cb73b4b1bf9c1b6355a1fc9ea663fcd6da902287159494f15b905e5e651d6a60f2ec834598"), []byte("password")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + + err = CheckAthemePassphrase([]byte("khMlbBBIFya2ihyN42abc3e768663e2c4fd0e0020e46292bf9fdf44e9a51d2a2e69509cb73b4b1bf9c1b6355a1fc9ea663fcd6da902287159494f15b905e5e651d6a60f2ec834598"), []byte("passw0rd")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } + + err = CheckAthemePassphrase([]byte("$z$65$64000$1kz1I9YJPJ2gkJALbrpL2DoxRDhYPBOg60KNJMK/6do=$Cnfg6pYhBNrVXiaXYH46byrC+3HKet/XvYwvI1BvZbs=$m0hrT33gcF90n2TU3lm8tdm9V9XC4xEV13KsjuT38iY="), []byte("password")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + + err = CheckAthemePassphrase([]byte("$z$65$64000$1kz1I9YJPJ2gkJALbrpL2DoxRDhYPBOg60KNJMK/6do=$Cnfg6pYhBNrVXiaXYH46byrC+3HKet/XvYwvI1BvZbs=$m0hrT33gcF90n2TU3lm8tdm9V9XC4xEV13KsjuT38iY="), []byte("passw0rd")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } +} + +func TestOragonoLegacyPassphrase(t *testing.T) { + shivaramHash, err := base64.StdEncoding.DecodeString("ZPLKvCGipalUo9AlDIlMzAuY/ACWvM3yr1kh7k0/wa7lLlCwaPpe2ht9LNZZlZ9FPUWggUi7D4jyg2WnJDJhJDE0JDRsN0gwVmYvNHlyNjR1U212U2Q0YU9EVmRvWngwcXNGLkkyYVc4eUZISGxYaGE4SWVrRzRt") + if err != nil { + panic(err) + } + edHash, err := base64.StdEncoding.DecodeString("ZPLKvCGipalUo9AlDIlMzAuY/ACWvM3yr1kh7k0/+42q72mFnpDZWgjmqp1Zd77rEUO8ItYe4aGwWelUJDJhJDE0JHFqSGJ5NWVJbnJTdXBRT29pUmNUUWV5U2xmWjZETlRNcXlSMExUb2RmY3l1Skw2c3BTb3lh") + if err != nil { + panic(err) + } + + err = CheckOragonoPassphraseV0(shivaramHash, []byte("shivarampassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckOragonoPassphraseV0(shivaramHash, []byte("edpassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } + + err = CheckOragonoPassphraseV0(edHash, []byte("edpassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckOragonoPassphraseV0(edHash, []byte("shivarampassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } +} diff --git a/oragono.go b/oragono.go index e7ef49b3..308409b6 100644 --- a/oragono.go +++ b/oragono.go @@ -96,6 +96,7 @@ func main() { Usage: oragono initdb [--conf ] [--quiet] oragono upgradedb [--conf ] [--quiet] + oragono importdb [--conf ] [--quiet] oragono genpasswd [--conf ] [--quiet] oragono mkcerts [--conf ] [--quiet] oragono run [--conf ] [--quiet] [--smoke] @@ -155,7 +156,10 @@ Options: } if arguments["initdb"].(bool) { - irc.InitDB(config.Datastore.Path) + err = irc.InitDB(config.Datastore.Path) + if err != nil { + log.Fatal("Error while initializing db:", err.Error()) + } if !arguments["--quiet"].(bool) { log.Println("database initialized: ", config.Datastore.Path) } @@ -167,6 +171,11 @@ Options: if !arguments["--quiet"].(bool) { log.Println("database upgraded: ", config.Datastore.Path) } + } else if arguments["importdb"].(bool) { + err = irc.ImportDB(config, arguments[""].(string)) + if err != nil { + log.Fatal("Error while importing db:", err.Error()) + } } else if arguments["run"].(bool) { if !arguments["--quiet"].(bool) { logman.Info("server", fmt.Sprintf("%s starting", irc.Ver)) diff --git a/vendor/github.com/GehirnInc/crypt/.travis.yml b/vendor/github.com/GehirnInc/crypt/.travis.yml new file mode 100644 index 00000000..6a63bc8e --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/.travis.yml @@ -0,0 +1,7 @@ +language: go +go: + - 1.6.x + - 1.7.x + - master +script: + - go test -v -race ./... diff --git a/vendor/github.com/GehirnInc/crypt/AUTHORS.md b/vendor/github.com/GehirnInc/crypt/AUTHORS.md new file mode 100644 index 00000000..4490cf22 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/AUTHORS.md @@ -0,0 +1,8 @@ +### Initial author + +[Jeramey Crawford](https://github.com/jeramey) + +### Other authors + +- [Jonas mg](https://github.com/kless) +- [Kohei YOSHIDA](https://github.com/yosida95) diff --git a/vendor/github.com/GehirnInc/crypt/LICENSE b/vendor/github.com/GehirnInc/crypt/LICENSE new file mode 100644 index 00000000..7048fece --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2012, Jeramey Crawford +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/GehirnInc/crypt/README.rst b/vendor/github.com/GehirnInc/crypt/README.rst new file mode 100644 index 00000000..0608624f --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/README.rst @@ -0,0 +1,61 @@ +.. image:: https://travis-ci.org/GehirnInc/crypt.svg?branch=master + :target: https://travis-ci.org/GehirnInc/crypt + +crypt - A password hashing library for Go +========================================= +crypt provides pure golang implementations of UNIX's crypt(3). + +The goal of crypt is to bring a library of many common and popular password +hashing algorithms to Go and to provide a simple and consistent interface to +each of them. As every hashing method is implemented in pure Go, this library +should be as portable as Go itself. + +All hashing methods come with a test suite which verifies their operation +against itself as well as the output of other password hashing implementations +to ensure compatibility with them. + +I hope you find this library to be useful and easy to use! + +Install +------- + +To install crypt, use the *go get* command. + +.. code-block:: sh + + go get github.com/GehirnInc/crypt + + +Usage +----- + +.. code-block:: go + + package main + + import ( + "fmt" + + "github.com/GehirnInc/crypt" + _ "github.com/GehirnInc/crypt/sha256_crypt" + ) + + func main() { + crypt := crypt.SHA256.New() + ret, _ := crypt.Generate([]byte("secret"), []byte("$5$salt")) + fmt.Println(ret) + + err := crypt.Verify(ret, []byte("secret")) + fmt.Println(err) + + // Output: + // $5$salt$kpa26zwgX83BPSR8d7w93OIXbFt/d3UOTZaAu5vsTM6 + // + } + +Documentation +------------- + +The documentation is available on GoDoc_. + +.. _GoDoc: https://godoc.org/github.com/GehirnInc/crypt diff --git a/vendor/github.com/GehirnInc/crypt/common/base64.go b/vendor/github.com/GehirnInc/crypt/common/base64.go new file mode 100644 index 00000000..ee5240e1 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/common/base64.go @@ -0,0 +1,59 @@ +// (C) Copyright 2012, Jeramey Crawford . All +// rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package common + +const ( + alphabet = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +) + +// Base64_24Bit is a variant of Base64 encoding, commonly used with password +// hashing algorithms to encode the result of their checksum output. +// +// The algorithm operates on up to 3 bytes at a time, encoding the following +// 6-bit sequences into up to 4 hash64 ASCII bytes. +// +// 1. Bottom 6 bits of the first byte +// 2. Top 2 bits of the first byte, and bottom 4 bits of the second byte. +// 3. Top 4 bits of the second byte, and bottom 2 bits of the third byte. +// 4. Top 6 bits of the third byte. +// +// This encoding method does not emit padding bytes as Base64 does. +func Base64_24Bit(src []byte) []byte { + if len(src) == 0 { + return []byte{} // TODO: return nil + } + + dstlen := (len(src)*8 + 5) / 6 + dst := make([]byte, dstlen) + + di, si := 0, 0 + n := len(src) / 3 * 3 + for si < n { + val := uint(src[si+2])<<16 | uint(src[si+1])<<8 | uint(src[si]) + dst[di+0] = alphabet[val&0x3f] + dst[di+1] = alphabet[val>>6&0x3f] + dst[di+2] = alphabet[val>>12&0x3f] + dst[di+3] = alphabet[val>>18] + di += 4 + si += 3 + } + + rem := len(src) - si + if rem == 0 { + return dst + } + + val := uint(src[si+0]) + if rem == 2 { + val |= uint(src[si+1]) << 8 + } + + dst[di+0] = alphabet[val&0x3f] + dst[di+1] = alphabet[val>>6&0x3f] + if rem == 2 { + dst[di+2] = alphabet[val>>12] + } + return dst +} diff --git a/vendor/github.com/GehirnInc/crypt/common/doc.go b/vendor/github.com/GehirnInc/crypt/common/doc.go new file mode 100644 index 00000000..8ba84e96 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/common/doc.go @@ -0,0 +1,10 @@ +// (C) Copyright 2012, Jeramey Crawford . All +// rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package common contains routines used by multiple password hashing +// algorithms. +// +// Generally, you will never import this package directly. Many of the +// *_crypt packages will import this package if they require it. +package common diff --git a/vendor/github.com/GehirnInc/crypt/common/salt.go b/vendor/github.com/GehirnInc/crypt/common/salt.go new file mode 100644 index 00000000..54372be0 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/common/salt.go @@ -0,0 +1,148 @@ +// (C) Copyright 2012, Jeramey Crawford . All +// rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package common + +import ( + "bytes" + "crypto/rand" + "errors" + "strconv" +) + +var ( + ErrSaltPrefix = errors.New("invalid magic prefix") + ErrSaltFormat = errors.New("invalid salt format") + ErrSaltRounds = errors.New("invalid rounds") +) + +const ( + roundsPrefix = "rounds=" +) + +// Salt represents a salt. +type Salt struct { + MagicPrefix []byte + + SaltLenMin int + SaltLenMax int + + RoundsMin int + RoundsMax int + RoundsDefault int +} + +// Generate generates a random salt of a given length. +// +// The length is set thus: +// +// length > SaltLenMax: length = SaltLenMax +// length < SaltLenMin: length = SaltLenMin +func (s *Salt) Generate(length int) []byte { + if length > s.SaltLenMax { + length = s.SaltLenMax + } else if length < s.SaltLenMin { + length = s.SaltLenMin + } + + saltLen := (length * 6 / 8) + if (length*6)%8 != 0 { + saltLen += 1 + } + salt := make([]byte, saltLen) + rand.Read(salt) + + out := make([]byte, len(s.MagicPrefix)+length) + copy(out, s.MagicPrefix) + copy(out[len(s.MagicPrefix):], Base64_24Bit(salt)) + return out +} + +// GenerateWRounds creates a random salt with the random bytes being of the +// length provided, and the rounds parameter set as specified. +// +// The parameters are set thus: +// +// length > SaltLenMax: length = SaltLenMax +// length < SaltLenMin: length = SaltLenMin +// +// rounds < 0: rounds = RoundsDefault +// rounds < RoundsMin: rounds = RoundsMin +// rounds > RoundsMax: rounds = RoundsMax +// +// If rounds is equal to RoundsDefault, then the "rounds=" part of the salt is +// removed. +func (s *Salt) GenerateWRounds(length, rounds int) []byte { + if length > s.SaltLenMax { + length = s.SaltLenMax + } else if length < s.SaltLenMin { + length = s.SaltLenMin + } + if rounds < 0 { + rounds = s.RoundsDefault + } else if rounds < s.RoundsMin { + rounds = s.RoundsMin + } else if rounds > s.RoundsMax { + rounds = s.RoundsMax + } + + saltLen := (length * 6 / 8) + if (length*6)%8 != 0 { + saltLen += 1 + } + salt := make([]byte, saltLen) + rand.Read(salt) + + roundsText := "" + if rounds != s.RoundsDefault { + roundsText = roundsPrefix + strconv.Itoa(rounds) + "$" + } + + out := make([]byte, len(s.MagicPrefix)+len(roundsText)+length) + copy(out, s.MagicPrefix) + copy(out[len(s.MagicPrefix):], []byte(roundsText)) + copy(out[len(s.MagicPrefix)+len(roundsText):], Base64_24Bit(salt)) + return out +} + +func (s *Salt) Decode(raw []byte) (salt []byte, rounds int, isRoundsDef bool, rest []byte, err error) { + tokens := bytes.SplitN(raw, []byte{'$'}, 4) + if len(tokens) < 3 { + err = ErrSaltFormat + return + } + if !bytes.HasPrefix(raw, s.MagicPrefix) { + err = ErrSaltPrefix + return + } + + if bytes.HasPrefix(tokens[2], []byte(roundsPrefix)) { + if len(tokens) < 4 { + err = ErrSaltFormat + return + } + salt = tokens[3] + + rounds, err = strconv.Atoi(string(tokens[2][len(roundsPrefix):])) + if err != nil { + err = ErrSaltRounds + return + } + if rounds < s.RoundsMin { + rounds = s.RoundsMin + } + if rounds > s.RoundsMax { + rounds = s.RoundsMax + } + isRoundsDef = true + } else { + salt = tokens[2] + rounds = s.RoundsDefault + } + if len(salt) > s.SaltLenMax { + salt = salt[0:s.SaltLenMax] + } + + return +} diff --git a/vendor/github.com/GehirnInc/crypt/crypt.go b/vendor/github.com/GehirnInc/crypt/crypt.go new file mode 100644 index 00000000..1b4151f3 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/crypt.go @@ -0,0 +1,121 @@ +// (C) Copyright 2013, Jonas mg. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file. + +// Package crypt provides interface for password crypt functions and collects +// common constants. +package crypt + +import ( + "errors" + "strings" + + "github.com/GehirnInc/crypt/common" +) + +var ErrKeyMismatch = errors.New("hashed value is not the hash of the given password") + +// Crypter is the common interface implemented by all crypt functions. +type Crypter interface { + // Generate performs the hashing algorithm, returning a full hash suitable + // for storage and later password verification. + // + // If the salt is empty, a randomly-generated salt will be generated with a + // length of SaltLenMax and number RoundsDefault of rounds. + // + // Any error only can be got when the salt argument is not empty. + Generate(key, salt []byte) (string, error) + + // Verify compares a hashed key with its possible key equivalent. + // Returns nil on success, or an error on failure; if the hashed key is + // diffrent, the error is "ErrKeyMismatch". + Verify(hashedKey string, key []byte) error + + // Cost returns the hashing cost (in rounds) used to create the given hashed + // key. + // + // When, in the future, the hashing cost of a key needs to be increased in + // order to adjust for greater computational power, this function allows one + // to establish which keys need to be updated. + // + // The algorithms based in MD5-crypt use a fixed value of rounds. + Cost(hashedKey string) (int, error) + + // SetSalt sets a different salt. It is used to easily create derivated + // algorithms, i.e. "apr1_crypt" from "md5_crypt". + SetSalt(salt common.Salt) +} + +// Crypt identifies a crypt function that is implemented in another package. +type Crypt uint + +const ( + APR1 Crypt = 1 + iota // import github.com/GehirnInc/crypt/apr1_crypt + MD5 // import github.com/GehirnInc/crypt/md5_crypt + SHA256 // import github.com/GehirnInc/crypt/sha256_crypt + SHA512 // import github.com/GehirnInc/crypt/sha512_crypt + maxCrypt +) + +var crypts = make([]func() Crypter, maxCrypt) + +// New returns new Crypter making the Crypt c. +// New panics if the Crypt c is unavailable. +func (c Crypt) New() Crypter { + if c > 0 && c < maxCrypt { + f := crypts[c] + if f != nil { + return f() + } + } + panic("crypt: requested crypt function is unavailable") +} + +// Available reports whether the Crypt c is available. +func (c Crypt) Available() bool { + return c > 0 && c < maxCrypt && crypts[c] != nil +} + +var cryptPrefixes = make([]string, maxCrypt) + +// RegisterCrypt registers a function that returns a new instance of the given +// crypt function. This is intended to be called from the init function in +// packages that implement crypt functions. +func RegisterCrypt(c Crypt, f func() Crypter, prefix string) { + if c >= maxCrypt { + panic("crypt: RegisterHash of unknown crypt function") + } + crypts[c] = f + cryptPrefixes[c] = prefix +} + +// New returns a new crypter. +func New(c Crypt) Crypter { + return c.New() +} + +// IsHashSupported returns true if hashedKey has a supported prefix. +// NewFromHash will not panic for this hashedKey +func IsHashSupported(hashedKey string) bool { + for i := range cryptPrefixes { + prefix := cryptPrefixes[i] + if crypts[i] != nil && strings.HasPrefix(hashedKey, prefix) { + return true + } + } + + return false +} + +// NewFromHash returns a new Crypter using the prefix in the given hashed key. +func NewFromHash(hashedKey string) Crypter { + for i := range cryptPrefixes { + prefix := cryptPrefixes[i] + if crypts[i] != nil && strings.HasPrefix(hashedKey, prefix) { + crypt := Crypt(uint(i)) + return crypt.New() + } + } + + panic("crypt: unknown crypt function") +} diff --git a/vendor/github.com/GehirnInc/crypt/internal/utils.go b/vendor/github.com/GehirnInc/crypt/internal/utils.go new file mode 100644 index 00000000..2d36e86a --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/internal/utils.go @@ -0,0 +1,41 @@ +// Copyright (c) 2015 Kohei YOSHIDA. All rights reserved. +// This software is licensed under the 3-Clause BSD License +// that can be found in LICENSE file. +package internal + +const ( + cleanBytesLen = 64 +) + +var ( + cleanBytes = make([]byte, cleanBytesLen) +) + +func CleanSensitiveData(b []byte) { + l := len(b) + + for ; l > cleanBytesLen; l -= cleanBytesLen { + copy(b[l-cleanBytesLen:l], cleanBytes) + } + + if l > 0 { + copy(b[0:l], cleanBytes[0:l]) + } +} + +func RepeatByteSequence(input []byte, length int) []byte { + var ( + sequence = make([]byte, length) + unit = len(input) + ) + + j := length / unit * unit + for i := 0; i < j; i += unit { + copy(sequence[i:length], input) + } + if j < length { + copy(sequence[j:length], input[0:length-j]) + } + + return sequence +} diff --git a/vendor/github.com/GehirnInc/crypt/md5_crypt/md5_crypt.go b/vendor/github.com/GehirnInc/crypt/md5_crypt/md5_crypt.go new file mode 100644 index 00000000..768aff92 --- /dev/null +++ b/vendor/github.com/GehirnInc/crypt/md5_crypt/md5_crypt.go @@ -0,0 +1,143 @@ +// (C) Copyright 2012, Jeramey Crawford . All +// rights reserved. Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package md5_crypt implements the standard Unix MD5-crypt algorithm created by +// Poul-Henning Kamp for FreeBSD. +package md5_crypt + +import ( + "bytes" + "crypto/md5" + "crypto/subtle" + + "github.com/GehirnInc/crypt" + "github.com/GehirnInc/crypt/common" + "github.com/GehirnInc/crypt/internal" +) + +func init() { + crypt.RegisterCrypt(crypt.MD5, New, MagicPrefix) +} + +// NOTE: Cisco IOS only allows salts of length 4. + +const ( + MagicPrefix = "$1$" + SaltLenMin = 1 // Real minimum is 0, but that isn't useful. + SaltLenMax = 8 + RoundsDefault = 1000 +) + +type crypter struct{ Salt common.Salt } + +// New returns a new crypt.Crypter computing the MD5-crypt password hashing. +func New() crypt.Crypter { + return &crypter{ + common.Salt{ + MagicPrefix: []byte(MagicPrefix), + SaltLenMin: SaltLenMin, + SaltLenMax: SaltLenMax, + RoundsDefault: RoundsDefault, + }, + } +} + +func (c *crypter) Generate(key, salt []byte) (result string, err error) { + if len(salt) == 0 { + salt = c.Salt.Generate(SaltLenMax) + } + salt, _, _, _, err = c.Salt.Decode(salt) + if err != nil { + return + } + + keyLen := len(key) + h := md5.New() + + // Compute sumB + h.Write(key) + h.Write(salt) + h.Write(key) + sumB := h.Sum(nil) + + // Compute sumA + h.Reset() + h.Write(key) + h.Write(c.Salt.MagicPrefix) + h.Write(salt) + h.Write(internal.RepeatByteSequence(sumB, keyLen)) + // The original implementation now does something weird: + // For every 1 bit in the key, the first 0 is added to the buffer + // For every 0 bit, the first character of the key + // This does not seem to be what was intended but we have to follow this to + // be compatible. + for i := keyLen; i > 0; i >>= 1 { + if i%2 == 0 { + h.Write(key[0:1]) + } else { + h.Write([]byte{0}) + } + } + sumA := h.Sum(nil) + internal.CleanSensitiveData(sumB) + + // In fear of password crackers here comes a quite long loop which just + // processes the output of the previous round again. + // We cannot ignore this here. + for i := 0; i < RoundsDefault; i++ { + h.Reset() + + // Add key or last result. + if i%2 != 0 { + h.Write(key) + } else { + h.Write(sumA) + } + // Add salt for numbers not divisible by 3. + if i%3 != 0 { + h.Write(salt) + } + // Add key for numbers not divisible by 7. + if i%7 != 0 { + h.Write(key) + } + // Add key or last result. + if i&1 != 0 { + h.Write(sumA) + } else { + h.Write(key) + } + copy(sumA, h.Sum(nil)) + } + + buf := bytes.Buffer{} + buf.Grow(len(c.Salt.MagicPrefix) + len(salt) + 1 + 22) + buf.Write(c.Salt.MagicPrefix) + buf.Write(salt) + buf.WriteByte('$') + buf.Write(common.Base64_24Bit([]byte{ + sumA[12], sumA[6], sumA[0], + sumA[13], sumA[7], sumA[1], + sumA[14], sumA[8], sumA[2], + sumA[15], sumA[9], sumA[3], + sumA[5], sumA[10], sumA[4], + sumA[11], + })) + return buf.String(), nil +} + +func (c *crypter) Verify(hashedKey string, key []byte) error { + newHash, err := c.Generate(key, []byte(hashedKey)) + if err != nil { + return err + } + if subtle.ConstantTimeCompare([]byte(newHash), []byte(hashedKey)) != 1 { + return crypt.ErrKeyMismatch + } + return nil +} + +func (c *crypter) Cost(hashedKey string) (int, error) { return RoundsDefault, nil } + +func (c *crypter) SetSalt(salt common.Salt) { c.Salt = salt } diff --git a/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go b/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go new file mode 100644 index 00000000..593f6530 --- /dev/null +++ b/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go @@ -0,0 +1,77 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package pbkdf2 implements the key derivation function PBKDF2 as defined in RFC +2898 / PKCS #5 v2.0. + +A key derivation function is useful when encrypting data based on a password +or any other not-fully-random data. It uses a pseudorandom function to derive +a secure encryption key based on the password. + +While v2.0 of the standard defines only one pseudorandom function to use, +HMAC-SHA1, the drafted v2.1 specification allows use of all five FIPS Approved +Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To +choose, you can pass the `New` functions from the different SHA packages to +pbkdf2.Key. +*/ +package pbkdf2 // import "golang.org/x/crypto/pbkdf2" + +import ( + "crypto/hmac" + "hash" +) + +// Key derives a key from the password, salt and iteration count, returning a +// []byte of length keylen that can be used as cryptographic key. The key is +// derived based on the method described as PBKDF2 with the HMAC variant using +// the supplied hash function. +// +// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you +// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by +// doing: +// +// dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New) +// +// Remember to get a good random salt. At least 8 bytes is recommended by the +// RFC. +// +// Using a higher iteration count will increase the cost of an exhaustive +// search but will also make derivation proportionally slower. +func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte { + prf := hmac.New(h, password) + hashLen := prf.Size() + numBlocks := (keyLen + hashLen - 1) / hashLen + + var buf [4]byte + dk := make([]byte, 0, numBlocks*hashLen) + U := make([]byte, hashLen) + for block := 1; block <= numBlocks; block++ { + // N.B.: || means concatenation, ^ means XOR + // for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter + // U_1 = PRF(password, salt || uint(i)) + prf.Reset() + prf.Write(salt) + buf[0] = byte(block >> 24) + buf[1] = byte(block >> 16) + buf[2] = byte(block >> 8) + buf[3] = byte(block) + prf.Write(buf[:4]) + dk = prf.Sum(dk) + T := dk[len(dk)-hashLen:] + copy(U, T) + + // U_n = PRF(password, U_(n-1)) + for n := 2; n <= iter; n++ { + prf.Reset() + prf.Write(U) + U = U[:0] + U = prf.Sum(U) + for x := range U { + T[x] ^= U[x] + } + } + } + return dk[:keyLen] +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 108fd21a..561000f8 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,12 @@ # code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 ## explicit code.cloudfoundry.org/bytefmt +# github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 +## explicit +github.com/GehirnInc/crypt +github.com/GehirnInc/crypt/common +github.com/GehirnInc/crypt/internal +github.com/GehirnInc/crypt/md5_crypt # github.com/dgrijalva/jwt-go v3.2.0+incompatible ## explicit github.com/dgrijalva/jwt-go @@ -56,6 +62,7 @@ github.com/toorop/go-dkim ## explicit golang.org/x/crypto/bcrypt golang.org/x/crypto/blowfish +golang.org/x/crypto/pbkdf2 golang.org/x/crypto/sha3 golang.org/x/crypto/ssh/terminal # golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f From a19ac34acee1e70972478de1c42a1be1b1aff02a Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 5 Oct 2020 11:44:22 -0400 Subject: [PATCH 2/3] check +F instead of +q for founder --- distrib/atheme/atheme2json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distrib/atheme/atheme2json.py b/distrib/atheme/atheme2json.py index 05a588bb..c30fe99c 100644 --- a/distrib/atheme/atheme2json.py +++ b/distrib/atheme/atheme2json.py @@ -68,7 +68,7 @@ def convert(infile): set_at = int(parts[4]) if 'amode' not in chdata: chdata['amode'] = {} - if 'q' in flags: + if 'F' in flags: # there can only be one founder preexisting_founder, preexisting_set_at = channel_to_founder[chname] if preexisting_founder is None or set_at < preexisting_set_at: From 1ec029a53b8a45613542f273c0d27e8c9607944a Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 6 Oct 2020 17:38:47 -0400 Subject: [PATCH 3/3] review fixes 1. Avoid undefined behavior of time.Time{}.UnixNano() 2. Times should be compared with Equal() --- irc/channelreg.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/irc/channelreg.go b/irc/channelreg.go index 5a5a0335..f7196b12 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -200,8 +200,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey)) topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey)) topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey)) - topicSetTime, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey)) - topicSetTimeInt, _ := strconv.ParseInt(topicSetTime, 10, 64) + var topicSetTime time.Time + topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey)) + if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil { + topicSetTime = time.Unix(0, topicSetTimeInt).UTC() + } password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey)) modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey)) userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey)) @@ -237,7 +240,7 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC Founder: founder, Topic: topic, TopicSetBy: topicSetBy, - TopicSetTime: time.Unix(0, topicSetTimeInt).UTC(), + TopicSetTime: topicSetTime, Key: password, Modes: modeSlice, Bans: banlist, @@ -277,7 +280,7 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key)) // to see if we're deleting the right channel, confirm the founder and the registration time - if founder == info.Founder && registeredAt == info.RegisteredAt { + if founder == info.Founder && registeredAt.Equal(info.RegisteredAt) { for _, keyFmt := range channelKeyStrings { tx.Delete(fmt.Sprintf(keyFmt, key)) } @@ -345,7 +348,11 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha if includeFlags&IncludeTopic != 0 { tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil) - tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10), nil) + var topicSetTimeStr string + if !channelInfo.TopicSetTime.IsZero() { + topicSetTimeStr = strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10) + } + tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), topicSetTimeStr, nil) tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil) }