Files
upstream-ory-kratos/hash/hash_limits.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

163 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}