148 lines
4.2 KiB
Go
148 lines
4.2 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package hash
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
|
)
|
|
|
|
// This package takes care of hashing passwords, verifying passwords, defining
|
|
// available password algorithms, defining recommended password algorithms and
|
|
// choosing the default password algorithm.
|
|
|
|
// PasswordSaltHasher will hash a provided password with the provided saltBytes
|
|
type PasswordSaltHasher interface {
|
|
HashWithSaltBytes(password string, saltBytes []byte) string
|
|
}
|
|
|
|
// PasswordHasher will hash a provided password with the salt
|
|
type PasswordHasher interface {
|
|
Hash(password, salt string) (string, error)
|
|
}
|
|
|
|
// PasswordVerifier will ensure that a providedPassword matches the hashPassword when hashed with the salt
|
|
type PasswordVerifier interface {
|
|
VerifyPassword(providedPassword, hashedPassword, salt string) bool
|
|
}
|
|
|
|
// PasswordHashAlgorithms are named PasswordSaltHashers with a default verifier and hash function
|
|
type PasswordHashAlgorithm struct {
|
|
PasswordSaltHasher
|
|
Name string
|
|
}
|
|
|
|
// Hash the provided password with the salt and return the hash
|
|
func (algorithm *PasswordHashAlgorithm) Hash(password, salt string) (string, error) {
|
|
var saltBytes []byte
|
|
|
|
// There are two formats for the salt value:
|
|
// * The new format is a (32+)-byte hex-encoded string
|
|
// * The old format was a 10-byte binary format
|
|
// We have to tolerate both here.
|
|
if len(salt) == 10 {
|
|
saltBytes = []byte(salt)
|
|
} else {
|
|
var err error
|
|
saltBytes, err = hex.DecodeString(salt)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return algorithm.HashWithSaltBytes(password, saltBytes), nil
|
|
}
|
|
|
|
// Verify the provided password matches the hashPassword when hashed with the salt
|
|
func (algorithm *PasswordHashAlgorithm) VerifyPassword(providedPassword, hashedPassword, salt string) bool {
|
|
// The bcrypt package has its own specialized compare function that takes into
|
|
// account the stored password's bcrypt parameters.
|
|
if verifier, ok := algorithm.PasswordSaltHasher.(PasswordVerifier); ok {
|
|
return verifier.VerifyPassword(providedPassword, hashedPassword, salt)
|
|
}
|
|
|
|
// Compute the hash of the password.
|
|
providedPasswordHash, err := algorithm.Hash(providedPassword, salt)
|
|
if err != nil {
|
|
log.Error("passwordhash: %v.Hash(): %v", algorithm.Name, err)
|
|
return false
|
|
}
|
|
|
|
// Compare it against the hashed password in constant-time.
|
|
return subtle.ConstantTimeCompare([]byte(hashedPassword), []byte(providedPasswordHash)) == 1
|
|
}
|
|
|
|
var (
|
|
lastNonDefaultAlgorithm atomic.Value
|
|
availableHasherFactories = map[string]func(string) PasswordSaltHasher{}
|
|
)
|
|
|
|
// Register registers a PasswordSaltHasher with the availableHasherFactories
|
|
// This is not thread safe.
|
|
func Register[T PasswordSaltHasher](name string, newFn func(config string) T) {
|
|
if _, has := availableHasherFactories[name]; has {
|
|
panic(fmt.Errorf("duplicate registration of password salt hasher: %s", name))
|
|
}
|
|
|
|
availableHasherFactories[name] = func(config string) PasswordSaltHasher {
|
|
n := newFn(config)
|
|
return n
|
|
}
|
|
}
|
|
|
|
// In early versions of gitea the password hash algorithm field could be empty
|
|
// At that point the default was `pbkdf2` without configuration values
|
|
// Please note this is not the same as the DefaultAlgorithm
|
|
const defaultEmptyHashAlgorithmName = "pbkdf2"
|
|
|
|
func Parse(algorithm string) *PasswordHashAlgorithm {
|
|
if algorithm == "" {
|
|
algorithm = defaultEmptyHashAlgorithmName
|
|
}
|
|
|
|
if DefaultHashAlgorithm != nil && algorithm == DefaultHashAlgorithm.Name {
|
|
return DefaultHashAlgorithm
|
|
}
|
|
|
|
ptr := lastNonDefaultAlgorithm.Load()
|
|
if ptr != nil {
|
|
hashAlgorithm, ok := ptr.(*PasswordHashAlgorithm)
|
|
if ok && hashAlgorithm.Name == algorithm {
|
|
return hashAlgorithm
|
|
}
|
|
}
|
|
|
|
vals := strings.SplitN(algorithm, "$", 2)
|
|
var name string
|
|
var config string
|
|
if len(vals) == 0 {
|
|
return nil
|
|
}
|
|
name = vals[0]
|
|
if len(vals) > 1 {
|
|
config = vals[1]
|
|
}
|
|
newFn, has := availableHasherFactories[name]
|
|
if !has {
|
|
return nil
|
|
}
|
|
ph := newFn(config)
|
|
if ph == nil {
|
|
return nil
|
|
}
|
|
hashAlgorithm := &PasswordHashAlgorithm{
|
|
PasswordSaltHasher: ph,
|
|
Name: algorithm,
|
|
}
|
|
|
|
lastNonDefaultAlgorithm.Store(hashAlgorithm)
|
|
|
|
return hashAlgorithm
|
|
}
|