mirror of
https://github.com/jeremyd/ergo.git
synced 2026-06-15 09:03:36 -07:00
Merge pull request #1301 from oragono/atheme_migration
first draft of atheme migration code
This commit is contained in:
+30
-3
@@ -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
|
||||
}
|
||||
|
||||
+15
-8
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+124
-8
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user