mirror of
https://github.com/jeremyd/ergo.git
synced 2026-07-13 19:38:11 -07:00
refactor the password hashing / password autoupgrade system
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) 2018 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package passwd
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
import "golang.org/x/crypto/sha3"
|
||||
|
||||
const (
|
||||
MinCost = bcrypt.MinCost
|
||||
DefaultCost = 12 // ballpark: 250 msec on a modern Intel CPU
|
||||
)
|
||||
|
||||
// implements Dropbox's strategy of applying an initial pass of a "normal"
|
||||
// (i.e., fast) cryptographically secure hash with 512 bits of output before
|
||||
// applying bcrypt. This allows the use of, e.g., Diceware/XKCD-style passphrases
|
||||
// that may be longer than the 80-character bcrypt limit.
|
||||
// https://blogs.dropbox.com/tech/2016/09/how-dropbox-securely-stores-your-passwords/
|
||||
|
||||
// we are only using this for user-generated passwords, as opposed to the server
|
||||
// and operator passwords that are hashed by `oragono genpasswd` and then
|
||||
// hard-coded by the server admins into the config file, to avoid breaking
|
||||
// backwards compatibility (since we can't upgrade the config file on the fly
|
||||
// the way we can with the database).
|
||||
|
||||
func GenerateFromPassword(password []byte, cost int) (result []byte, err error) {
|
||||
sum := sha3.Sum512(password)
|
||||
return bcrypt.GenerateFromPassword(sum[:], cost)
|
||||
}
|
||||
|
||||
func CompareHashAndPassword(hashedPassword, password []byte) error {
|
||||
sum := sha3.Sum512(password)
|
||||
return bcrypt.CompareHashAndPassword(hashedPassword, sum[:])
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2018 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package passwd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBasic(t *testing.T) {
|
||||
hash, err := GenerateFromPassword([]byte("this is my passphrase"), DefaultCost)
|
||||
if err != nil || len(hash) != 60 {
|
||||
t.Errorf("bad password hash output: error %s, output %s, len %d", err, hash, len(hash))
|
||||
}
|
||||
|
||||
if CompareHashAndPassword(hash, []byte("this is my passphrase")) != nil {
|
||||
t.Errorf("hash comparison failed unexpectedly")
|
||||
}
|
||||
|
||||
if CompareHashAndPassword(hash, []byte("this is not my passphrase")) == nil {
|
||||
t.Errorf("hash comparison succeeded unexpectedly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongPassphrases(t *testing.T) {
|
||||
longPassphrase := make([]byte, 168)
|
||||
for i := range longPassphrase {
|
||||
longPassphrase[i] = 'a'
|
||||
}
|
||||
hash, err := GenerateFromPassword(longPassphrase, DefaultCost)
|
||||
if err != nil {
|
||||
t.Errorf("bad password hash output: error %s", err)
|
||||
}
|
||||
|
||||
if CompareHashAndPassword(hash, longPassphrase) != nil {
|
||||
t.Errorf("hash comparison failed unexpectedly")
|
||||
}
|
||||
|
||||
// change a byte of the passphrase beyond the normal 80-character
|
||||
// bcrypt truncation boundary:
|
||||
longPassphrase[150] = 'b'
|
||||
if CompareHashAndPassword(hash, longPassphrase) == nil {
|
||||
t.Errorf("hash comparison succeeded unexpectedly")
|
||||
}
|
||||
}
|
||||
|
||||
// this could be useful for tuning the cost parameter on specific hardware
|
||||
func BenchmarkComparisons(b *testing.B) {
|
||||
pass := []byte("passphrase for benchmarking")
|
||||
hash, err := GenerateFromPassword(pass, DefaultCost)
|
||||
if err != nil {
|
||||
b.Errorf("bad output")
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
CompareHashAndPassword(hash, pass)
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package passwd
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
// newSaltLen is how many bytes long newly-generated salts are.
|
||||
newSaltLen = 30
|
||||
// defaultPasswordCost is the bcrypt cost we use for passwords.
|
||||
defaultPasswordCost = 14
|
||||
)
|
||||
|
||||
// NewSalt returns a salt for crypto uses.
|
||||
func NewSalt() ([]byte, error) {
|
||||
salt := make([]byte, newSaltLen)
|
||||
_, err := rand.Read(salt)
|
||||
|
||||
if err != nil {
|
||||
var emptySalt []byte
|
||||
return emptySalt, err
|
||||
}
|
||||
|
||||
return salt, nil
|
||||
}
|
||||
|
||||
// SaltedManager supports the hashing and comparing of passwords with the given salt.
|
||||
type SaltedManager struct {
|
||||
salt []byte
|
||||
}
|
||||
|
||||
// NewSaltedManager returns a new SaltedManager with the given salt.
|
||||
func NewSaltedManager(salt []byte) SaltedManager {
|
||||
return SaltedManager{
|
||||
salt: salt,
|
||||
}
|
||||
}
|
||||
|
||||
// assemblePassword returns an assembled slice of bytes for the given password details.
|
||||
func (sm *SaltedManager) assemblePassword(specialSalt []byte, password string) []byte {
|
||||
var assembledPasswordBytes []byte
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, sm.salt...)
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, '-')
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, specialSalt...)
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, '-')
|
||||
assembledPasswordBytes = append(assembledPasswordBytes, []byte(password)...)
|
||||
return assembledPasswordBytes
|
||||
}
|
||||
|
||||
// GenerateFromPassword encrypts the given password.
|
||||
func (sm *SaltedManager) GenerateFromPassword(specialSalt []byte, password string) ([]byte, error) {
|
||||
assembledPasswordBytes := sm.assemblePassword(specialSalt, password)
|
||||
return bcrypt.GenerateFromPassword(assembledPasswordBytes, defaultPasswordCost)
|
||||
}
|
||||
|
||||
// CompareHashAndPassword compares a hashed password with its possible plaintext equivalent.
|
||||
// Returns nil on success, or an error on failure.
|
||||
func (sm *SaltedManager) CompareHashAndPassword(hashedPassword []byte, specialSalt []byte, password string) error {
|
||||
assembledPasswordBytes := sm.assemblePassword(specialSalt, password)
|
||||
return bcrypt.CompareHashAndPassword(hashedPassword, assembledPasswordBytes)
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package passwd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type SaltedPasswordTest struct {
|
||||
ManagerSalt string
|
||||
Salt string
|
||||
Hash string
|
||||
Password string
|
||||
}
|
||||
|
||||
var SaltedPasswords = []SaltedPasswordTest{
|
||||
{
|
||||
ManagerSalt: "3TPITDVf/NGb4OlCyV1uZNW1H7zy3BFos+Dsu7dj",
|
||||
Salt: "b6oVqshJUfcm1zWEtqwKqUVylqLONAZfqt17ns+Y",
|
||||
Hash: "JDJhJDE0JFYuT28xOFFNZldaaTI1UWpzNENMeHVKdm5vS1lkL2tFL1lFVkQ2a0loUEY2Vzk3UTZSVDVP",
|
||||
Password: "test",
|
||||
},
|
||||
{
|
||||
ManagerSalt: "iNGeNEfuPihM8kYDZ/C6qAJ0JERKeKkUYp6wYDU0",
|
||||
Salt: "U7TA6k6VLSLHfdjSsQH0vc3Jqq6cUezJNyd0DC9c",
|
||||
Hash: "JDJhJDE0JEguY2Rva3VOTVRrNm1VeGdXWjAwamViMGNvV0xYZFdHcTZjenFCRWE3Ymt2N1JiSFJDZlYy",
|
||||
Password: "test2",
|
||||
},
|
||||
{
|
||||
ManagerSalt: "ghKJaaSNTjuFmgLRqrgY4FGfx8wXEGOBE02PZvbv",
|
||||
Salt: "NO/mtrMhGjX1FGDGdpGrDJIi4jxsb0aFa7ybId7r",
|
||||
Hash: "JDJhJDE0JEI0M055Z2NDcjNUanB5ZEJ5MzUybi5FT3o4Y1MyNXp2c1NDVS9hS0hOcUxSRDZTWmUxTnN5",
|
||||
Password: "supermono",
|
||||
},
|
||||
}
|
||||
|
||||
func TestSaltedPassword(t *testing.T) {
|
||||
// check newly-generated password
|
||||
managerSalt, err := NewSalt()
|
||||
if err != nil {
|
||||
t.Error("Could not generate manager salt")
|
||||
}
|
||||
|
||||
salt, err := NewSalt()
|
||||
if err != nil {
|
||||
t.Error("Could not generate salt")
|
||||
}
|
||||
|
||||
manager := NewSaltedManager(managerSalt)
|
||||
|
||||
passHash, err := manager.GenerateFromPassword(salt, "this is a test password")
|
||||
if err != nil {
|
||||
t.Error("Could not generate from password")
|
||||
}
|
||||
|
||||
if manager.CompareHashAndPassword(passHash, salt, "this is a test password") != nil {
|
||||
t.Error("Generated password does not match")
|
||||
}
|
||||
|
||||
// check our stored passwords
|
||||
for i, info := range SaltedPasswords {
|
||||
// decode strings to bytes
|
||||
managerSalt, err = base64.StdEncoding.DecodeString(info.ManagerSalt)
|
||||
if err != nil {
|
||||
t.Errorf("Could not decode manager salt for test %d", i)
|
||||
}
|
||||
|
||||
salt, err := base64.StdEncoding.DecodeString(info.Salt)
|
||||
if err != nil {
|
||||
t.Errorf("Could not decode salt for test %d", i)
|
||||
}
|
||||
|
||||
hash, err := base64.StdEncoding.DecodeString(info.Hash)
|
||||
if err != nil {
|
||||
t.Errorf("Could not decode hash for test %d", i)
|
||||
}
|
||||
|
||||
// make sure our test values are still correct
|
||||
manager := NewSaltedManager(managerSalt)
|
||||
if manager.CompareHashAndPassword(hash, salt, info.Password) != nil {
|
||||
t.Errorf("Password does not match for [%s]", info.Password)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) 2012-2014 Jeremy Latt
|
||||
// released under the MIT license
|
||||
|
||||
package passwd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrEmptyPassword means that an empty password was given.
|
||||
ErrEmptyPassword = errors.New("empty password")
|
||||
)
|
||||
|
||||
// GenerateEncodedPasswordBytes returns an encrypted password, returning the bytes directly.
|
||||
func GenerateEncodedPasswordBytes(passwd string) (encoded []byte, err error) {
|
||||
if passwd == "" {
|
||||
err = ErrEmptyPassword
|
||||
return
|
||||
}
|
||||
encoded, err = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.MinCost)
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateEncodedPassword returns an encrypted password, encoded into a string with base64.
|
||||
func GenerateEncodedPassword(passwd string) (encoded string, err error) {
|
||||
bcrypted, err := GenerateEncodedPasswordBytes(passwd)
|
||||
encoded = base64.StdEncoding.EncodeToString(bcrypted)
|
||||
return
|
||||
}
|
||||
|
||||
// DecodePasswordHash takes a base64-encoded password hash and returns the appropriate bytes.
|
||||
func DecodePasswordHash(encoded string) (decoded []byte, err error) {
|
||||
if encoded == "" {
|
||||
err = ErrEmptyPassword
|
||||
return
|
||||
}
|
||||
decoded, err = base64.StdEncoding.DecodeString(encoded)
|
||||
return
|
||||
}
|
||||
|
||||
// ComparePassword compares a given password with the given hash.
|
||||
func ComparePassword(hash, password []byte) error {
|
||||
return bcrypt.CompareHashAndPassword(hash, password)
|
||||
}
|
||||
|
||||
// ComparePasswordString compares a given password string with the given hash.
|
||||
func ComparePasswordString(hash []byte, password string) error {
|
||||
return ComparePassword(hash, []byte(password))
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package passwd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var UnsaltedPasswords = map[string]string{
|
||||
"test1": "JDJhJDA0JFFwZ1V0RWZTMFVaMkFrdlRrTG9FZk9FNEZWbWkvVEhsdGFnSXlIUC5wVmpYTkNERFJPNlcu",
|
||||
"test2": "JDJhJDA0JHpQTGNqczlIanc3V2NFQ3JEOVlTM09aNkRTbGRsQzRyNmt3Q01aSUs2Y2xyWURVODZ1V0px",
|
||||
"supernomo": "JDJhJDA0JHdJekhnQmk1VXQ4WUphL0pIL0tXQWVKVXJ6dXcvRDJ3WFljWW9XOGhzNllIbW1DRlFkL1VL",
|
||||
}
|
||||
|
||||
func TestUnsaltedPassword(t *testing.T) {
|
||||
for password, hash := range UnsaltedPasswords {
|
||||
generatedHash, err := GenerateEncodedPassword(password)
|
||||
if err != nil {
|
||||
t.Errorf("Could not hash password for [%s]: %s", password, err.Error())
|
||||
}
|
||||
|
||||
hashBytes, err := DecodePasswordHash(hash)
|
||||
if err != nil {
|
||||
t.Errorf("Could not decode hash for [%s]: %s", password, err.Error())
|
||||
}
|
||||
|
||||
generatedHashBytes, err := DecodePasswordHash(generatedHash)
|
||||
if err != nil {
|
||||
t.Errorf("Could not decode generated hash for [%s]: %s", password, err.Error())
|
||||
}
|
||||
|
||||
passwordBytes := []byte(password)
|
||||
|
||||
if ComparePassword(hashBytes, passwordBytes) != nil {
|
||||
t.Errorf("Stored hash for [%s] did not match", password)
|
||||
}
|
||||
if ComparePassword(generatedHashBytes, passwordBytes) != nil {
|
||||
t.Errorf("Generated hash for [%s] did not match", password)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsaltedPasswordFailures(t *testing.T) {
|
||||
_, err := GenerateEncodedPassword("")
|
||||
if err != ErrEmptyPassword {
|
||||
t.Error("Generating empty password did not fail as expected!")
|
||||
}
|
||||
|
||||
_, err = DecodePasswordHash("")
|
||||
if err != ErrEmptyPassword {
|
||||
t.Error("Decoding empty password hash did not fail as expected!")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user