Documentation
¶
Overview ¶
Package derive computes the login secret a client sends in place of a password.
The password is stretched with PBKDF2-SHA256 and the result is expanded through HKDF-SHA256 under a per-product label. kynotes-server and kypost-server both do this, each mirroring a browser-side TypeScript implementation, which makes it a contract across four programs rather than a helper: change any byte and every user is locked out. The golden vectors in the test come from kynotes-server's implementation, not this one.
Both existing copies import golang.org/x/crypto/pbkdf2 and .../hkdf. Both moved into the standard library in Go 1.24, so adopting this package deletes a dependency from each rather than adding one — which is why it can live here at all.
Index ¶
- Constants
- Variables
- func AuthSecret(password, saltBase64 string, iterations int, label string) (string, error)
- func AuthSecretContext(ctx context.Context, password, saltBase64 string, iterations int, label string) (string, error)
- func SyntheticSalt(key []byte, label, username string) (string, error)
Constants ¶
const ( // MinIterations and MaxIterations bound a value that arrives from a client or a // stored record. Below the floor the stretch is worthless; above the ceiling one // login can ask for minutes of CPU. // // The two products do not agree on the ceiling, and this constant takes the looser // of the two. kypost-server enforces 12,000,000 and nothing tighter sits in front of // it (users.MaxLoginIterations, checked in login_params.go). kynotes-server's // derivation carries the same 12,000,000 (auth.MaxLoginIterations), and four of its // five routes that accept a client iteration count cap it below that: registration // silently clamps an out-of-range value to 600,000 rather than refusing it // (httpapi/auth_routes.go:80), and password change, admin user creation, and admin // password reset each refuse anything outside 100,000-1,000,000 // (auth_routes.go:227, admin_routes.go:135,246). The fifth, account recovery // (auth_routes.go:264), checks the client's count only through // auth.DeriveAuthSecret's own 100,000-12,000,000 bound and stores what it is given — // so kynotes does admit the full 12,000,000, just not on every route. // // Which ceiling is right is unresolved, and it is a product decision rather than a // library one. Adopting this package as the only bound would raise those four // routes' ceiling 12-fold; keep their tighter checks until that call is made. // // The cross-repo line numbers above are evidence this module cannot execute — it has // no access to those repositories. What it can pin is its own pair of numbers, so // that moving either one is a deliberate diff: TestIterationBoundsAreWhatTheDocSays. MinIterations = 100_000 MaxIterations = 12_000_000 )
const ( // MaxConcurrent bounds how many stretches run at once. // // MaxIterations bounds one call, which is not admission control: a burst all asking // for the ceiling occupies every core for as long as it takes, and everything sharing // the process waits behind it. The iteration count comes from a client or a stored // record, so the expensive number is the caller's to choose and the concurrency is // ours. PBKDF2 is single-threaded, so a slot really is a core. // // This budget is separate from password's, and deliberately: this package is // standard-library-only, and importing password would pull x/crypto into it. The two // also bound different resources — slots of CPU here, bytes and lanes of memory // there. A product adopting both is spending MaxConcurrent cores plus // password.MaxMemoryKiB of memory across password.MaxLanes lanes; add them up // against the box before deciding either is safe. MaxConcurrent = 4 )
ponytail: fixed budget and wait. Make them configurable if a deployment needs a different ceiling.
const MinSyntheticKeyBytes = 32
MinSyntheticKeyBytes is the floor for the key behind a synthetic salt.
The salt itself is not secret — its job is uniqueness. The key is: an attacker who can compute synthetic salts can tell them apart from the random salts real accounts carry, which is exactly the account-existence oracle this function exists to close. An empty or short key makes every synthetic salt predictable, so it is refused rather than used.
Variables ¶
var ErrBusy = errors.New("derive: too many concurrent derivations")
ErrBusy reports that too many stretches are already running. Callers should answer 503 and must not spend a lockout strike: this is the server saying "not now", not the user getting the password wrong.
Functions ¶
func AuthSecret ¶
AuthSecret is AuthSecretContext without a deadline of its own. It derives the hex login secret for a password, a base64 salt and an iteration count, separated by label.
func AuthSecretContext ¶
func AuthSecretContext(ctx context.Context, password, saltBase64 string, iterations int, label string) (string, error)
AuthSecretContext is AuthSecret, bounded by a context.
The context governs the wait for a derivation slot, not the derivation: PBKDF2 at these iteration counts is not interruptible, so a cancellation during the stretch itself is noticed when it finishes.
func SyntheticSalt ¶
SyntheticSalt derives a stable per-user login salt for an account that has none, so a probe cannot tell a registered username from an unregistered one by whether a salt comes back.
The username is lower-cased first: keying anything off the raw string lets one account present as many, which quietly multiplies any per-account budget layered on top.
Types ¶
This section is empty.