mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-07 13:56:29 +00:00
refactor(security): move all database access into pkg/security/lookup
pkg/security no longer contains SQL. Every provider calls a store interface
from lookup, implemented by a procedure backend (Postgres stored procedures,
the default there) and a direct backend (dialect-driven SQL for postgres,
sqlite, mysql and mssql with configurable table and column names).
- add sectypes, lookup, lookup/{dialect,procedure,direct,backends,ddl,conformance}
- split totp and providers sub packages out of the core package
- replace SQLNames/TableNames/QueryMode with lookup.Config (see breaking_changes.md)
- direct backend now covers column/row security and API-key login
- move txsettings SQL to lookup.ApplyTxSettings; remove password.go
- move schema scripts under lookup/, add reference DDL per dialect
- add a shared conformance suite; run it on sqlite, and on Postgres in a
podman/docker container (RESOLVESPEC_TEST_CONTAINERS=1)
- fix procedure schema bugs found on real Postgres: duplicate p_data
parameter, JSON null arrays, expires_at timezone casts, passkey list
GROUP BY, missing resolvespec_passkey_login; accept zone-less timestamps
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package direct
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// bcrypt only considers the first 72 bytes of input; longer passwords are
|
||||
// rejected rather than silently truncated.
|
||||
const maxPasswordBytes = 72
|
||||
|
||||
var errPasswordTooLong = errors.New("password must be at most 72 bytes")
|
||||
|
||||
// HashPassword returns the bcrypt hash of password.
|
||||
func HashPassword(password string) (string, error) {
|
||||
if len(password) > maxPasswordBytes {
|
||||
return "", errPasswordTooLong
|
||||
}
|
||||
h, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(h), nil
|
||||
}
|
||||
|
||||
func isBcryptHash(s string) bool {
|
||||
return strings.HasPrefix(s, "$2a$") || strings.HasPrefix(s, "$2b$") || strings.HasPrefix(s, "$2y$")
|
||||
}
|
||||
|
||||
// VerifyPassword checks supplied against the stored value. A stored bcrypt hash is compared
|
||||
// with bcrypt. A legacy cleartext value (written before hashing was implemented) is compared
|
||||
// in constant time and, on a match, needsRehash is true so the caller can upgrade the row.
|
||||
// An empty stored value (e.g. an OAuth2-only user) never matches.
|
||||
func VerifyPassword(stored, supplied string) (ok, needsRehash bool) {
|
||||
if stored == "" || supplied == "" || len(supplied) > maxPasswordBytes {
|
||||
return false, false
|
||||
}
|
||||
if isBcryptHash(stored) {
|
||||
return bcrypt.CompareHashAndPassword([]byte(stored), []byte(supplied)) == nil, false
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(stored), []byte(supplied)) == 1 {
|
||||
return true, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
var (
|
||||
dummyHashOnce sync.Once
|
||||
dummyHash string
|
||||
)
|
||||
|
||||
// BurnPasswordCheck spends roughly one bcrypt comparison so an unknown username costs about
|
||||
// the same as a wrong password.
|
||||
func BurnPasswordCheck(supplied string) {
|
||||
dummyHashOnce.Do(func() {
|
||||
h, _ := bcrypt.GenerateFromPassword([]byte("resolvespec-dummy"), bcrypt.DefaultCost)
|
||||
dummyHash = string(h)
|
||||
})
|
||||
if len(supplied) > maxPasswordBytes {
|
||||
supplied = supplied[:maxPasswordBytes]
|
||||
}
|
||||
_ = bcrypt.CompareHashAndPassword([]byte(dummyHash), []byte(supplied))
|
||||
}
|
||||
Reference in New Issue
Block a user