From 82be9a842346d16e311f48c7f4f3289fea3a40fc Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 12 Oct 2020 15:06:17 -0400 Subject: [PATCH] support migrating anope databases --- distrib/anope/anope2json.py | 165 +++++++++++++++++++++++++++++++ distrib/atheme/atheme2json.py | 53 +++++++--- irc/accounts.go | 3 + irc/channelreg.go | 7 +- irc/import.go | 72 ++++++++++---- irc/migrations/passwords.go | 102 ++++++++++++++++++- irc/migrations/passwords_test.go | 142 ++++++++++++++++++++++++++ irc/migrations/sha256.go | 128 ++++++++++++++++++++++++ irc/migrations/sha256block.go | 154 +++++++++++++++++++++++++++++ irc/modes/modes.go | 8 +- 10 files changed, 790 insertions(+), 44 deletions(-) create mode 100644 distrib/anope/anope2json.py create mode 100644 irc/migrations/sha256.go create mode 100644 irc/migrations/sha256block.go diff --git a/distrib/anope/anope2json.py b/distrib/anope/anope2json.py new file mode 100644 index 00000000..27a713e0 --- /dev/null +++ b/distrib/anope/anope2json.py @@ -0,0 +1,165 @@ +#!/usr/bin/python3 + +import re +import json +import logging +import sys +from collections import defaultdict, namedtuple + +AnopeObject = namedtuple('AnopeObject', ('type', 'kv')) + +MASK_MAGIC_REGEX = re.compile(r'[*?!@]') + +def access_level_to_amode(level): + try: + level = int(level) + except: + return None + if level >= 10000: + return 'q' + elif level >= 9999: + return 'a' + elif level >= 5: + return 'o' + elif level >= 4: + return 'h' + elif level >= 3: + return 'v' + else: + return None + +def to_unixnano(timestamp): + return int(timestamp) * (10**9) + +def file_to_objects(infile): + result = [] + obj = None + for line in infile: + pieces = line.rstrip('\r\n').split(' ', maxsplit=2) + if len(pieces) == 0: + logging.warning("skipping blank line in db") + continue + if pieces[0] == 'END': + result.append(obj) + obj = None + elif pieces[0] == 'OBJECT': + obj = AnopeObject(pieces[1], {}) + elif pieces[0] == 'DATA': + obj.kv[pieces[1]] = pieces[2] + else: + raise ValueError("unknown command found in anope db", pieces[0]) + return result + +ANOPE_MODENAME_TO_MODE = { + 'NOEXTERNAL': 'n', + 'TOPIC': 't', + 'INVITE': 'i', + 'NOCTCP': 'C', + 'AUDITORIUM': 'u', + 'SECRET': 's', +} + +def convert(infile): + out = { + 'version': 1, + 'source': 'anope', + 'users': defaultdict(dict), + 'channels': defaultdict(dict), + } + + objects = file_to_objects(infile) + + lastmode_channels = set() + + for obj in objects: + if obj.type == 'NickCore': + username = obj.kv['display'] + userdata = {'name': username, 'hash': obj.kv['pass'], 'email': obj.kv['email']} + out['users'][username] = userdata + elif obj.type == 'NickAlias': + username = obj.kv['nc'] + nick = obj.kv['nick'] + userdata = out['users'][username] + if username.lower() == nick.lower(): + userdata['registeredAt'] = to_unixnano(obj.kv['time_registered']) + else: + if 'additionalNicks' not in userdata: + userdata['additionalNicks'] = [] + userdata['additionalNicks'].append(nick) + elif obj.type == 'ChannelInfo': + chname = obj.kv['name'] + founder = obj.kv['founder'] + chdata = { + 'name': chname, + 'founder': founder, + 'registeredAt': to_unixnano(obj.kv['time_registered']), + 'topic': obj.kv['last_topic'], + 'topicSetBy': obj.kv['last_topic_setter'], + 'topicSetAt': to_unixnano(obj.kv['last_topic_time']), + 'amode': {founder: 'q',} + } + # DATA last_modes INVITE KEY,hunter2 NOEXTERNAL REGISTERED TOPIC + last_modes = obj.kv.get('last_modes') + if last_modes: + modes = [] + for mode_desc in last_modes.split(): + if ',' in mode_desc: + mode_name, mode_value = mode_desc.split(',', maxsplit=1) + else: + mode_name, mode_value = mode_desc, None + if mode_name == 'KEY': + chdata['key'] = mode_value + else: + modes.append(ANOPE_MODENAME_TO_MODE.get(mode_name, '')) + chdata['modes'] = ''.join(modes) + # prevent subsequent ModeLock objects from modifying the mode list further: + lastmode_channels.add(chname) + out['channels'][chname] = chdata + elif obj.type == 'ModeLock': + if obj.kv.get('set') != '1': + continue + chname = obj.kv['ci'] + if chname in lastmode_channels: + continue + chdata = out['channels'][chname] + modename = obj.kv['name'] + if modename == 'KEY': + chdata['key'] = obj.kv['param'] + else: + oragono_mode = ANOPE_MODENAME_TO_MODE.get(modename) + if oragono_mode is not None: + stored_modes = chdata.get('modes', '') + stored_modes += oragono_mode + chdata['modes'] = stored_modes + elif obj.type == 'ChanAccess': + chname = obj.kv['ci'] + target = obj.kv['mask'] + mode = access_level_to_amode(obj.kv['data']) + if mode is None: + continue + if MASK_MAGIC_REGEX.search(target): + continue + chdata = out['channels'][chname] + amode = chdata.setdefault('amode', {}) + amode[target] = mode + chdata['amode'] = amode + + # 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')) + + return out + +def main(): + if len(sys.argv) != 3: + raise Exception("Usage: anope2json.py anope.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/distrib/atheme/atheme2json.py b/distrib/atheme/atheme2json.py index c30fe99c..b96141fd 100644 --- a/distrib/atheme/atheme2json.py +++ b/distrib/atheme/atheme2json.py @@ -1,3 +1,5 @@ +#!/usr/bin/python3 + import json import logging import sys @@ -6,6 +8,14 @@ from collections import defaultdict def to_unixnano(timestamp): return int(timestamp) * (10**9) +# include/atheme/channels.h +CMODE_FLAG_TO_MODE = { + 0x001: 'i', # CMODE_INVITE + 0x010: 'n', # CMODE_NOEXT + 0x080: 's', # CMODE_SEC + 0x100: 't', # CMODE_TOPIC +} + def convert(infile): out = { 'version': 1, @@ -17,8 +27,8 @@ def convert(infile): channel_to_founder = defaultdict(lambda: (None, None)) for line in infile: - line = line.strip() - parts = line.split() + line = line.rstrip('\r\n') + parts = line.split(' ') category = parts[0] if category == 'MU': # user account @@ -43,8 +53,23 @@ def convert(infile): elif category == 'MC': # channel registration # MC #mychannel 1600134478 1600467343 +v 272 0 0 + # MC #NEWCHANNELTEST 1602270889 1602270974 +vg 1 0 0 jaeger4 chname = parts[1] - out['channels'][chname].update({'name': chname, 'registeredAt': to_unixnano(parts[2])}) + chdata = out['channels'][chname] + # XXX just give everyone +nt, regardless of lock status; they can fix it later + chdata.update({'name': chname, 'registeredAt': to_unixnano(parts[2])}) + if parts[8] != '': + chdata['key'] = parts[8] + modes = {'n', 't'} + mlock_on, mlock_off = int(parts[5]), int(parts[6]) + for flag, mode in CMODE_FLAG_TO_MODE.items(): + if flag & mlock_on != 0: + modes.add(mode) + for flag, mode in CMODE_FLAG_TO_MODE.items(): + if flag & mlock_off != 0: + modes.remove(mode) + chdata['modes'] = ''.join(modes) + chdata['limit'] = int(parts[7]) elif category == 'MDC': # auxiliary data for a channel registration # MDC #mychannel private:topic:setter s @@ -68,6 +93,7 @@ def convert(infile): set_at = int(parts[4]) if 'amode' not in chdata: chdata['amode'] = {} + # see libathemecore/flags.c: +o is op, +O is autoop, etc. if 'F' in flags: # there can only be one founder preexisting_founder, preexisting_set_at = channel_to_founder[chname] @@ -75,15 +101,15 @@ def convert(infile): 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') + chdata['amode'][username] = 'q' + elif 'q' in flags: + chdata['amode'][username] = 'q' + elif 'o' in flags or 'O' in flags: + chdata['amode'][username] = 'o' + elif 'h' in flags or 'H' in flags: + chdata['amode'][username] = 'h' + elif 'v' in flags or 'V' in flags: + chdata['amode'][username] = 'v' else: pass @@ -92,9 +118,6 @@ def convert(infile): 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 diff --git a/irc/accounts.go b/irc/accounts.go index 94e51513..f98eaada 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -1056,6 +1056,8 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou } case -1: err = am.checkLegacyPassphrase(migrations.CheckAthemePassphrase, accountName, account.Credentials.PassphraseHash, passphrase) + case -2: + err = am.checkLegacyPassphrase(migrations.CheckAnopePassphrase, accountName, account.Credentials.PassphraseHash, passphrase) default: err = errAccountInvalidCredentials } @@ -1899,6 +1901,7 @@ const ( CredentialsSHA3Bcrypt CredentialsVersion = 1 // negative numbers for migration CredentialsAtheme = -1 + CredentialsAnope = -2 ) // AccountCredentials stores the various methods for verifying accounts. diff --git a/irc/channelreg.go b/irc/channelreg.go index f7196b12..5cb6f636 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -358,11 +358,8 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha if includeFlags&IncludeModes != 0 { tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil) - modeStrings := make([]string, len(channelInfo.Modes)) - for i, mode := range channelInfo.Modes { - modeStrings[i] = string(mode) - } - tx.Set(fmt.Sprintf(keyChannelModes, channelKey), strings.Join(modeStrings, ""), nil) + modeString := modes.Modes(channelInfo.Modes).String() + tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil) tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil) } diff --git a/irc/import.go b/irc/import.go index 6893c7fc..c9b9cb28 100644 --- a/irc/import.go +++ b/irc/import.go @@ -9,7 +9,6 @@ import ( "io/ioutil" "log" "strconv" - "strings" "github.com/tidwall/buntdb" @@ -17,13 +16,12 @@ import ( ) type userImport struct { - Name string - Hash string - Email string - RegisteredAt int64 `json:"registeredAt"` - Vhost string - AdditionalNicks []string `json:"additionalNicks"` - RegisteredChannels []string + Name string + Hash string + Email string + RegisteredAt int64 `json:"registeredAt"` + Vhost string + AdditionalNicks []string `json:"additionalNicks"` } type channelImport struct { @@ -33,7 +31,10 @@ type channelImport struct { Topic string TopicSetBy string `json:"topicSetBy"` TopicSetAt int64 `json:"topicSetAt"` - Amode map[string]int + Amode map[string]string + Modes string + Key string + Limit int } type databaseImport struct { @@ -43,7 +44,23 @@ type databaseImport struct { Channels map[string]channelImport } -func doImportAthemeDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err error) { +func serializeAmodes(raw map[string]string) (result []byte, err error) { + processed := make(map[string]int, len(raw)) + for accountName, mode := range raw { + if len(mode) != 1 { + return nil, fmt.Errorf("invalid mode %s for account %s", mode, accountName) + } + cfname, err := CasefoldName(accountName) + if err != nil { + return nil, fmt.Errorf("invalid amode recipient %s: %w", accountName, err) + } + processed[cfname] = int(mode[0]) + } + result, err = json.Marshal(processed) + return +} + +func doImportDBGeneric(config *Config, dbImport databaseImport, credsType CredentialsVersion, 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) @@ -63,7 +80,7 @@ func doImportAthemeDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (e continue } credentials := AccountCredentials{ - Version: CredentialsAtheme, + Version: credsType, PassphraseHash: []byte(userInfo.Hash), } marshaledCredentials, err := json.Marshal(&credentials) @@ -83,9 +100,6 @@ func doImportAthemeDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (e 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 { @@ -94,23 +108,43 @@ func doImportAthemeDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (e log.Printf("invalid channel name %s: %v", chname, err) continue } + cffounder, err := CasefoldName(chInfo.Founder) + if err != nil { + log.Printf("invalid founder %s for channel %s: %v", chInfo.Founder, 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) + tx.Set(fmt.Sprintf(keyChannelFounder, cfchname), cffounder, nil) + accountChannelsKey := fmt.Sprintf(keyAccountChannels, cffounder) + founderChannels, fcErr := tx.Get(accountChannelsKey) + if fcErr != nil || founderChannels == "" { + founderChannels = cfchname + } else { + founderChannels = fmt.Sprintf("%s,%s", founderChannels, cfchname) + } + tx.Set(accountChannelsKey, founderChannels, 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) + m, err := serializeAmodes(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) } } + tx.Set(fmt.Sprintf(keyChannelModes, cfchname), chInfo.Modes, nil) + if chInfo.Key != "" { + tx.Set(fmt.Sprintf(keyChannelPassword, cfchname), chInfo.Key, nil) + } + if chInfo.Limit > 0 { + tx.Set(fmt.Sprintf(keyChannelUserLimit, cfchname), strconv.Itoa(chInfo.Limit), nil) + } } return nil @@ -119,9 +153,11 @@ func doImportAthemeDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (e func doImportDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err error) { switch dbImport.Source { case "atheme": - return doImportAthemeDB(config, dbImport, tx) + return doImportDBGeneric(config, dbImport, CredentialsAtheme, tx) + case "anope": + return doImportDBGeneric(config, dbImport, CredentialsAnope, tx) default: - return fmt.Errorf("only imports from atheme are currently supported") + return fmt.Errorf("unsupported import source: %s", dbImport.Source) } } diff --git a/irc/migrations/passwords.go b/irc/migrations/passwords.go index 70fc7d7a..230fdd57 100644 --- a/irc/migrations/passwords.go +++ b/irc/migrations/passwords.go @@ -9,12 +9,14 @@ import ( "crypto/sha512" "crypto/subtle" "encoding/base64" + "encoding/binary" "encoding/hex" "errors" "hash" "strconv" "github.com/GehirnInc/crypt/md5_crypt" + "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/pbkdf2" ) @@ -24,15 +26,18 @@ var ( hmacServerKeyText = []byte("Server Key") athemePBKDF2V2Prefix = []byte("$z") + athemeRawSHA1Prefix = []byte("$rawsha1$") ) type PassphraseCheck func(hash, passphrase []byte) (err error) func CheckAthemePassphrase(hash, passphrase []byte) (err error) { - if len(hash) < 60 { - return checkAthemePosixCrypt(hash, passphrase) + if bytes.HasPrefix(hash, athemeRawSHA1Prefix) { + return checkAthemeRawSha1(hash, passphrase) } else if bytes.HasPrefix(hash, athemePBKDF2V2Prefix) { return checkAthemePBKDF2V2(hash, passphrase) + } else if len(hash) < 60 { + return checkAthemePosixCrypt(hash, passphrase) } else { return checkAthemePBKDF2(hash, passphrase) } @@ -181,3 +186,96 @@ func checkAthemePBKDF2(hash, passphrase []byte) (err error) { return ErrHashCheckFailed } } + +func checkAthemeRawSha1(hash, passphrase []byte) (err error) { + return checkRawHash(hash[len(athemeRawSHA1Prefix):], passphrase, sha1.New()) +} + +func checkRawHash(expected, passphrase []byte, h hash.Hash) (err error) { + var rawExpected []byte + size := h.Size() + if len(expected) == 2*size { + rawExpected = make([]byte, h.Size()) + _, err = hex.Decode(rawExpected, expected) + if err != nil { + return ErrHashInvalid + } + } else if len(expected) == size { + rawExpected = expected + } else { + return ErrHashInvalid + } + + h.Write(passphrase) + hashedPassphrase := h.Sum(nil) + if subtle.ConstantTimeCompare(rawExpected, hashedPassphrase) == 1 { + return nil + } else { + return ErrHashCheckFailed + } +} + +func checkAnopeEncSha256(hashBytes, ivBytes, passphrase []byte) (err error) { + if len(ivBytes) != 32 { + return ErrHashInvalid + } + // https://github.com/anope/anope/blob/2cf507ed662620d0b97c8484fbfbfa09265e86e1/modules/encryption/enc_sha256.cpp#L67 + var iv [8]uint32 + for i := 0; i < 8; i++ { + iv[i] = binary.BigEndian.Uint32(ivBytes[i*4 : (i+1)*4]) + } + result := anopeSum256(passphrase, iv) + if subtle.ConstantTimeCompare(result[:], hashBytes) == 1 { + return nil + } else { + return ErrHashCheckFailed + } +} + +func CheckAnopePassphrase(hash, passphrase []byte) (err error) { + pieces := bytes.Split(hash, []byte{':'}) + if len(pieces) < 2 { + return ErrHashInvalid + } + switch string(pieces[0]) { + case "plain": + // base64, standard encoding + expectedPassphrase, err := base64.StdEncoding.DecodeString(string(pieces[1])) + if err != nil { + return ErrHashInvalid + } + if subtle.ConstantTimeCompare(passphrase, expectedPassphrase) == 1 { + return nil + } else { + return ErrHashCheckFailed + } + case "md5": + // raw MD5 + return checkRawHash(pieces[1], passphrase, md5.New()) + case "sha1": + // raw SHA-1 + return checkRawHash(pieces[1], passphrase, sha1.New()) + case "bcrypt": + if bcrypt.CompareHashAndPassword(pieces[1], passphrase) == nil { + return nil + } else { + return ErrHashCheckFailed + } + case "sha256": + // SHA-256 with an overridden IV + if len(pieces) != 3 { + return ErrHashInvalid + } + hashBytes, err := hex.DecodeString(string(pieces[1])) + if err != nil { + return ErrHashInvalid + } + ivBytes, err := hex.DecodeString(string(pieces[2])) + if err != nil { + return ErrHashInvalid + } + return checkAnopeEncSha256(hashBytes, ivBytes, passphrase) + default: + return ErrHashInvalid + } +} diff --git a/irc/migrations/passwords_test.go b/irc/migrations/passwords_test.go index 06fe2978..fc51eadc 100644 --- a/irc/migrations/passwords_test.go +++ b/irc/migrations/passwords_test.go @@ -11,6 +11,7 @@ import ( func TestAthemePassphrases(t *testing.T) { var err error + // modules/crypto/crypt3-md5: err = CheckAthemePassphrase([]byte("$1$hcspif$nCm4r3S14Me9ifsOPGuJT."), []byte("shivarampassphrase")) if err != nil { t.Errorf("failed to check passphrase: %v", err) @@ -21,6 +22,16 @@ func TestAthemePassphrases(t *testing.T) { t.Errorf("accepted invalid passphrase") } + err = CheckAthemePassphrase([]byte("$1$diwesm$9MjapdOyhyC.2FdHzKMzK."), []byte("1Ss1GN4q-3e8SgIJblfQxw")) + 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") + } + + // modules/crypto/pbkdf2: err = CheckAthemePassphrase([]byte("khMlbBBIFya2ihyN42abc3e768663e2c4fd0e0020e46292bf9fdf44e9a51d2a2e69509cb73b4b1bf9c1b6355a1fc9ea663fcd6da902287159494f15b905e5e651d6a60f2ec834598"), []byte("password")) if err != nil { t.Errorf("failed to check passphrase: %v", err) @@ -31,6 +42,7 @@ func TestAthemePassphrases(t *testing.T) { t.Errorf("accepted invalid passphrase") } + // modules/crypto/pbkdf2v2: 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) @@ -40,6 +52,30 @@ func TestAthemePassphrases(t *testing.T) { if err == nil { t.Errorf("accepted invalid passphrase") } + + weirdHash := []byte("$z$6$64000$rWfIGzPY9qiIt7m5$VdFroDOlTQSLlFUJtpvlbp2i7sH3ZUndqwdnOvoDvt6b2AzLjaAK/lhSO/QaR2nA3Wm4ObHdl3WMW32NdtSMdw==") + err = CheckAthemePassphrase(weirdHash, []byte("pHQpwje5CjS3_Lx0RaeS7w")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAthemePassphrase(weirdHash, []byte("pHQpwje5CjS3-Lx0RaeS7w")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } +} + +func TestAthemeRawSha1(t *testing.T) { + var err error + + shivaramHash := []byte("$rawsha1$49fffa5543f21dd6effe88a79633e4073e36a828") + err = CheckAthemePassphrase(shivaramHash, []byte("shivarampassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAthemePassphrase(shivaramHash, []byte("edpassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } } func TestOragonoLegacyPassphrase(t *testing.T) { @@ -70,3 +106,109 @@ func TestOragonoLegacyPassphrase(t *testing.T) { t.Errorf("accepted invalid passphrase") } } + +func TestAnopePassphraseRawSha1(t *testing.T) { + var err error + shivaramHash := []byte("sha1:49fffa5543f21dd6effe88a79633e4073e36a828") + err = CheckAnopePassphrase(shivaramHash, []byte("shivarampassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAnopePassphrase(shivaramHash, []byte("edpassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } + + edHash := []byte("sha1:ea44e256819de972c25fef0aa277396067d6024f") + err = CheckAnopePassphrase(edHash, []byte("edpassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAnopePassphrase(edHash, []byte("shivarampassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } +} + +func TestAnopePassphraseRawMd5(t *testing.T) { + var err error + shivaramHash := []byte("md5:ce4bd864f37ffaa1b871aef22eea82ff") + err = CheckAnopePassphrase(shivaramHash, []byte("shivarampassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAnopePassphrase(shivaramHash, []byte("edpassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } + + edHash := []byte("md5:dbf8be80e8dccdd33915b482e4390426") + err = CheckAnopePassphrase(edHash, []byte("edpassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAnopePassphrase(edHash, []byte("shivarampassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } +} + +func TestAnopePassphrasePlain(t *testing.T) { + var err error + // not actually a hash + weirdHash := []byte("plain:YVxzMC1fMmZ+ZjM0OEAhN2FzZGYxNDJAIyFhZmE=") + err = CheckAnopePassphrase(weirdHash, []byte("a\\s0-_2f~f348@!7asdf142@#!afa")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAnopePassphrase(weirdHash, []byte("edpassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } +} + +func TestAnopePassphraseBcrypt(t *testing.T) { + var err error + shivaramHash := []byte("bcrypt:$2a$10$UyNgHyniPukGf/3A6vzBx.VMNfej0h4WzATg4ahKW2H86a0QLcVIK") + err = CheckAnopePassphrase(shivaramHash, []byte("shivarampassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAnopePassphrase(shivaramHash, []byte("edpassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } +} + +func TestAnopePassphraseEncSha256(t *testing.T) { + var err error + shivaramHash := []byte("sha256:ff337943c8c4219cd330a3075a699492e0f8b1a823bb76af0129f1f117ba0630:60250c3053f7b34e35576fc5063b8b396fe7b9ab416842117991a8e027aa72f6") + err = CheckAnopePassphrase(shivaramHash, []byte("shivarampassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAnopePassphrase(shivaramHash, []byte("edpassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } + + edHash := []byte("sha256:93a430c8c3c6917dc6e9a32ac1aba90bc5768265278a45b86eacd636fc723d8f:10ea72683a499c155d72cd3571cb80e5050280620f789a44492c0e0c7956942f") + err = CheckAnopePassphrase(edHash, []byte("edpassphrase")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAnopePassphrase(edHash, []byte("shivarampassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } + + weirdHash := []byte("sha256:06d11a06025354e37a7ddf48913a1c9831ffab47d04e4c22a89fd7835abcb6cc:3137788c2749da0419bc9df320991d2d72495c7065da4f39004fd21710601409") + err = CheckAnopePassphrase(weirdHash, []byte("1Ss1GN4q-3e8SgIJblfQxw")) + if err != nil { + t.Errorf("failed to check passphrase: %v", err) + } + err = CheckAnopePassphrase(weirdHash, []byte("shivarampassphrase")) + if err == nil { + t.Errorf("accepted invalid passphrase") + } +} diff --git a/irc/migrations/sha256.go b/irc/migrations/sha256.go new file mode 100644 index 00000000..4c28cb18 --- /dev/null +++ b/irc/migrations/sha256.go @@ -0,0 +1,128 @@ +/* +Copyright (c) 2009 The Go Authors. 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. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +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 +OWNER 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. +*/ + +// SHA256 implementation from golang/go, modified to accommodate anope's +// password hashing scheme, which overrides the initialization vector +// using the salt. + +package migrations + +import ( + "encoding/binary" +) + +// The size of a SHA256 checksum in bytes. +const Size = 32 + +const ( + chunk = 64 +) + +// digest represents the partial evaluation of a checksum. +type digest struct { + h [8]uint32 + x [chunk]byte + nx int + len uint64 +} + +func (d *digest) Write(p []byte) (nn int, err error) { + nn = len(p) + d.len += uint64(nn) + if d.nx > 0 { + n := copy(d.x[d.nx:], p) + d.nx += n + if d.nx == chunk { + sha256BlockGeneric(d, d.x[:]) + d.nx = 0 + } + p = p[n:] + } + if len(p) >= chunk { + n := len(p) &^ (chunk - 1) + sha256BlockGeneric(d, p[:n]) + p = p[n:] + } + if len(p) > 0 { + d.nx = copy(d.x[:], p) + } + return +} + +func (d *digest) Sum(in []byte) []byte { + // Make a copy of d so that caller can keep writing and summing. + d0 := *d + hash := d0.checkSum() + return append(in, hash[:]...) +} + +func (d *digest) checkSum() [Size]byte { + len := d.len + // Padding. Add a 1 bit and 0 bits until 56 bytes mod 64. + var tmp [64]byte + tmp[0] = 0x80 + if len%64 < 56 { + d.Write(tmp[0 : 56-len%64]) + } else { + d.Write(tmp[0 : 64+56-len%64]) + } + + // Length in bits. + len <<= 3 + binary.BigEndian.PutUint64(tmp[:], len) + d.Write(tmp[0:8]) + + if d.nx != 0 { + panic("d.nx != 0") + } + + var digest [Size]byte + + binary.BigEndian.PutUint32(digest[0:], d.h[0]) + binary.BigEndian.PutUint32(digest[4:], d.h[1]) + binary.BigEndian.PutUint32(digest[8:], d.h[2]) + binary.BigEndian.PutUint32(digest[12:], d.h[3]) + binary.BigEndian.PutUint32(digest[16:], d.h[4]) + binary.BigEndian.PutUint32(digest[20:], d.h[5]) + binary.BigEndian.PutUint32(digest[24:], d.h[6]) + binary.BigEndian.PutUint32(digest[28:], d.h[7]) + + return digest +} + +// Anope password hashing function: SHA-256 with an override for the IV +// The actual SHA-256 IV for reference: +// [8]uint32{0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19} +func anopeSum256(data []byte, iv [8]uint32) [Size]byte { + var d digest + d.h = iv + d.Write(data) + return d.checkSum() +} diff --git a/irc/migrations/sha256block.go b/irc/migrations/sha256block.go new file mode 100644 index 00000000..ba6d6cf5 --- /dev/null +++ b/irc/migrations/sha256block.go @@ -0,0 +1,154 @@ +/* +Copyright (c) 2009 The Go Authors. 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. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +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 +OWNER 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. +*/ + +// SHA256 block step. +// In its own file so that a faster assembly or C version +// can be substituted easily. + +package migrations + +import "math/bits" + +var _K = []uint32{ + 0x428a2f98, + 0x71374491, + 0xb5c0fbcf, + 0xe9b5dba5, + 0x3956c25b, + 0x59f111f1, + 0x923f82a4, + 0xab1c5ed5, + 0xd807aa98, + 0x12835b01, + 0x243185be, + 0x550c7dc3, + 0x72be5d74, + 0x80deb1fe, + 0x9bdc06a7, + 0xc19bf174, + 0xe49b69c1, + 0xefbe4786, + 0x0fc19dc6, + 0x240ca1cc, + 0x2de92c6f, + 0x4a7484aa, + 0x5cb0a9dc, + 0x76f988da, + 0x983e5152, + 0xa831c66d, + 0xb00327c8, + 0xbf597fc7, + 0xc6e00bf3, + 0xd5a79147, + 0x06ca6351, + 0x14292967, + 0x27b70a85, + 0x2e1b2138, + 0x4d2c6dfc, + 0x53380d13, + 0x650a7354, + 0x766a0abb, + 0x81c2c92e, + 0x92722c85, + 0xa2bfe8a1, + 0xa81a664b, + 0xc24b8b70, + 0xc76c51a3, + 0xd192e819, + 0xd6990624, + 0xf40e3585, + 0x106aa070, + 0x19a4c116, + 0x1e376c08, + 0x2748774c, + 0x34b0bcb5, + 0x391c0cb3, + 0x4ed8aa4a, + 0x5b9cca4f, + 0x682e6ff3, + 0x748f82ee, + 0x78a5636f, + 0x84c87814, + 0x8cc70208, + 0x90befffa, + 0xa4506ceb, + 0xbef9a3f7, + 0xc67178f2, +} + +func sha256BlockGeneric(dig *digest, p []byte) { + var w [64]uint32 + h0, h1, h2, h3, h4, h5, h6, h7 := dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4], dig.h[5], dig.h[6], dig.h[7] + for len(p) >= chunk { + // Can interlace the computation of w with the + // rounds below if needed for speed. + for i := 0; i < 16; i++ { + j := i * 4 + w[i] = uint32(p[j])<<24 | uint32(p[j+1])<<16 | uint32(p[j+2])<<8 | uint32(p[j+3]) + } + for i := 16; i < 64; i++ { + v1 := w[i-2] + t1 := (bits.RotateLeft32(v1, -17)) ^ (bits.RotateLeft32(v1, -19)) ^ (v1 >> 10) + v2 := w[i-15] + t2 := (bits.RotateLeft32(v2, -7)) ^ (bits.RotateLeft32(v2, -18)) ^ (v2 >> 3) + w[i] = t1 + w[i-7] + t2 + w[i-16] + } + + a, b, c, d, e, f, g, h := h0, h1, h2, h3, h4, h5, h6, h7 + + for i := 0; i < 64; i++ { + t1 := h + ((bits.RotateLeft32(e, -6)) ^ (bits.RotateLeft32(e, -11)) ^ (bits.RotateLeft32(e, -25))) + ((e & f) ^ (^e & g)) + _K[i] + w[i] + + t2 := ((bits.RotateLeft32(a, -2)) ^ (bits.RotateLeft32(a, -13)) ^ (bits.RotateLeft32(a, -22))) + ((a & b) ^ (a & c) ^ (b & c)) + + h = g + g = f + f = e + e = d + t1 + d = c + c = b + b = a + a = t1 + t2 + } + + h0 += a + h1 += b + h2 += c + h3 += d + h4 += e + h5 += f + h6 += g + h7 += h + + p = p[chunk:] + } + + dig.h[0], dig.h[1], dig.h[2], dig.h[3], dig.h[4], dig.h[5], dig.h[6], dig.h[7] = h0, h1, h2, h3, h4, h5, h6, h7 +} diff --git a/irc/modes/modes.go b/irc/modes/modes.go index 237ef0ca..e1f2079e 100644 --- a/irc/modes/modes.go +++ b/irc/modes/modes.go @@ -89,11 +89,11 @@ func (changes ModeChanges) Strings() (result []string) { type Modes []Mode func (modes Modes) String() string { - strs := make([]string, len(modes)) - for index, mode := range modes { - strs[index] = mode.String() + var builder strings.Builder + for _, m := range modes { + builder.WriteRune(rune(m)) } - return strings.Join(strs, "") + return builder.String() } // User Modes