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:
commit
509d3f1fdd
1
Makefile
1
Makefile
@ -27,6 +27,7 @@ test:
|
||||
cd irc/email && go test . && go vet .
|
||||
cd irc/history && go test . && go vet .
|
||||
cd irc/isupport && go test . && go vet .
|
||||
cd irc/migrations && go test . && go vet .
|
||||
cd irc/modes && go test . && go vet .
|
||||
cd irc/mysql && go test . && go vet .
|
||||
cd irc/passwd && go test . && go vet .
|
||||
|
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 '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
1
go.mod
@ -4,6 +4,7 @@ go 1.15
|
||||
|
||||
require (
|
||||
code.cloudfoundry.org/bytefmt v0.0.0-20200131002437-cf55d5288a48
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
|
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/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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
132
irc/database.go
132
irc/database.go
@ -5,6 +5,7 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -23,7 +24,7 @@ const (
|
||||
// 'version' of the database schema
|
||||
keySchemaVersion = "db.version"
|
||||
// latest schema of the db
|
||||
latestDbSchema = "12"
|
||||
latestDbSchema = "14"
|
||||
|
||||
keyCloakSecret = "crypto.cloak_secret"
|
||||
)
|
||||
@ -39,19 +40,26 @@ type SchemaChange struct {
|
||||
// maps an initial version to a schema change capable of upgrading it
|
||||
var schemaChanges map[string]SchemaChange
|
||||
|
||||
// InitDB creates the database, implementing the `oragono initdb` command.
|
||||
func InitDB(path string) {
|
||||
func checkDBReadyForInit(path string) error {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
log.Fatal("Datastore already exists (delete it manually to continue): ", path)
|
||||
return fmt.Errorf("Datastore already exists (delete it manually to continue): %s", path)
|
||||
} else if !os.IsNotExist(err) {
|
||||
log.Fatal("Datastore path is inaccessible: ", err.Error())
|
||||
return fmt.Errorf("Datastore path %s is inaccessible: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitDB creates the database, implementing the `oragono initdb` command.
|
||||
func InitDB(path string) error {
|
||||
if err := checkDBReadyForInit(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = initializeDB(path)
|
||||
if err != nil {
|
||||
log.Fatal("Could not save datastore: ", err.Error())
|
||||
if err := initializeDB(path); err != nil {
|
||||
return fmt.Errorf("Could not save datastore: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// internal database initialization code
|
||||
@ -686,6 +694,104 @@ func schemaChangeV11ToV12(config *Config, tx *buntdb.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type accountCredsLegacyV13 struct {
|
||||
Version CredentialsVersion
|
||||
PassphraseHash []byte
|
||||
Certfps []string
|
||||
}
|
||||
|
||||
// see #212 / #284. this packs the legacy salts into a single passphrase hash,
|
||||
// allowing legacy passphrases to be verified using the new API `checkLegacyPassphrase`.
|
||||
func schemaChangeV12ToV13(config *Config, tx *buntdb.Tx) error {
|
||||
salt, err := tx.Get("crypto.salt")
|
||||
if err != nil {
|
||||
return nil // no change required
|
||||
}
|
||||
tx.Delete("crypto.salt")
|
||||
rawSalt, err := base64.StdEncoding.DecodeString(salt)
|
||||
if err != nil {
|
||||
return nil // just throw away the creds at this point
|
||||
}
|
||||
prefix := "account.credentials "
|
||||
var accounts []string
|
||||
var credentials []accountCredsLegacyV13
|
||||
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
return false
|
||||
}
|
||||
account := strings.TrimPrefix(key, prefix)
|
||||
|
||||
var credsOld accountCredsLegacyV9
|
||||
err = json.Unmarshal([]byte(value), &credsOld)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
// skip if these aren't legacy creds!
|
||||
if credsOld.Version != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
var credsNew accountCredsLegacyV13
|
||||
credsNew.Version = 0 // mark hash for migration
|
||||
credsNew.Certfps = credsOld.Certfps
|
||||
credsNew.PassphraseHash = append(credsNew.PassphraseHash, rawSalt...)
|
||||
credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseSalt...)
|
||||
credsNew.PassphraseHash = append(credsNew.PassphraseHash, credsOld.PassphraseHash...)
|
||||
|
||||
accounts = append(accounts, account)
|
||||
credentials = append(credentials, credsNew)
|
||||
return true
|
||||
})
|
||||
|
||||
for i, account := range accounts {
|
||||
bytesOut, err := json.Marshal(credentials[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _, err = tx.Set(prefix+account, string(bytesOut), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// channel registration time and topic set time at nanosecond resolution
|
||||
func schemaChangeV13ToV14(config *Config, tx *buntdb.Tx) error {
|
||||
prefix := "channel.registered.time "
|
||||
var channels, times []string
|
||||
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
return false
|
||||
}
|
||||
channel := strings.TrimPrefix(key, prefix)
|
||||
channels = append(channels, channel)
|
||||
times = append(times, value)
|
||||
return true
|
||||
})
|
||||
|
||||
billion := int64(time.Second)
|
||||
for i, channel := range channels {
|
||||
regTime, err := strconv.ParseInt(times[i], 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("corrupt registration time entry for %s: %v\n", channel, err)
|
||||
continue
|
||||
}
|
||||
regTime = regTime * billion
|
||||
tx.Set(prefix+channel, strconv.FormatInt(regTime, 10), nil)
|
||||
|
||||
topicTimeKey := "channel.topic.settime " + channel
|
||||
topicSetAt, err := tx.Get(topicTimeKey)
|
||||
if err == nil {
|
||||
if setTime, err := strconv.ParseInt(topicSetAt, 10, 64); err == nil {
|
||||
tx.Set(topicTimeKey, strconv.FormatInt(setTime*billion, 10), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
allChanges := []SchemaChange{
|
||||
{
|
||||
@ -743,6 +849,16 @@ func init() {
|
||||
TargetVersion: "12",
|
||||
Changer: schemaChangeV11ToV12,
|
||||
},
|
||||
{
|
||||
InitialVersion: "12",
|
||||
TargetVersion: "13",
|
||||
Changer: schemaChangeV12ToV13,
|
||||
},
|
||||
{
|
||||
InitialVersion: "13",
|
||||
TargetVersion: "14",
|
||||
Changer: schemaChangeV13ToV14,
|
||||
},
|
||||
}
|
||||
|
||||
// build the index
|
||||
|
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 (
|
||||
"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
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:
|
||||
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
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
|
||||
## 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
|
||||
|
Loading…
Reference in New Issue
Block a user