mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-02 03:22:09 +00:00
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
68 lines
2.0 KiB
Go
68 lines
2.0 KiB
Go
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))
|
|
}
|