summaryrefslogtreecommitdiffstats
path: root/modules/auth/password/hash/hash.go
blob: 459320e1b022241eb0a16bf5277eed3cb3ea6136 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// 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
	Specification string // The specification that is used to create the internal PasswordSaltHasher
}

// 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 {
	// Some PasswordSaltHashers have their own specialised compare function that takes into
	// account the stored parameters within the hash. e.g. bcrypt
	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.Specification, 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{}
)

// MustRegister registers a PasswordSaltHasher with the availableHasherFactories
// Caution: This is not thread safe.
func MustRegister[T PasswordSaltHasher](name string, newFn func(config string) T) {
	if err := Register(name, newFn); err != nil {
		panic(err)
	}
}

// Register registers a PasswordSaltHasher with the availableHasherFactories
// Caution: This is not thread safe.
func Register[T PasswordSaltHasher](name string, newFn func(config string) T) error {
	if _, has := availableHasherFactories[name]; has {
		return fmt.Errorf("duplicate registration of password salt hasher: %s", name)
	}

	availableHasherFactories[name] = func(config string) PasswordSaltHasher {
		n := newFn(config)
		return n
	}
	return nil
}

// In early versions of gitea the password hash algorithm field of a user could be
// empty. At that point the default was `pbkdf2` without configuration values
//
// Please note this is not the same as the DefaultAlgorithm which is used
// to determine what an empty PASSWORD_HASH_ALGO setting in the app.ini means.
// These are not the same even if they have the same apparent value and they mean different things.
//
// DO NOT COALESCE THESE VALUES
const defaultEmptyHashAlgorithmSpecification = "pbkdf2"

// Parse will convert the provided algorithm specification in to a PasswordHashAlgorithm
// If the provided specification matches the DefaultHashAlgorithm Specification it will be
// used.
// In addition the last non-default hasher will be cached to help reduce the load from
// parsing specifications.
//
// NOTE: No de-aliasing is done in this function, thus any specification which does not
// contain a configuration will use the default values for that hasher. These are not
// necessarily the same values as those obtained by dealiasing. This allows for
// seamless backwards compatibility with the original configuration.
//
// To further labour this point, running `Parse("pbkdf2")` does not obtain the
// same algorithm as setting `PASSWORD_HASH_ALGO=pbkdf2` in app.ini, nor is it intended to.
// A user that has `password_hash_algo='pbkdf2'` in the db means get the original, unconfigured algorithm
// Users will be migrated automatically as they log-in to have the complete specification stored
// in their `password_hash_algo` fields by other code.
func Parse(algorithmSpec string) *PasswordHashAlgorithm {
	if algorithmSpec == "" {
		algorithmSpec = defaultEmptyHashAlgorithmSpecification
	}

	if DefaultHashAlgorithm != nil && algorithmSpec == DefaultHashAlgorithm.Specification {
		return DefaultHashAlgorithm
	}

	ptr := lastNonDefaultAlgorithm.Load()
	if ptr != nil {
		hashAlgorithm, ok := ptr.(*PasswordHashAlgorithm)
		if ok && hashAlgorithm.Specification == algorithmSpec {
			return hashAlgorithm
		}
	}

	// Now convert the provided specification in to a hasherType +/- some configuration parameters
	vals := strings.SplitN(algorithmSpec, "$", 2)
	var hasherType string
	var config string

	if len(vals) == 0 {
		// This should not happen as algorithmSpec should not be empty
		// due to it being assigned to defaultEmptyHashAlgorithmSpecification above
		// but we should be absolutely cautious here
		return nil
	}

	hasherType = vals[0]
	if len(vals) > 1 {
		config = vals[1]
	}

	newFn, has := availableHasherFactories[hasherType]
	if !has {
		// unknown hasher type
		return nil
	}

	ph := newFn(config)
	if ph == nil {
		// The provided configuration is likely invalid - it will have been logged already
		// but we cannot hash safely
		return nil
	}

	hashAlgorithm := &PasswordHashAlgorithm{
		PasswordSaltHasher: ph,
		Specification:      algorithmSpec,
	}

	lastNonDefaultAlgorithm.Store(hashAlgorithm)

	return hashAlgorithm
}