From 8fabdbd39d00c65637cf00032b84b715e57b3035 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Thu, 24 Sep 2026 21:18:33 +1000 Subject: [PATCH] fix(md5crypt): apply the default iterations for the sun variant The documented default of 34000 iterations was never applied. The only place it was set compared the uint32 iterations against an IterationsMin of 0, which can never be true, so the Sun variant always defaulted to 0 additional rounds. The hasher now tracks whether iterations were explicitly configured and applies IterationsDefault when they were not, following the same approach as sha1crypt. An explicit WithIterations(0) is still honoured. The unreachable check in Digest.defaults is removed, and the WithIterations documentation now reflects the rounds parameter and the actual maximum. Digests produced by the Sun variant with default options will now include rounds=34000. Existing digests are unaffected. --- algorithm/md5crypt/digest.go | 4 --- algorithm/md5crypt/hasher.go | 5 +++ algorithm/md5crypt/opts.go | 5 +-- algorithm/md5crypt/regression_test.go | 45 +++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 6 deletions(-) diff --git a/algorithm/md5crypt/digest.go b/algorithm/md5crypt/digest.go index 66a6613..bb5afbe 100644 --- a/algorithm/md5crypt/digest.go +++ b/algorithm/md5crypt/digest.go @@ -89,8 +89,4 @@ func (d *Digest) defaults() { default: d.variant = variantDefault } - - if d.iterations < IterationsMin { - d.iterations = IterationsDefault - } } diff --git a/algorithm/md5crypt/hasher.go b/algorithm/md5crypt/hasher.go index a02d504..e2d9986 100644 --- a/algorithm/md5crypt/hasher.go +++ b/algorithm/md5crypt/hasher.go @@ -29,6 +29,7 @@ type Hasher struct { variant Variant iterations uint32 + i bool bytesSalt int @@ -128,6 +129,10 @@ func (h *Hasher) defaults() { h.d = true + if !h.i { + h.iterations = IterationsDefault + } + if h.bytesSalt < SaltLengthMin { h.bytesSalt = SaltLengthDefault } diff --git a/algorithm/md5crypt/opts.go b/algorithm/md5crypt/opts.go index 2ce9893..647a6af 100644 --- a/algorithm/md5crypt/opts.go +++ b/algorithm/md5crypt/opts.go @@ -47,14 +47,15 @@ func WithVariantName(identifier string) Opt { } // WithIterations sets the iterations parameter of the resulting md5crypt.Digest. Only valid for the Sun variant. This -// is encoded in the hash with the 'iterations' parameter. -// Minimum is 0, Maximum is 4294967295. Default is 34000. +// is encoded in the hash with the 'rounds' parameter. +// Minimum is 0, Maximum is 4294963199. Default is 34000. func WithIterations(iterations uint32) Opt { return func(h *Hasher) (err error) { if iterations < IterationsMin || iterations > IterationsMax { return fmt.Errorf(algorithm.ErrFmtHasherValidation, AlgName, fmt.Errorf(algorithm.ErrFmtInvalidIntParameter, algorithm.ErrParameterInvalid, "iterations", IterationsMin, "", IterationsMax, iterations)) } + h.i = true h.iterations = iterations return nil diff --git a/algorithm/md5crypt/regression_test.go b/algorithm/md5crypt/regression_test.go index a388347..6ff8de8 100644 --- a/algorithm/md5crypt/regression_test.go +++ b/algorithm/md5crypt/regression_test.go @@ -79,3 +79,48 @@ func TestWithIterationsRejectsRoundsThatOverflow(t *testing.T) { assert.NoError(t, err) } + +func TestSunVariantAppliesDefaultIterations(t *testing.T) { + hasher, err := New(WithVariant(VariantSun)) + require.NoError(t, err) + + digest, err := hasher.Hash("password") + require.NoError(t, err) + + encoded := digest.Encode() + assert.Contains(t, encoded, "$md5,rounds=34000$") + + decoded, err := Decode(encoded) + require.NoError(t, err, "encoded digest %q could not be decoded", encoded) + + assert.Equal(t, encoded, decoded.Encode()) + assert.True(t, decoded.Match("password")) + assert.False(t, decoded.Match("incorrect")) +} + +func TestSunVariantHonoursExplicitZeroIterations(t *testing.T) { + hasher, err := New(WithVariant(VariantSun), WithIterations(0)) + require.NoError(t, err) + + digest, err := hasher.Hash("password") + require.NoError(t, err) + + encoded := digest.Encode() + assert.Regexp(t, `^\$md5\$[^$]+\$\$[^$]+$`, encoded) + + decoded, err := Decode(encoded) + require.NoError(t, err) + + assert.True(t, decoded.Match("password")) +} + +func TestStandardVariantUnaffectedByDefaultIterations(t *testing.T) { + hasher, err := New() + require.NoError(t, err) + + digest, err := hasher.Hash("password") + require.NoError(t, err) + + assert.Regexp(t, `^\$1\$[^$]+\$[^$]+$`, digest.Encode()) + assert.True(t, digest.Match("password")) +}