diff options
| author | Guillaume Horel <guillaume.horel@gmail.com> | 2019-10-17 22:33:34 -0400 |
|---|---|---|
| committer | Guillaume Horel <guillaume.horel@gmail.com> | 2019-10-17 22:33:34 -0400 |
| commit | a177c2ec2d897ff75ac2c6bcc4d248cd2db20d5c (patch) | |
| tree | d3528337b72a522e00d93c02f63235b608db78e7 | |
| parent | 2ab5b1aa4c0d01a5012a7f9599fd00241f8d33a7 (diff) | |
| download | id-a177c2ec2d897ff75ac2c6bcc4d248cd2db20d5c.tar.gz | |
| -rw-r--r-- | hash.go | 174 | ||||
| -rw-r--r-- | store.go | 28 |
2 files changed, 189 insertions, 13 deletions
@@ -0,0 +1,174 @@ +package main + +import ( + "crypto/md5" + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strings" + + "golang.org/x/crypto/argon2" +) + +type PasswordHash interface { + verify(password string, hash string) (bool, error) + hash(password string) (string, error) +} + +type Md5 struct { +} + +var ( + // ErrInvalidHash in returned by ComparePasswordAndHash if the provided + // hash isn't in the expected format. + ErrInvalidHash = errors.New("argon2id: hash is not in the correct format") + + // ErrIncompatibleVersion in returned by ComparePasswordAndHash if the + // provided hash was created using a different version of Argon2. + ErrIncompatibleVersion = errors.New("argon2id: incompatible version of argon2") +) + +// DefaultParams provides some sane default parameters for hashing passwords. +// You are encouraged to change the Memory, Iterations and Parallelism parameters +// to values appropraite for the environment that your code will be running in. +var DefaultParams = &Argon2{ + Memory: 64 * 1024, + Iterations: 3, + Parallelism: 2, + SaltLength: 16, + KeyLength: 32, +} + +// Params describes the input parameters used by the Argon2id algorithm. The +// Memory and Iterations parameters control the computational cost of hashing +// the password. The higher these figures are, the greater the cost of generating +// the hash and the longer the runtime. It also follows that the greater the cost +// will be for any attacker trying to guess the password. If the code is running +// on a machine with multiple cores, then you can decrease the runtime without +// reducing the cost by increasing the Parallelism parameter. This controls the +// number of threads that the work is spread across. Important note: Changing the +// value of the Parallelism parameter changes the hash output. +// +// For guidance and an outline process for choosing appropriate parameters see +// https://tools.ietf.org/html/draft-irtf-cfrg-argon2-04#section-4 +type Argon2 struct { + // The amount of memory used by the algorithm (in kibibytes). + Memory uint32 + + // The number of iterations over the memory. + Iterations uint32 + + // The number of threads (or lanes) used by the algorithm. + Parallelism uint8 + + // Length of the random salt. 16 bytes is recommended for password hashing. + SaltLength uint32 + + // Length of the generated key. 16 bytes or more is recommended. + KeyLength uint32 +} + +func (h Argon2) verify(password, hash string) (match bool, err error) { + params, salt, key, err := decodeHash(hash) + if err != nil { + return false, err + } + + otherKey := argon2.IDKey([]byte(password), salt, params.Iterations, params.Memory, params.Parallelism, params.KeyLength) + + keyLen := int32(len(key)) + otherKeyLen := int32(len(otherKey)) + + if subtle.ConstantTimeEq(keyLen, otherKeyLen) == 0 { + return false, nil + } + if subtle.ConstantTimeCompare(key, otherKey) == 1 { + return true, nil + } + return false, nil +} + +func decodeHash(hash string) (params *Argon2, salt, key []byte, err error) { + vals := strings.Split(hash, "$") + if len(vals) != 6 { + return nil, nil, nil, ErrInvalidHash + } + + var version int + _, err = fmt.Sscanf(vals[2], "v=%d", &version) + if err != nil { + return nil, nil, nil, err + } + if version != argon2.Version { + return nil, nil, nil, ErrIncompatibleVersion + } + + params = &Argon2{} + _, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", ¶ms.Memory, ¶ms.Iterations, ¶ms.Parallelism) + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.DecodeString(vals[4]) + if err != nil { + return nil, nil, nil, err + } + params.SaltLength = uint32(len(salt)) + + key, err = base64.RawStdEncoding.DecodeString(vals[5]) + if err != nil { + return nil, nil, nil, err + } + params.KeyLength = uint32(len(key)) + + return params, salt, key, nil +} + +func (Md5) hash(password string) (string, error) { + hashsum := md5.Sum([]byte(password)) + return hex.EncodeToString(hashsum[:]), nil +} + +func (h Md5) verify(password, hash string) (match bool, err error) { + tentative_hash, _ := h.hash(password) + if subtle.ConstantTimeCompare([]byte(tentative_hash), []byte(hash)) != 1 { + return false, errors.New("Mot de passe incorrect.") + } else { + return true, nil + } +} + +// CreateHash returns a Argon2id hash of a plain-text password using the +// provided algorithm parameters. The returned hash follows the format used by +// the Argon2 reference C implementation and contains the base64-encoded Argon2id d +// derived key prefixed by the salt and parameters. It looks like this: +// +// $argon2id$v=19$m=65536,t=3,p=2$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG +// +func (h Argon2) hash(password string) (hash string, err error) { + salt, err := generateRandomBytes(h.SaltLength) + if err != nil { + return "", err + } + + key := argon2.IDKey([]byte(password), salt, h.Iterations, h.Memory, h.Parallelism, h.KeyLength) + + b64Salt := base64.RawStdEncoding.EncodeToString(salt) + b64Key := base64.RawStdEncoding.EncodeToString(key) + + hash = fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, h.Memory, h.Iterations, h.Parallelism, b64Salt, b64Key) + return hash, nil +} + +func generateRandomBytes(n uint32) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} @@ -1,8 +1,6 @@ package main import ( - "bytes" - "crypto/subtle" "database/sql" "errors" "log" @@ -20,7 +18,7 @@ type Session struct { type User struct { Id int64 Name string - Password []byte + Password string } type Store interface { @@ -96,24 +94,28 @@ func (store *PgStore) GetUser(name string) (*User, bool) { func (store *PgStore) ValidateUser(name string, password string) (*User, error) { u := &User{Name: name} row := store.QueryRow( - "SELECT id, password FROM users WHERE name = $1", + "SELECT id, password, hash_type FROM users WHERE name = $1", name, ) - if err := row.Scan(&u.Id, &u.Password); err != nil { + var hash_scheme string + if err := row.Scan(&u.Id, &u.Password, &hash_scheme); err != nil { return nil, errors.New("Utilisateur non enregistrĂ©") } - z := bytes.SplitN(u.Password, []byte("}"), 2) - scheme := string(z[0][:len(z[0])]) - true_hash := z[1] - var hash []byte - switch scheme { + var concrete_hash PasswordHash + switch hash_scheme { case "PLAIN-MD5": - hash = md5hex([]byte(password)) + concrete_hash = Md5{} + case "ARGON2ID": + concrete_hash = Argon2{} default: return nil, errors.New("Unknown password hashing scheme.") } - if subtle.ConstantTimeCompare(true_hash, hash) != 1 { - return nil, errors.New("Mot de passe incorrect.") + if ok, err := concrete_hash.verify(password, u.Password); !ok { + if err != nil { + return nil, err + } else { + return nil, errors.New("Mot de passe incorrect.") + } } else { return u, nil } |
