91 lines
2.5 KiB
Go
91 lines
2.5 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
const (
|
|
// Hash formats used by the database
|
|
hashFmtPlaintext int = 0
|
|
hashFmtArgon2i_v1 int = 1
|
|
|
|
hashFmtPreferred = hashFmtArgon2i_v1
|
|
|
|
// Constant parameters for our v1 version of this
|
|
argon2_time = 1
|
|
argon2_memory = 16 * 1024 // 16 MiB
|
|
argon2_want_output_bytes = 32 // 256-bit
|
|
)
|
|
|
|
// verifyPassword checks if a provided plaintext password is valid for a known
|
|
// hashed password.
|
|
// If the verification is successful, but the used format is not the current
|
|
// preferred format, you may want to re-hash the password with the current
|
|
// preferred format.
|
|
func verifyPassword(format int, hashValue, testValue string) (bool, error) {
|
|
switch format {
|
|
case hashFmtPlaintext:
|
|
return subtle.ConstantTimeCompare([]byte(hashValue), []byte(testValue)) == 1, nil
|
|
|
|
case hashFmtArgon2i_v1:
|
|
// base64(salt) $ base64(hash)
|
|
tParts := strings.SplitN(hashValue, `$`, 2)
|
|
if len(tParts) != 2 {
|
|
return false, fmt.Errorf("malformed hash value (expected 2 segments, got %d)", len(tParts))
|
|
}
|
|
|
|
salt, err := base64.StdEncoding.DecodeString(tParts[0])
|
|
if err != nil {
|
|
return false, errors.New(`malformed hash value (malformed base64 of salt)`)
|
|
}
|
|
|
|
existingHashBytes, err := base64.StdEncoding.DecodeString(tParts[1])
|
|
if err != nil {
|
|
return false, errors.New(`malformed hash value (malformed base64 of hash)`)
|
|
}
|
|
|
|
newHash := argon2.Key([]byte(testValue), salt, argon2_time, argon2_memory, 1, argon2_want_output_bytes)
|
|
|
|
return subtle.ConstantTimeCompare(existingHashBytes, newHash) == 1, nil
|
|
|
|
default:
|
|
return false, fmt.Errorf("unrecognised password hash format %d", format)
|
|
}
|
|
}
|
|
|
|
// hashPassword converts the provided plaintext password into a hash format.
|
|
// It is recommended to pass in `hashFmtPreferred` as the format.
|
|
func hashPassword(format int, newValue string) (string, error) {
|
|
switch format {
|
|
case hashFmtPlaintext:
|
|
return newValue, nil
|
|
|
|
case hashFmtArgon2i_v1:
|
|
|
|
salt := make([]byte, 32)
|
|
n, err := rand.Read(salt)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if n != len(salt) {
|
|
return "", fmt.Errorf(`short read from urandom (got %d expected %d)`, n, len(salt))
|
|
}
|
|
|
|
newHash := argon2.Key([]byte(newValue), salt, argon2_time, argon2_memory, 1, argon2_want_output_bytes)
|
|
|
|
// base64(salt) $ base64(hash)
|
|
return base64.StdEncoding.EncodeToString(salt) + `$` + base64.StdEncoding.EncodeToString(newHash), nil
|
|
|
|
default:
|
|
return "", fmt.Errorf("unrecognised password hash format %d", format)
|
|
|
|
}
|
|
}
|