Last active
May 10, 2017 14:39
-
-
Save aprice/4377938df0bb297b5c01a27dca93e5d2 to your computer and use it in GitHub Desktop.
Simple, secure, best-practices password storage in Go.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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