3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-22 03:49:27 +01:00

support migrating anope databases

This commit is contained in:
Shivaram Lingamneni 2020-10-12 15:06:17 -04:00
parent 4336f56204
commit 82be9a8423
10 changed files with 790 additions and 44 deletions

165
distrib/anope/anope2json.py Normal file
View File

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

View File

@ -1,3 +1,5 @@
#!/usr/bin/python3
import json import json
import logging import logging
import sys import sys
@ -6,6 +8,14 @@ from collections import defaultdict
def to_unixnano(timestamp): def to_unixnano(timestamp):
return int(timestamp) * (10**9) 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): def convert(infile):
out = { out = {
'version': 1, 'version': 1,
@ -17,8 +27,8 @@ def convert(infile):
channel_to_founder = defaultdict(lambda: (None, None)) channel_to_founder = defaultdict(lambda: (None, None))
for line in infile: for line in infile:
line = line.strip() line = line.rstrip('\r\n')
parts = line.split() parts = line.split(' ')
category = parts[0] category = parts[0]
if category == 'MU': if category == 'MU':
# user account # user account
@ -43,8 +53,23 @@ def convert(infile):
elif category == 'MC': elif category == 'MC':
# channel registration # channel registration
# MC #mychannel 1600134478 1600467343 +v 272 0 0 # MC #mychannel 1600134478 1600467343 +v 272 0 0
# MC #NEWCHANNELTEST 1602270889 1602270974 +vg 1 0 0 jaeger4
chname = parts[1] 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': elif category == 'MDC':
# auxiliary data for a channel registration # auxiliary data for a channel registration
# MDC #mychannel private:topic:setter s # MDC #mychannel private:topic:setter s
@ -68,6 +93,7 @@ def convert(infile):
set_at = int(parts[4]) set_at = int(parts[4])
if 'amode' not in chdata: if 'amode' not in chdata:
chdata['amode'] = {} chdata['amode'] = {}
# see libathemecore/flags.c: +o is op, +O is autoop, etc.
if 'F' in flags: if 'F' in flags:
# there can only be one founder # there can only be one founder
preexisting_founder, preexisting_set_at = channel_to_founder[chname] preexisting_founder, preexisting_set_at = channel_to_founder[chname]
@ -75,15 +101,15 @@ def convert(infile):
chdata['founder'] = username chdata['founder'] = username
channel_to_founder[chname] = (username, set_at) channel_to_founder[chname] = (username, set_at)
# but multiple people can receive the 'q' amode # but multiple people can receive the 'q' amode
chdata['amode'][username] = ord('q') chdata['amode'][username] = 'q'
elif 'a' in flags: elif 'q' in flags:
chdata['amode'][username] = ord('a') chdata['amode'][username] = 'q'
elif 'o' in flags: elif 'o' in flags or 'O' in flags:
chdata['amode'][username] = ord('o') chdata['amode'][username] = 'o'
elif 'h' in flags: elif 'h' in flags or 'H' in flags:
chdata['amode'][username] = ord('h') chdata['amode'][username] = 'h'
elif 'v' in flags: elif 'v' in flags or 'V' in flags:
chdata['amode'][username] = ord('v') chdata['amode'][username] = 'v'
else: else:
pass pass
@ -92,9 +118,6 @@ def convert(infile):
founder = chdata.get('founder') founder = chdata.get('founder')
if founder not in out['users']: if founder not in out['users']:
raise ValueError("no user corresponding to channel founder", chname, chdata.get('founder')) 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 return out

View File

@ -1056,6 +1056,8 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
} }
case -1: case -1:
err = am.checkLegacyPassphrase(migrations.CheckAthemePassphrase, accountName, account.Credentials.PassphraseHash, passphrase) err = am.checkLegacyPassphrase(migrations.CheckAthemePassphrase, accountName, account.Credentials.PassphraseHash, passphrase)
case -2:
err = am.checkLegacyPassphrase(migrations.CheckAnopePassphrase, accountName, account.Credentials.PassphraseHash, passphrase)
default: default:
err = errAccountInvalidCredentials err = errAccountInvalidCredentials
} }
@ -1899,6 +1901,7 @@ const (
CredentialsSHA3Bcrypt CredentialsVersion = 1 CredentialsSHA3Bcrypt CredentialsVersion = 1
// negative numbers for migration // negative numbers for migration
CredentialsAtheme = -1 CredentialsAtheme = -1
CredentialsAnope = -2
) )
// AccountCredentials stores the various methods for verifying accounts. // AccountCredentials stores the various methods for verifying accounts.

View File

@ -358,11 +358,8 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
if includeFlags&IncludeModes != 0 { if includeFlags&IncludeModes != 0 {
tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil) tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil)
modeStrings := make([]string, len(channelInfo.Modes)) modeString := modes.Modes(channelInfo.Modes).String()
for i, mode := range channelInfo.Modes { tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil)
modeStrings[i] = string(mode)
}
tx.Set(fmt.Sprintf(keyChannelModes, channelKey), strings.Join(modeStrings, ""), nil)
tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil) tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil)
} }

View File

@ -9,7 +9,6 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"strconv" "strconv"
"strings"
"github.com/tidwall/buntdb" "github.com/tidwall/buntdb"
@ -17,13 +16,12 @@ import (
) )
type userImport struct { type userImport struct {
Name string Name string
Hash string Hash string
Email string Email string
RegisteredAt int64 `json:"registeredAt"` RegisteredAt int64 `json:"registeredAt"`
Vhost string Vhost string
AdditionalNicks []string `json:"additionalNicks"` AdditionalNicks []string `json:"additionalNicks"`
RegisteredChannels []string
} }
type channelImport struct { type channelImport struct {
@ -33,7 +31,10 @@ type channelImport struct {
Topic string Topic string
TopicSetBy string `json:"topicSetBy"` TopicSetBy string `json:"topicSetBy"`
TopicSetAt int64 `json:"topicSetAt"` TopicSetAt int64 `json:"topicSetAt"`
Amode map[string]int Amode map[string]string
Modes string
Key string
Limit int
} }
type databaseImport struct { type databaseImport struct {
@ -43,7 +44,23 @@ type databaseImport struct {
Channels map[string]channelImport 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 requiredVersion := 1
if dbImport.Version != requiredVersion { if dbImport.Version != requiredVersion {
return fmt.Errorf("unsupported version of the db for import: version %d is required", 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 continue
} }
credentials := AccountCredentials{ credentials := AccountCredentials{
Version: CredentialsAtheme, Version: credsType,
PassphraseHash: []byte(userInfo.Hash), PassphraseHash: []byte(userInfo.Hash),
} }
marshaledCredentials, err := json.Marshal(&credentials) marshaledCredentials, err := json.Marshal(&credentials)
@ -83,9 +100,6 @@ func doImportAthemeDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (e
if len(userInfo.AdditionalNicks) != 0 { if len(userInfo.AdditionalNicks) != 0 {
tx.Set(fmt.Sprintf(keyAccountAdditionalNicks, cfUsername), marshalReservedNicks(userInfo.AdditionalNicks), nil) 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 { 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) log.Printf("invalid channel name %s: %v", chname, err)
continue 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(keyChannelExists, cfchname), "1", nil)
tx.Set(fmt.Sprintf(keyChannelName, cfchname), chname, 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(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 != "" { if chInfo.Topic != "" {
tx.Set(fmt.Sprintf(keyChannelTopic, cfchname), chInfo.Topic, nil) 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(keyChannelTopicSetTime, cfchname), strconv.FormatInt(chInfo.TopicSetAt, 10), nil)
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, cfchname), chInfo.TopicSetBy, nil) tx.Set(fmt.Sprintf(keyChannelTopicSetBy, cfchname), chInfo.TopicSetBy, nil)
} }
if len(chInfo.Amode) != 0 { if len(chInfo.Amode) != 0 {
m, err := json.Marshal(chInfo.Amode) m, err := serializeAmodes(chInfo.Amode)
if err == nil { if err == nil {
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil) tx.Set(fmt.Sprintf(keyChannelAccountToUMode, cfchname), string(m), nil)
} else { } else {
log.Printf("couldn't serialize amodes for %s: %v", chname, err) 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 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) { func doImportDB(config *Config, dbImport databaseImport, tx *buntdb.Tx) (err error) {
switch dbImport.Source { switch dbImport.Source {
case "atheme": case "atheme":
return doImportAthemeDB(config, dbImport, tx) return doImportDBGeneric(config, dbImport, CredentialsAtheme, tx)
case "anope":
return doImportDBGeneric(config, dbImport, CredentialsAnope, tx)
default: default:
return fmt.Errorf("only imports from atheme are currently supported") return fmt.Errorf("unsupported import source: %s", dbImport.Source)
} }
} }

View File

@ -9,12 +9,14 @@ import (
"crypto/sha512" "crypto/sha512"
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
"hash" "hash"
"strconv" "strconv"
"github.com/GehirnInc/crypt/md5_crypt" "github.com/GehirnInc/crypt/md5_crypt"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/pbkdf2"
) )
@ -24,15 +26,18 @@ var (
hmacServerKeyText = []byte("Server Key") hmacServerKeyText = []byte("Server Key")
athemePBKDF2V2Prefix = []byte("$z") athemePBKDF2V2Prefix = []byte("$z")
athemeRawSHA1Prefix = []byte("$rawsha1$")
) )
type PassphraseCheck func(hash, passphrase []byte) (err error) type PassphraseCheck func(hash, passphrase []byte) (err error)
func CheckAthemePassphrase(hash, passphrase []byte) (err error) { func CheckAthemePassphrase(hash, passphrase []byte) (err error) {
if len(hash) < 60 { if bytes.HasPrefix(hash, athemeRawSHA1Prefix) {
return checkAthemePosixCrypt(hash, passphrase) return checkAthemeRawSha1(hash, passphrase)
} else if bytes.HasPrefix(hash, athemePBKDF2V2Prefix) { } else if bytes.HasPrefix(hash, athemePBKDF2V2Prefix) {
return checkAthemePBKDF2V2(hash, passphrase) return checkAthemePBKDF2V2(hash, passphrase)
} else if len(hash) < 60 {
return checkAthemePosixCrypt(hash, passphrase)
} else { } else {
return checkAthemePBKDF2(hash, passphrase) return checkAthemePBKDF2(hash, passphrase)
} }
@ -181,3 +186,96 @@ func checkAthemePBKDF2(hash, passphrase []byte) (err error) {
return ErrHashCheckFailed 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
}
}

View File

@ -11,6 +11,7 @@ import (
func TestAthemePassphrases(t *testing.T) { func TestAthemePassphrases(t *testing.T) {
var err error var err error
// modules/crypto/crypt3-md5:
err = CheckAthemePassphrase([]byte("$1$hcspif$nCm4r3S14Me9ifsOPGuJT."), []byte("shivarampassphrase")) err = CheckAthemePassphrase([]byte("$1$hcspif$nCm4r3S14Me9ifsOPGuJT."), []byte("shivarampassphrase"))
if err != nil { if err != nil {
t.Errorf("failed to check passphrase: %v", err) t.Errorf("failed to check passphrase: %v", err)
@ -21,6 +22,16 @@ func TestAthemePassphrases(t *testing.T) {
t.Errorf("accepted invalid passphrase") 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")) err = CheckAthemePassphrase([]byte("khMlbBBIFya2ihyN42abc3e768663e2c4fd0e0020e46292bf9fdf44e9a51d2a2e69509cb73b4b1bf9c1b6355a1fc9ea663fcd6da902287159494f15b905e5e651d6a60f2ec834598"), []byte("password"))
if err != nil { if err != nil {
t.Errorf("failed to check passphrase: %v", err) t.Errorf("failed to check passphrase: %v", err)
@ -31,6 +42,7 @@ func TestAthemePassphrases(t *testing.T) {
t.Errorf("accepted invalid passphrase") t.Errorf("accepted invalid passphrase")
} }
// modules/crypto/pbkdf2v2:
err = CheckAthemePassphrase([]byte("$z$65$64000$1kz1I9YJPJ2gkJALbrpL2DoxRDhYPBOg60KNJMK/6do=$Cnfg6pYhBNrVXiaXYH46byrC+3HKet/XvYwvI1BvZbs=$m0hrT33gcF90n2TU3lm8tdm9V9XC4xEV13KsjuT38iY="), []byte("password")) err = CheckAthemePassphrase([]byte("$z$65$64000$1kz1I9YJPJ2gkJALbrpL2DoxRDhYPBOg60KNJMK/6do=$Cnfg6pYhBNrVXiaXYH46byrC+3HKet/XvYwvI1BvZbs=$m0hrT33gcF90n2TU3lm8tdm9V9XC4xEV13KsjuT38iY="), []byte("password"))
if err != nil { if err != nil {
t.Errorf("failed to check passphrase: %v", err) t.Errorf("failed to check passphrase: %v", err)
@ -40,6 +52,30 @@ func TestAthemePassphrases(t *testing.T) {
if err == nil { if err == nil {
t.Errorf("accepted invalid passphrase") 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) { func TestOragonoLegacyPassphrase(t *testing.T) {
@ -70,3 +106,109 @@ func TestOragonoLegacyPassphrase(t *testing.T) {
t.Errorf("accepted invalid passphrase") 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")
}
}

128
irc/migrations/sha256.go Normal file
View File

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

View File

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

View File

@ -89,11 +89,11 @@ func (changes ModeChanges) Strings() (result []string) {
type Modes []Mode type Modes []Mode
func (modes Modes) String() string { func (modes Modes) String() string {
strs := make([]string, len(modes)) var builder strings.Builder
for index, mode := range modes { for _, m := range modes {
strs[index] = mode.String() builder.WriteRune(rune(m))
} }
return strings.Join(strs, "") return builder.String()
} }
// User Modes // User Modes