webscaffold/hash.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)
}
}