Skip to content

Instantly share code, notes, and snippets.

@aprice
Last active May 10, 2017 14:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aprice/4377938df0bb297b5c01a27dca93e5d2 to your computer and use it in GitHub Desktop.
Save aprice/4377938df0bb297b5c01a27dca93e5d2 to your computer and use it in GitHub Desktop.
Simple, secure, best-practices password storage in Go.
package password
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"fmt"
"bufio"
"errors"
"io"
"unicode/utf8"
"golang.org/x/crypto/pbkdf2"
)
const currentPasswordVersion uint8 = 1
// Password encapsulates all of the data necessary to hash
// and validate passwords securely.
type Password struct {
Version uint8
Hash []byte
Salt []byte
}
// NewPassword creates a new salt and hash for the given
// password, using the current latest password version.
func NewPassword(password string) (Password, error) {
scheme := schemes[currentPasswordVersion]
salt, err := scheme.Salt()
if err != nil {
return Password{}, err
}
return Password{
Version: currentPasswordVersion,
Hash: scheme.Hash([]byte(password), salt),
Salt: salt,
}, nil
}
// Verify that the given password string matches this hash.
func (p Password) Verify(input string) (bool, error) {
// First make sure the password itself is valid and not corrupted during store/load
if int(p.Version) >= len(schemes) {
return false, fmt.Errorf("unknown password version: %d", p.Version)
}
si := schemes[p.Version].GetInfo()
if len(p.Salt) != si.SaltLen || len(p.Hash) != si.HashLen {
return false, fmt.Errorf("invalid password, bad salt or hash length")
}
// Then validate the input matches
provided := schemes[p.Version].Hash([]byte(input), p.Salt)
return bytes.Equal(provided, p.Hash), nil
}
// NeedsUpdate returns true if the password scheme is out of date
// and needs updating. Note that this cannot be done automatically,
// because we can't get the plaintext password from the old hash to
// generate a new one.
func (p Password) NeedsUpdate() bool {
return p.Version < currentPasswordVersion
}
// Password schemes
// passwordScheme defines a secure password storage mechanism.
type passwordScheme interface {
// Salt generates the user's unique password salt
Salt() ([]byte, error)
// Hash generates a secure hash from a password and salt
Hash(password, salt []byte) []byte
// GetInfo returns metadata about this password scheme
GetInfo() schemeInfo
}
// schemeInfo provides details for sanity checking password data.
type schemeInfo struct {
Version uint8
SaltLen int
HashLen int
}
// Any scheme used should be registered here.
var schemes = []passwordScheme{
nil,
new(passwordV1),
}
// Password Version 1
// - PBKDF2-HMAC-SHA256
// - 16 byte salt
// - 10,000 iterations
// - 32 byte hash
// - Based on NIST and OWASP recommendations for 2016 and drafts for 2017
type passwordV1 struct{}
func (p *passwordV1) Salt() ([]byte, error) {
salt := make([]byte, p.GetInfo().SaltLen)
_, err := rand.Read(salt)
return salt, err
}
func (p *passwordV1) Hash(password, salt []byte) []byte {
return pbkdf2.Key(password, salt, 10000, p.GetInfo().HashLen, sha256.New)
}
func (p *passwordV1) GetInfo() schemeInfo {
return schemeInfo{Version: 1, SaltLen: 16, HashLen: 32}
}
// Password rules
var (
// ErrPasswordTooShort indicates a password doesn't meet the minimum length requirement
ErrPasswordTooShort = fmt.Errorf("password too short, must be at least %d characters", minPasswordLength)
// ErrPasswordTooLong indicates a password doesn't meet the maximum length requirement
ErrPasswordTooLong = fmt.Errorf("password too long, must be no more than %d characters", maxPasswordLength)
// ErrPasswordTooRepetitive indicates a password doesn't contain enough unique characters
ErrPasswordTooRepetitive = errors.New("password too repetitive")
// ErrPasswordTooCommon indicates a password is in the list of most common passwords
ErrPasswordTooCommon = errors.New("password too common")
)
const minPasswordLength = 8
const maxPasswordLength = 128 // We only have a max to avoid fatal hash overdose
// No mutex because this should only be written once, at startup.
var mostCommonPasswords map[string]struct{}
// LoadCommonPasswords builds a list of common passwords from a newline-delimited input stream.
// E.g. https://github.com/danielmiessler/SecLists/tree/master/Passwords
func LoadCommonPasswords(r io.Reader) error {
mcp := make(map[string]struct{})
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
// Only add to the list if it meets the other requirements. This is why we use a local
// map to write to - otherwise we'd also compare every password to the list so far
if ValidatePassword(line) == nil {
mcp[line] = struct{}{}
}
}
mostCommonPasswords = mcp
return scanner.Err()
}
// ValidatePassword checks if the given password meets requirements. If not, an error is returned.
// In particular, we care about length (long enough to be secure, short enough not to be malicious),
// repetition (unique character count more than half the minimum character count),
// and commonality (not found in our list of the most common passwords).
// We do not care about character mix (upper/lower/alpha/number/special/etc),
// nor do we limit the mix.
// This permits user-friendly passwords while eliminating the greatest causes for security concerns.
func ValidatePassword(password string) error {
byteLen := len(password) // Number of *bytes*
runeLen := utf8.RuneCountInString(password) // Number of *characters*
// For min length we care about bytes; four two-byte runes are as secure as eight one-byte runes.
if byteLen < minPasswordLength {
return ErrPasswordTooShort
}
// For max length we care about bytes, because work factor is impacted by byte count.
if byteLen > maxPasswordLength {
return ErrPasswordTooLong
}
unique := make(map[rune]struct{}, runeLen)
for _, ch := range password {
unique[ch] = struct{}{}
}
// For repetition, we care about characters, not bytes.
if len(unique) <= runeLen/2 {
return ErrPasswordTooRepetitive
}
// If MCP has been set, check if this password is in the list.
if mostCommonPasswords != nil {
if _, ok := mostCommonPasswords[password]; ok {
return ErrPasswordTooCommon
}
}
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment