mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 13:29:27 +01:00
first draft of atheme migration code
This commit is contained in:
parent
c060113c74
commit
7a6413ea2c
1
Makefile
1
Makefile
@ -27,6 +27,7 @@ test:
|
|||||||
cd irc/email && go test . && go vet .
|
cd irc/email && go test . && go vet .
|
||||||
cd irc/history && go test . && go vet .
|
cd irc/history && go test . && go vet .
|
||||||
cd irc/isupport && 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/modes && go test . && go vet .
|
||||||
cd irc/mysql && go test . && go vet .
|
cd irc/mysql && go test . && go vet .
|
||||||
cd irc/passwd && go test . && go vet .
|
cd irc/passwd && go test . && go vet .
|
||||||
|
111
distrib/atheme/atheme2json.py
Normal file
111
distrib/atheme/atheme2json.py
Normal file
@ -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())
|
1
go.mod
1
go.mod
@ -4,6 +4,7 @@ go 1.15
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
|
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/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||||
github.com/go-sql-driver/mysql v1.5.0
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
|
2
go.sum
2
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 h1:/EMHruHCFXR9xClkGV/t0rmHrdhX4+trQUcBqjwc9xE=
|
||||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
|
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/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 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
|
@ -18,6 +18,7 @@ import (
|
|||||||
|
|
||||||
"github.com/oragono/oragono/irc/connection_limits"
|
"github.com/oragono/oragono/irc/connection_limits"
|
||||||
"github.com/oragono/oragono/irc/email"
|
"github.com/oragono/oragono/irc/email"
|
||||||
|
"github.com/oragono/oragono/irc/migrations"
|
||||||
"github.com/oragono/oragono/irc/modes"
|
"github.com/oragono/oragono/irc/modes"
|
||||||
"github.com/oragono/oragono/irc/passwd"
|
"github.com/oragono/oragono/irc/passwd"
|
||||||
"github.com/oragono/oragono/irc/utils"
|
"github.com/oragono/oragono/irc/utils"
|
||||||
@ -1047,17 +1048,35 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
|
|||||||
|
|
||||||
switch account.Credentials.Version {
|
switch account.Credentials.Version {
|
||||||
case 0:
|
case 0:
|
||||||
err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase)
|
err = am.checkLegacyPassphrase(migrations.CheckOragonoPassphraseV0, accountName, account.Credentials.PassphraseHash, passphrase)
|
||||||
case 1:
|
case 1:
|
||||||
if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil {
|
if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil {
|
||||||
err = errAccountInvalidCredentials
|
err = errAccountInvalidCredentials
|
||||||
}
|
}
|
||||||
|
case -1:
|
||||||
|
err = am.checkLegacyPassphrase(migrations.CheckAthemePassphrase, accountName, account.Credentials.PassphraseHash, passphrase)
|
||||||
default:
|
default:
|
||||||
err = errAccountInvalidCredentials
|
err = errAccountInvalidCredentials
|
||||||
}
|
}
|
||||||
return
|
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) {
|
func (am *AccountManager) loadWithAutocreation(accountName string, autocreate bool) (account ClientAccount, err error) {
|
||||||
account, err = am.LoadAccount(accountName)
|
account, err = am.LoadAccount(accountName)
|
||||||
if err == errAccountDoesNotExist && autocreate {
|
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.
|
// AccountCredentials stores the various methods for verifying accounts.
|
||||||
type AccountCredentials struct {
|
type AccountCredentials struct {
|
||||||
Version uint
|
Version CredentialsVersion
|
||||||
PassphraseSalt []byte // legacy field, not used by v1 and later
|
|
||||||
PassphraseHash []byte
|
PassphraseHash []byte
|
||||||
Certfps []string
|
Certfps []string
|
||||||
}
|
}
|
||||||
|
@ -233,11 +233,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
|
|||||||
info = RegisteredChannel{
|
info = RegisteredChannel{
|
||||||
Name: name,
|
Name: name,
|
||||||
NameCasefolded: nameCasefolded,
|
NameCasefolded: nameCasefolded,
|
||||||
RegisteredAt: time.Unix(regTimeInt, 0).UTC(),
|
RegisteredAt: time.Unix(0, regTimeInt).UTC(),
|
||||||
Founder: founder,
|
Founder: founder,
|
||||||
Topic: topic,
|
Topic: topic,
|
||||||
TopicSetBy: topicSetBy,
|
TopicSetBy: topicSetBy,
|
||||||
TopicSetTime: time.Unix(topicSetTimeInt, 0).UTC(),
|
TopicSetTime: time.Unix(0, topicSetTimeInt).UTC(),
|
||||||
Key: password,
|
Key: password,
|
||||||
Modes: modeSlice,
|
Modes: modeSlice,
|
||||||
Bans: banlist,
|
Bans: banlist,
|
||||||
@ -273,11 +273,11 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key))
|
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key))
|
||||||
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
|
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))
|
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key))
|
||||||
|
|
||||||
// to see if we're deleting the right channel, confirm the founder and the registration time
|
// 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 {
|
for _, keyFmt := range channelKeyStrings {
|
||||||
tx.Delete(fmt.Sprintf(keyFmt, key))
|
tx.Delete(fmt.Sprintf(keyFmt, key))
|
||||||
}
|
}
|
||||||
@ -339,13 +339,13 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
|
|||||||
if includeFlags&IncludeInitial != 0 {
|
if includeFlags&IncludeInitial != 0 {
|
||||||
tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil)
|
tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil)
|
||||||
tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, 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)
|
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if includeFlags&IncludeTopic != 0 {
|
if includeFlags&IncludeTopic != 0 {
|
||||||
tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
|
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)
|
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
132
irc/database.go
132
irc/database.go
@ -5,6 +5,7 @@
|
|||||||
package irc
|
package irc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
@ -23,7 +24,7 @@ const (
|
|||||||
// 'version' of the database schema
|
// 'version' of the database schema
|
||||||
keySchemaVersion = "db.version"
|
keySchemaVersion = "db.version"
|
||||||
// latest schema of the db
|
// latest schema of the db
|
||||||
latestDbSchema = "12"
|
latestDbSchema = "14"
|
||||||
|
|
||||||
keyCloakSecret = "crypto.cloak_secret"
|
keyCloakSecret = "crypto.cloak_secret"
|
||||||
)
|
)
|
||||||
@ -39,19 +40,26 @@ type SchemaChange struct {
|
|||||||
// maps an initial version to a schema change capable of upgrading it
|
// maps an initial version to a schema change capable of upgrading it
|
||||||
var schemaChanges map[string]SchemaChange
|
var schemaChanges map[string]SchemaChange
|
||||||
|
|
||||||
// InitDB creates the database, implementing the `oragono initdb` command.
|
func checkDBReadyForInit(path string) error {
|
||||||
func InitDB(path string) {
|
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
if err == nil {
|
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) {
|
} 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
|
||||||
}
|
}
|
||||||
|
|
||||||
err = initializeDB(path)
|
// InitDB creates the database, implementing the `oragono initdb` command.
|
||||||
if err != nil {
|
func InitDB(path string) error {
|
||||||
log.Fatal("Could not save datastore: ", err.Error())
|
if err := checkDBReadyForInit(path); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := initializeDB(path); err != nil {
|
||||||
|
return fmt.Errorf("Could not save datastore: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal database initialization code
|
// internal database initialization code
|
||||||
@ -686,6 +694,104 @@ func schemaChangeV11ToV12(config *Config, tx *buntdb.Tx) error {
|
|||||||
return nil
|
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() {
|
func init() {
|
||||||
allChanges := []SchemaChange{
|
allChanges := []SchemaChange{
|
||||||
{
|
{
|
||||||
@ -743,6 +849,16 @@ func init() {
|
|||||||
TargetVersion: "12",
|
TargetVersion: "12",
|
||||||
Changer: schemaChangeV11ToV12,
|
Changer: schemaChangeV11ToV12,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
InitialVersion: "12",
|
||||||
|
TargetVersion: "13",
|
||||||
|
Changer: schemaChangeV12ToV13,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InitialVersion: "13",
|
||||||
|
TargetVersion: "14",
|
||||||
|
Changer: schemaChangeV13ToV14,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// build the index
|
// build the index
|
||||||
|
155
irc/import.go
Normal file
155
irc/import.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// 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)
|
||||||
|
}
|
@ -5,10 +5,6 @@ package irc
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/tidwall/buntdb"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -29,44 +25,3 @@ func decodeLegacyPasswordHash(hash string) ([]byte, error) {
|
|||||||
return nil, errInvalidPasswordHash
|
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
|
|
||||||
}
|
|
||||||
|
20
irc/migrations/legacy.go
Normal file
20
irc/migrations/legacy.go
Normal file
@ -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)
|
||||||
|
}
|
183
irc/migrations/passwords.go
Normal file
183
irc/migrations/passwords.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
72
irc/migrations/passwords_test.go
Normal file
72
irc/migrations/passwords_test.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
11
oragono.go
11
oragono.go
@ -96,6 +96,7 @@ func main() {
|
|||||||
Usage:
|
Usage:
|
||||||
oragono initdb [--conf <filename>] [--quiet]
|
oragono initdb [--conf <filename>] [--quiet]
|
||||||
oragono upgradedb [--conf <filename>] [--quiet]
|
oragono upgradedb [--conf <filename>] [--quiet]
|
||||||
|
oragono importdb <database.json> [--conf <filename>] [--quiet]
|
||||||
oragono genpasswd [--conf <filename>] [--quiet]
|
oragono genpasswd [--conf <filename>] [--quiet]
|
||||||
oragono mkcerts [--conf <filename>] [--quiet]
|
oragono mkcerts [--conf <filename>] [--quiet]
|
||||||
oragono run [--conf <filename>] [--quiet] [--smoke]
|
oragono run [--conf <filename>] [--quiet] [--smoke]
|
||||||
@ -155,7 +156,10 @@ Options:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if arguments["initdb"].(bool) {
|
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) {
|
if !arguments["--quiet"].(bool) {
|
||||||
log.Println("database initialized: ", config.Datastore.Path)
|
log.Println("database initialized: ", config.Datastore.Path)
|
||||||
}
|
}
|
||||||
@ -167,6 +171,11 @@ Options:
|
|||||||
if !arguments["--quiet"].(bool) {
|
if !arguments["--quiet"].(bool) {
|
||||||
log.Println("database upgraded: ", config.Datastore.Path)
|
log.Println("database upgraded: ", config.Datastore.Path)
|
||||||
}
|
}
|
||||||
|
} else if arguments["importdb"].(bool) {
|
||||||
|
err = irc.ImportDB(config, arguments["<database.json>"].(string))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error while importing db:", err.Error())
|
||||||
|
}
|
||||||
} else if arguments["run"].(bool) {
|
} else if arguments["run"].(bool) {
|
||||||
if !arguments["--quiet"].(bool) {
|
if !arguments["--quiet"].(bool) {
|
||||||
logman.Info("server", fmt.Sprintf("%s starting", irc.Ver))
|
logman.Info("server", fmt.Sprintf("%s starting", irc.Ver))
|
||||||
|
7
vendor/github.com/GehirnInc/crypt/.travis.yml
generated
vendored
Normal file
7
vendor/github.com/GehirnInc/crypt/.travis.yml
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.6.x
|
||||||
|
- 1.7.x
|
||||||
|
- master
|
||||||
|
script:
|
||||||
|
- go test -v -race ./...
|
8
vendor/github.com/GehirnInc/crypt/AUTHORS.md
generated
vendored
Normal file
8
vendor/github.com/GehirnInc/crypt/AUTHORS.md
generated
vendored
Normal file
@ -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)
|
26
vendor/github.com/GehirnInc/crypt/LICENSE
generated
vendored
Normal file
26
vendor/github.com/GehirnInc/crypt/LICENSE
generated
vendored
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
Copyright (c) 2012, Jeramey Crawford <jeramey@antihe.ro>
|
||||||
|
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.
|
61
vendor/github.com/GehirnInc/crypt/README.rst
generated
vendored
Normal file
61
vendor/github.com/GehirnInc/crypt/README.rst
generated
vendored
Normal file
@ -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
|
||||||
|
// <nil>
|
||||||
|
}
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The documentation is available on GoDoc_.
|
||||||
|
|
||||||
|
.. _GoDoc: https://godoc.org/github.com/GehirnInc/crypt
|
59
vendor/github.com/GehirnInc/crypt/common/base64.go
generated
vendored
Normal file
59
vendor/github.com/GehirnInc/crypt/common/base64.go
generated
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
// (C) Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>. 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
|
||||||
|
}
|
10
vendor/github.com/GehirnInc/crypt/common/doc.go
generated
vendored
Normal file
10
vendor/github.com/GehirnInc/crypt/common/doc.go
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// (C) Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>. 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
|
148
vendor/github.com/GehirnInc/crypt/common/salt.go
generated
vendored
Normal file
148
vendor/github.com/GehirnInc/crypt/common/salt.go
generated
vendored
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// (C) Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>. 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
|
||||||
|
}
|
121
vendor/github.com/GehirnInc/crypt/crypt.go
generated
vendored
Normal file
121
vendor/github.com/GehirnInc/crypt/crypt.go
generated
vendored
Normal file
@ -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")
|
||||||
|
}
|
41
vendor/github.com/GehirnInc/crypt/internal/utils.go
generated
vendored
Normal file
41
vendor/github.com/GehirnInc/crypt/internal/utils.go
generated
vendored
Normal file
@ -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
|
||||||
|
}
|
143
vendor/github.com/GehirnInc/crypt/md5_crypt/md5_crypt.go
generated
vendored
Normal file
143
vendor/github.com/GehirnInc/crypt/md5_crypt/md5_crypt.go
generated
vendored
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
// (C) Copyright 2012, Jeramey Crawford <jeramey@antihe.ro>. 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 }
|
77
vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go
generated
vendored
Normal file
77
vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go
generated
vendored
Normal file
@ -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]
|
||||||
|
}
|
7
vendor/modules.txt
vendored
7
vendor/modules.txt
vendored
@ -1,6 +1,12 @@
|
|||||||
# code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
|
# code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
|
||||||
## explicit
|
## explicit
|
||||||
code.cloudfoundry.org/bytefmt
|
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
|
# github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
## explicit
|
## explicit
|
||||||
github.com/dgrijalva/jwt-go
|
github.com/dgrijalva/jwt-go
|
||||||
@ -56,6 +62,7 @@ github.com/toorop/go-dkim
|
|||||||
## explicit
|
## explicit
|
||||||
golang.org/x/crypto/bcrypt
|
golang.org/x/crypto/bcrypt
|
||||||
golang.org/x/crypto/blowfish
|
golang.org/x/crypto/blowfish
|
||||||
|
golang.org/x/crypto/pbkdf2
|
||||||
golang.org/x/crypto/sha3
|
golang.org/x/crypto/sha3
|
||||||
golang.org/x/crypto/ssh/terminal
|
golang.org/x/crypto/ssh/terminal
|
||||||
# golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f
|
# golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f
|
||||||
|
Loading…
Reference in New Issue
Block a user