Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
163 lines
6.5 KiB
Go
163 lines
6.5 KiB
Go
// Copyright © 2025 Ory Corp
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
package hash
|
||
|
||
import (
|
||
"github.com/pkg/errors"
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
// ErrHashParametersOutOfBounds is returned when an imported or stored hash
|
||
// declares cost parameters that exceed the bounds enforced by this package.
|
||
// The bounds exist to prevent attacker-controlled hash parameters from causing
|
||
// memory exhaustion (process OOM) or unbounded CPU consumption when the
|
||
// password comparator is invoked.
|
||
var ErrHashParametersOutOfBounds = errors.New("hash parameters exceed allowed bounds")
|
||
|
||
// Bounds for cost parameters embedded in imported password hashes. Each value
|
||
// is chosen to comfortably accommodate strong real-world configurations while
|
||
// bounding worst-case memory and CPU consumption when the comparator runs at
|
||
// login time.
|
||
const (
|
||
// maxScryptLogN bounds the firebase-scrypt ln parameter.
|
||
// firebase-scrypt computes N = 1 << ln. Memory is 128 * N * r bytes.
|
||
// Firebase's strongest documented profile is ln=14 (~16 MiB at r=8); 17
|
||
// leaves headroom for legacy configs while bounding allocation at
|
||
// ~128 MiB at r=8.
|
||
maxScryptLogN uint32 = 17
|
||
|
||
// maxScryptN bounds plain scrypt's N directly. OWASP's Password Storage
|
||
// Cheat Sheet recommends N=2^17 (131072) as the minimum for password
|
||
// hashing; no widely-deployed password-hash system documents a larger
|
||
// value. Higher N appears only in file-encryption tooling (tarsnap, age)
|
||
// where multi-second cost is acceptable. The Kratos default and RFC 7914's
|
||
// example both use N=16384 (2^14). Combined with the r=8 ceiling, this
|
||
// caps memory at 128 MiB per compare.
|
||
maxScryptN uint32 = 1 << 17
|
||
|
||
// maxScryptR bounds scrypt's r (block size). RFC 7914 specifies r=8
|
||
// verbatim; all five OWASP recommended scrypt configurations use r=8.
|
||
// No documented production password hash uses r > 8. With N=2^17 this
|
||
// caps memory at 128 MiB per compare.
|
||
maxScryptR uint32 = 8
|
||
|
||
// maxScryptP bounds scrypt's p (parallelization). p=1 is the standard
|
||
// default; OWASP's highest documented config uses p=10. Go's scrypt
|
||
// implementation runs p iterations serially, so p multiplies CPU
|
||
// linearly without any parallelism gain.
|
||
maxScryptP uint32 = 10
|
||
|
||
// maxArgon2MemoryKiB bounds argon2 memory at 1 GiB. Kratos default is
|
||
// 128 MiB; OWASP recommends 64 MiB. 1 GiB matches the existing
|
||
// Argon2DefaultDedicatedMemory ceiling in driver/config.
|
||
maxArgon2MemoryKiB uint32 = 1 << 20
|
||
|
||
// maxArgon2Iterations bounds argon2 t. RFC 9106 recommends t∈{1,3};
|
||
// OWASP configurations use up to t=5; libsodium SENSITIVE preset uses
|
||
// t=4. Bitwarden's user-configurable hard cap is 10, the highest
|
||
// documented production value.
|
||
maxArgon2Iterations uint32 = 10
|
||
|
||
// maxArgon2Parallelism bounds argon2 p. Default is 2*NumCPU; 16 covers
|
||
// up to 8-core hosts.
|
||
maxArgon2Parallelism uint8 = 16
|
||
|
||
// maxPbkdf2Iterations bounds PBKDF2 i. OWASP 2023 recommends 600k
|
||
// (SHA-256) / 210k (SHA-512). 10M ≈ 10× the strongest published
|
||
// recommendation and bounds CPU to a few seconds on modern hardware.
|
||
maxPbkdf2Iterations uint32 = 10_000_000
|
||
|
||
// maxBcryptCost bounds bcrypt cost. The format spec allows 4–31, but
|
||
// cost grows exponentially: cost 12 (Kratos, PHP, Django default) is
|
||
// ~250 ms; cost 14 (high-security guidance) is ~1 s; cost 15 (practical
|
||
// max for interactive use) is ~2 s; cost 17 is ~8 s. No mainstream
|
||
// platform defaults above cost 12.
|
||
maxBcryptCost = 15
|
||
)
|
||
|
||
func validateFirebaseScryptParams(logN, r, p uint32) error {
|
||
if logN > maxScryptLogN {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "firescrypt ln=%d exceeds max %d", logN, maxScryptLogN)
|
||
}
|
||
if r == 0 || r > maxScryptR {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "firescrypt r=%d not in [1, %d]", r, maxScryptR)
|
||
}
|
||
if p == 0 || p > maxScryptP {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "firescrypt p=%d not in [1, %d]", p, maxScryptP)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateScryptParams(n, r, p uint32) error {
|
||
if n == 0 || n > maxScryptN {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "scrypt N=%d not in [1, %d]", n, maxScryptN)
|
||
}
|
||
if r == 0 || r > maxScryptR {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "scrypt r=%d not in [1, %d]", r, maxScryptR)
|
||
}
|
||
if p == 0 || p > maxScryptP {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "scrypt p=%d not in [1, %d]", p, maxScryptP)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateArgon2Params(memoryKiB uint64, iterations uint32, parallelism uint8) error {
|
||
if memoryKiB == 0 || memoryKiB > uint64(maxArgon2MemoryKiB) {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "argon2 m=%d KiB not in [1, %d]", memoryKiB, maxArgon2MemoryKiB)
|
||
}
|
||
if iterations == 0 || iterations > maxArgon2Iterations {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "argon2 t=%d not in [1, %d]", iterations, maxArgon2Iterations)
|
||
}
|
||
if parallelism == 0 || parallelism > maxArgon2Parallelism {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "argon2 p=%d not in [1, %d]", parallelism, maxArgon2Parallelism)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validatePbkdf2Params(iterations uint32) error {
|
||
if iterations == 0 || iterations > maxPbkdf2Iterations {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "pbkdf2 i=%d not in [1, %d]", iterations, maxPbkdf2Iterations)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateBcryptHashCost(hashed []byte) error {
|
||
cost, err := bcrypt.Cost(hashed)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if cost > maxBcryptCost {
|
||
return errors.Wrapf(ErrHashParametersOutOfBounds, "bcrypt cost=%d exceeds max %d", cost, maxBcryptCost)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValidateImportedHash performs cost-parameter bounds checking on a hash
|
||
// before it is persisted via the admin identity import API. It is the
|
||
// import-time counterpart to the bounds checks in the compare* paths and
|
||
// exists so we fail fast on malicious imports rather than persisting a hash
|
||
// that would crash the process at every login attempt.
|
||
//
|
||
// Hashers without attacker-controlled cost parameters (md5-crypt, sha-crypt,
|
||
// the static SHA/MD5/HMAC families, SSHA) pass through unchanged.
|
||
func ValidateImportedHash(hashed []byte) error {
|
||
switch {
|
||
case IsBcryptHash(hashed):
|
||
return validateBcryptHashCost(hashed)
|
||
case IsArgon2idHash(hashed), IsArgon2iHash(hashed):
|
||
_, _, _, err := decodeArgon2idHash(string(hashed))
|
||
return err
|
||
case IsPbkdf2Hash(hashed):
|
||
_, _, _, err := decodePbkdf2Hash(string(hashed))
|
||
return err
|
||
case IsScryptHash(hashed):
|
||
_, _, _, err := decodeScryptHash(string(hashed))
|
||
return err
|
||
case IsFirebaseScryptHash(hashed):
|
||
_, _, _, _, _, err := decodeFirebaseScryptHash(string(hashed))
|
||
return err
|
||
}
|
||
return nil
|
||
}
|