Repository navigation
feat(postgres): add SCRAM hashing and password type detection - #260
Merged
Merged
Conversation
armru
approved these changes
May 21, 2026
gbartolini
approved these changes
May 22, 2026
Introduce two new sibling packages under pkg/postgres: * `scram` implements PostgreSQL's SCRAM-SHA-256 password hash. It can both generate a hash in the canonical "SCRAM-SHA-256$<iter>:<salt>$<StoredKey>:<ServerKey>" form and verify a plaintext password against an existing hash. The comparison is constant-time, and credential derivation uses xdg-go/scram's GetStoredCredentialsWithError so that PBKDF2 failures surface as errors instead of panics. * `password` classifies a value stored in `pg_authid.rolpassword` as plaintext, MD5, or SCRAM-SHA-256, mirroring `get_password_type` in PostgreSQL's `src/backend/libpq/crypt.c` and the structural check in `parse_scram_secret` from `src/backend/libpq/auth-scram.c`. This lets callers decide whether a proposed password is already encoded or still in plaintext before deciding how to handle it. Signed-off-by: Leonardo Cecchi <[email protected]>
…y trust
Tighten the hash parser used by Verify so it agrees with the structural
check performed by the password classifier (package password):
- reject iteration counts <= 0 via the new ErrInvalidIterations
sentinel (was: any int accepted, including 0 and negatives)
- reject StoredKey / ServerKey values that do not decode to exactly
32 bytes via the new ErrInvalidStoredKey / ErrInvalidServerKey
sentinels (was: any base64-decodable length accepted, with the
mismatch caught later via the length-mismatch short-circuit in
subtle.ConstantTimeCompare)
Also clarify the wording of the two existing structural error
sentinels — "first block" / "second block" required the reader to
remember the canonical SCRAM-SHA-256$<iter>:<salt>$<StoredKey>:<ServerKey>
layout to know which is which.
Document on Verify that the iteration count parsed from the hash drives
the PBKDF2 work. Callers that may receive attacker-controlled hashes
should validate or cap that count before invoking Verify; the library
does not impose a hard maximum.
Test additions cover the four new error paths and tighten the existing
"non-base64 server key" entry so it no longer trips the new
StoredKey-length check before reaching the ServerKey decode.
Signed-off-by: Armando Ruocco <[email protected]>
Documentation-only follow-up: expand the package doc, normalize the Err* sentinels to "is returned" and have each name the structural rule it covers, document Verify's return contract, move the canonical hash format into parsePostgreSQLHash's godoc, cite the upstream defaults sources, switch GenerateOptions to Go's "// FieldName" form, and fix the "have not have been" typo on Defaults. Assisted-by: Claude Opus 4.7 Signed-off-by: Gabriele Bartolini <[email protected]>
Export the SCRAM-SHA-256 key length from pkg/postgres/password and consume it from pkg/postgres/scram, removing the duplicated unexported constant. Assisted-by: Claude Opus 4.7 Signed-off-by: Gabriele Bartolini <[email protected]>
PostgreSQL's parse_scram_secret reads the iteration count with strtol into a C int, so any hash a server accepts fits in 32 bits. Switch from strconv.Atoi (which on amd64 accepts values up to ~9.2e18) to strconv.ParseInt with bit size 32, so we reject the same overflow cases libpq does and no others. Signed-off-by: Marco Nenciarini <[email protected]>
Defaults treated zero as "use DefaultPostgresIterations" but passed any other value, including negatives, straight through to xdg-go/scram, where it produced an opaque downstream error. Reject negatives early with ErrInvalidIterations, matching what parsePostgreSQLHash already does on the read side. Signed-off-by: Marco Nenciarini <[email protected]>
Generate previously called the exported Defaults method, which wrote the random salt and default iteration count back onto the receiver. A caller that left Salt unset and invoked Generate twice on the same options got identical hashes, because the first call had silently filled in the salt. Unexport Defaults as applyDefaults and have Generate operate on a local copy of the receiver, so repeated calls draw independent salts and the caller's struct is never modified. Drop the direct Defaults tests and fold their assertions into observable Generate behavior, plus a regression guard that asserts the receiver is unchanged across calls. Signed-off-by: Marco Nenciarini <[email protected]>
mnencia
approved these changes
May 22, 2026
gbartolini
approved these changes
May 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Introduce two new sibling packages under pkg/postgres:
scramimplements PostgreSQL's SCRAM-SHA-256 password hash. It can both generate a hash in the canonical "SCRAM-SHA-256$:$:" form and verify a plaintext password against an existing hash. The comparison is constant-time, and credential derivation uses xdg-go/scram's GetStoredCredentialsWithError so that PBKDF2 failures surface as errors instead of panics.passwordclassifies a value stored inpg_authid.rolpasswordas plaintext, MD5, or SCRAM-SHA-256, mirroringget_password_typein PostgreSQL'ssrc/backend/libpq/crypt.cand the structural check inparse_scram_secretfromsrc/backend/libpq/auth-scram.c. This lets callers decide whether a proposed password is already encoded or still in plaintext before deciding how to handle it.