aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuillaume Horel <guillaume.horel@gmail.com>2019-10-17 22:33:34 -0400
committerGuillaume Horel <guillaume.horel@gmail.com>2019-10-17 22:33:34 -0400
commita177c2ec2d897ff75ac2c6bcc4d248cd2db20d5c (patch)
treed3528337b72a522e00d93c02f63235b608db78e7
parent2ab5b1aa4c0d01a5012a7f9599fd00241f8d33a7 (diff)
downloadid-a177c2ec2d897ff75ac2c6bcc4d248cd2db20d5c.tar.gz
proof of conceptHEADmaster
-rw-r--r--hash.go174
-rw-r--r--store.go28
2 files changed, 189 insertions, 13 deletions
diff --git a/hash.go b/hash.go
new file mode 100644
index 0000000..15f3b25
--- /dev/null
+++ b/hash.go
@@ -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", &params.Memory, &params.Iterations, &params.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
+}
diff --git a/store.go b/store.go
index 1744ada..3bfff1b 100644
--- a/store.go
+++ b/store.go
@@ -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
}