3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-12-22 18:52:41 +01:00

Merge pull request #1301 from oragono/atheme_migration

first draft of atheme migration code
This commit is contained in:
Shivaram Lingamneni 2020-10-06 15:03:08 -07:00 committed by GitHub
commit 509d3f1fdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1432 additions and 65 deletions

View File

@ -27,6 +27,7 @@ test:
cd irc/email && go test . && go vet .
cd irc/history && go test . && go vet .
cd irc/isupport && go test . && go vet .
cd irc/migrations && go test . && go vet .
cd irc/modes && go test . && go vet .
cd irc/mysql && go test . && go vet .
cd irc/passwd && go test . && go vet .

View 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 'F' in flags:
# there can only be one founder
preexisting_founder, preexisting_set_at = channel_to_founder[chname]
if preexisting_founder is None or set_at < preexisting_set_at:
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
View File

@ -4,6 +4,7 @@ go 1.15
require (
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/go-sql-driver/mysql v1.5.0

2
go.sum
View File

@ -1,6 +1,8 @@
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48 h1:/EMHruHCFXR9xClkGV/t0rmHrdhX4+trQUcBqjwc9xE=
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc=
github.com/DanielOaks/go-idn v0.0.0-20160120021903-76db0e10dc65/go.mod h1:GYIaL2hleNQvfMUBTes1Zd/lDTyI/p2hv3kYB4jssyU=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=

View File

@ -18,6 +18,7 @@ import (
"github.com/oragono/oragono/irc/connection_limits"
"github.com/oragono/oragono/irc/email"
"github.com/oragono/oragono/irc/migrations"
"github.com/oragono/oragono/irc/modes"
"github.com/oragono/oragono/irc/passwd"
"github.com/oragono/oragono/irc/utils"
@ -1047,17 +1048,35 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
switch account.Credentials.Version {
case 0:
err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase)
err = am.checkLegacyPassphrase(migrations.CheckOragonoPassphraseV0, accountName, account.Credentials.PassphraseHash, passphrase)
case 1:
if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil {
err = errAccountInvalidCredentials
}
case -1:
err = am.checkLegacyPassphrase(migrations.CheckAthemePassphrase, accountName, account.Credentials.PassphraseHash, passphrase)
default:
err = errAccountInvalidCredentials
}
return
}
func (am *AccountManager) checkLegacyPassphrase(check migrations.PassphraseCheck, account string, hash []byte, passphrase string) (err error) {
err = check(hash, []byte(passphrase))
if err != nil {
if err == migrations.ErrHashInvalid {
am.server.logger.Error("internal", "invalid legacy credentials for account", account)
}
return errAccountInvalidCredentials
}
// re-hash the passphrase with the latest algorithm
err = am.setPassword(account, passphrase, true)
if err != nil {
am.server.logger.Error("internal", "could not upgrade user password", err.Error())
}
return nil
}
func (am *AccountManager) loadWithAutocreation(accountName string, autocreate bool) (account ClientAccount, err error) {
account, err = am.LoadAccount(accountName)
if err == errAccountDoesNotExist && autocreate {
@ -1872,10 +1891,18 @@ var (
}
)
type CredentialsVersion int
const (
CredentialsLegacy CredentialsVersion = 0
CredentialsSHA3Bcrypt CredentialsVersion = 1
// negative numbers for migration
CredentialsAtheme = -1
)
// AccountCredentials stores the various methods for verifying accounts.
type AccountCredentials struct {
Version uint
PassphraseSalt []byte // legacy field, not used by v1 and later
Version CredentialsVersion
PassphraseHash []byte
Certfps []string
}

View File

@ -200,8 +200,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey))
topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey))
topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey))
topicSetTime, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
topicSetTimeInt, _ := strconv.ParseInt(topicSetTime, 10, 64)
var topicSetTime time.Time
topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil {
topicSetTime = time.Unix(0, topicSetTimeInt).UTC()
}
password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey))
modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey))
userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey))
@ -233,11 +236,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
info = RegisteredChannel{
Name: name,
NameCasefolded: nameCasefolded,
RegisteredAt: time.Unix(regTimeInt, 0).UTC(),
RegisteredAt: time.Unix(0, regTimeInt).UTC(),
Founder: founder,
Topic: topic,
TopicSetBy: topicSetBy,
TopicSetTime: time.Unix(topicSetTimeInt, 0).UTC(),
TopicSetTime: topicSetTime,
Key: password,
Modes: modeSlice,
Bans: banlist,
@ -273,11 +276,11 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist
if err == nil {
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key))
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
registeredAt := time.Unix(regTimeInt, 0).UTC()
registeredAt := time.Unix(0, regTimeInt).UTC()
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key))
// to see if we're deleting the right channel, confirm the founder and the registration time
if founder == info.Founder && registeredAt.Unix() == info.RegisteredAt.Unix() {
if founder == info.Founder && registeredAt.Equal(info.RegisteredAt) {
for _, keyFmt := range channelKeyStrings {
tx.Delete(fmt.Sprintf(keyFmt, key))
}
@ -339,13 +342,17 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
if includeFlags&IncludeInitial != 0 {
tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil)
tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil)
tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil)
tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.UnixNano(), 10), nil)
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
}
if includeFlags&IncludeTopic != 0 {
tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.Unix(), 10), nil)
var topicSetTimeStr string
if !channelInfo.TopicSetTime.IsZero() {
topicSetTimeStr = strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10)
}
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), topicSetTimeStr, nil)
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
}

View File

@ -5,6 +5,7 @@
package irc
import (
"encoding/base64"
"encoding/json"
"fmt"
"log"
@ -23,7 +24,7 @@ const (
// 'version' of the database schema
keySchemaVersion = "db.version"
// latest schema of the db
latestDbSchema = "12"
latestDbSchema = "14"
keyCloakSecret = "crypto.cloak_secret"
)
@ -39,19 +40,26 @@ type SchemaChange struct {
// maps an initial version to a schema change capable of upgrading it
var schemaChanges map[string]SchemaChange
// InitDB creates the database, implementing the `oragono initdb` command.
func InitDB(path string) {
func checkDBReadyForInit(path string) error {
_, err := os.Stat(path)
if err == nil {
log.Fatal("Datastore already exists (delete it manually to continue): ", path)
return fmt.Errorf("Datastore already exists (delete it manually to continue): %s", path)
} else if !os.IsNotExist(err) {
log.Fatal("Datastore path is inaccessible: ", err.Error())
return fmt.Errorf("Datastore path %s is inaccessible: %w", path, err)
}
return nil
}
// InitDB creates the database, implementing the `oragono initdb` command.
func InitDB(path string) error {
if err := checkDBReadyForInit(path); err != nil {
return err
}
err = initializeDB(path)
if err != nil {
log.Fatal("Could not save datastore: ", err.Error())
if err := initializeDB(path); err != nil {
return fmt.Errorf("Could not save datastore: %w", err)
}
return nil
}
// internal database initialization code
@ -686,6 +694,104 @@ func schemaChangeV11ToV12(config *Config, tx *buntdb.Tx) error {
return nil
}
type accountCredsLegacyV13 struct {
Version CredentialsVersion
PassphraseHash []byte
Certfps []string
}
// see #212 / #284. this packs the legacy salts into a single passphrase hash,
// allowing legacy passphrases to be verified using the new API `checkLegacyPassphrase`.
func schemaChangeV12ToV13(config *Config, tx *buntdb.Tx) error {
salt, err := tx.Get("crypto.salt")
if err != nil {
return nil // no change required
}
tx.Delete("crypto.salt")
rawSalt, err := base64.StdEncoding.DecodeString(salt)
if err != nil {
return nil // just throw away the creds at this point
}
prefix := "account.credentials "
var accounts []string
var credentials []accountCredsLegacyV13
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
account := strings.TrimPrefix(key, prefix)
var credsOld accountCredsLegacyV9
err = json.Unmarshal([]byte(value), &credsOld)
if err != nil {
return true
}
// skip if these aren't legacy creds!
if credsOld.Version != 0 {
return true
}
var credsNew accountCredsLegacyV13
credsNew.Version = 0 // mark hash for migration
credsNew.Certfps = credsOld.Certfps
credsNew.PassphraseHash = append(credsNew.PassphraseHash, rawSalt...)
credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseSalt...)
credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseHash...)
accounts = append(accounts, account)
credentials = append(credentials, credsNew)
return true
})
for i, account := range accounts {
bytesOut, err := json.Marshal(credentials[i])
if err != nil {
return err
}
_, _, err = tx.Set(prefix+account, string(bytesOut), nil)
if err != nil {
return err
}
}
return nil
}
// channel registration time and topic set time at nanosecond resolution
func schemaChangeV13ToV14(config *Config, tx *buntdb.Tx) error {
prefix := "channel.registered.time "
var channels, times []string
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
channel := strings.TrimPrefix(key, prefix)
channels = append(channels, channel)
times = append(times, value)
return true
})
billion := int64(time.Second)
for i, channel := range channels {
regTime, err := strconv.ParseInt(times[i], 10, 64)
if err != nil {
log.Printf("corrupt registration time entry for %s: %v\n", channel, err)
continue
}
regTime = regTime * billion
tx.Set(prefix+channel, strconv.FormatInt(regTime, 10), nil)
topicTimeKey := "channel.topic.settime " + channel
topicSetAt, err := tx.Get(topicTimeKey)
if err == nil {
if setTime, err := strconv.ParseInt(topicSetAt, 10, 64); err == nil {
tx.Set(topicTimeKey, strconv.FormatInt(setTime*billion, 10), nil)
}
}
}
return nil
}
func init() {
allChanges := []SchemaChange{
{
@ -743,6 +849,16 @@ func init() {
TargetVersion: "12",
Changer: schemaChangeV11ToV12,
},
{
InitialVersion: "12",
TargetVersion: "13",
Changer: schemaChangeV12ToV13,
},
{
InitialVersion: "13",
TargetVersion: "14",
Changer: schemaChangeV13ToV14,
},
}
// build the index

155
irc/import.go Normal file
View 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)
}

View File

@ -5,10 +5,6 @@ package irc
import (
"encoding/base64"
"errors"
"fmt"
"github.com/tidwall/buntdb"
"golang.org/x/crypto/bcrypt"
)
var (
@ -29,44 +25,3 @@ func decodeLegacyPasswordHash(hash string) ([]byte, error) {
return nil, errInvalidPasswordHash
}
}
// helper to check a version 0 password hash, with global and per-passphrase salts
func checkLegacyPasswordV0(hashedPassword, globalSalt, passphraseSalt []byte, passphrase string) error {
var assembledPasswordBytes []byte
assembledPasswordBytes = append(assembledPasswordBytes, globalSalt...)
assembledPasswordBytes = append(assembledPasswordBytes, '-')
assembledPasswordBytes = append(assembledPasswordBytes, passphraseSalt...)
assembledPasswordBytes = append(assembledPasswordBytes, '-')
assembledPasswordBytes = append(assembledPasswordBytes, []byte(passphrase)...)
return bcrypt.CompareHashAndPassword(hashedPassword, assembledPasswordBytes)
}
// checks a version 0 password hash; if successful, upgrades the database entry to version 1
func handleLegacyPasswordV0(server *Server, account string, credentials AccountCredentials, passphrase string) (err error) {
var globalSaltString string
err = server.store.View(func(tx *buntdb.Tx) (err error) {
globalSaltString, err = tx.Get("crypto.salt")
return err
})
if err != nil {
return err
}
globalSalt, err := base64.StdEncoding.DecodeString(globalSaltString)
if err != nil {
return err
}
err = checkLegacyPasswordV0(credentials.PassphraseHash, globalSalt, credentials.PassphraseSalt, passphrase)
if err != nil {
// invalid password
return err
}
// upgrade credentials
err = server.accounts.setPassword(account, passphrase, true)
if err != nil {
server.logger.Error("internal", fmt.Sprintf("could not upgrade user password: %v", err))
}
return nil
}

20
irc/migrations/legacy.go Normal file
View 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
View 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
}
}

View 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")
}
}

View File

@ -96,6 +96,7 @@ func main() {
Usage:
oragono initdb [--conf <filename>] [--quiet]
oragono upgradedb [--conf <filename>] [--quiet]
oragono importdb <database.json> [--conf <filename>] [--quiet]
oragono genpasswd [--conf <filename>] [--quiet]
oragono mkcerts [--conf <filename>] [--quiet]
oragono run [--conf <filename>] [--quiet] [--smoke]
@ -155,7 +156,10 @@ Options:
}
if arguments["initdb"].(bool) {
irc.InitDB(config.Datastore.Path)
err = irc.InitDB(config.Datastore.Path)
if err != nil {
log.Fatal("Error while initializing db:", err.Error())
}
if !arguments["--quiet"].(bool) {
log.Println("database initialized: ", config.Datastore.Path)
}
@ -167,6 +171,11 @@ Options:
if !arguments["--quiet"].(bool) {
log.Println("database upgraded: ", config.Datastore.Path)
}
} else if arguments["importdb"].(bool) {
err = irc.ImportDB(config, arguments["<database.json>"].(string))
if err != nil {
log.Fatal("Error while importing db:", err.Error())
}
} else if arguments["run"].(bool) {
if !arguments["--quiet"].(bool) {
logman.Info("server", fmt.Sprintf("%s starting", irc.Ver))

7
vendor/github.com/GehirnInc/crypt/.travis.yml generated vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View File

@ -1,6 +1,12 @@
# code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
## explicit
code.cloudfoundry.org/bytefmt
# github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
## explicit
github.com/GehirnInc/crypt
github.com/GehirnInc/crypt/common
github.com/GehirnInc/crypt/internal
github.com/GehirnInc/crypt/md5_crypt
# github.com/dgrijalva/jwt-go v3.2.0+incompatible
## explicit
github.com/dgrijalva/jwt-go
@ -56,6 +62,7 @@ github.com/toorop/go-dkim
## explicit
golang.org/x/crypto/bcrypt
golang.org/x/crypto/blowfish
golang.org/x/crypto/pbkdf2
golang.org/x/crypto/sha3
golang.org/x/crypto/ssh/terminal
# golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f