From 8b6606653f617a1405fa059ff731c00d67cc8167 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Thu, 24 Sep 2026 21:22:19 +1000 Subject: [PATCH] fix(scrypt): reject parallelism other than 1 for yescrypt The yescrypt key derivation only supports a parallelism of 1 and the yescrypt encoding has no field for it, but the hasher accepted any value from WithP during validation. A hasher configured with the yescrypt variant and a parallelism greater than 1 was therefore created without error and then failed on every call to Hash. Validation now rejects a parallelism other than 1 for the yescrypt variant so the misconfiguration is reported when the hasher is created. The scrypt variant is unaffected. --- algorithm/scrypt/hasher.go | 6 ++++ algorithm/scrypt/opts.go | 2 +- algorithm/scrypt/regression_test.go | 49 +++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/algorithm/scrypt/hasher.go b/algorithm/scrypt/hasher.go index 231cda7..28e8c35 100644 --- a/algorithm/scrypt/hasher.go +++ b/algorithm/scrypt/hasher.go @@ -134,6 +134,12 @@ func (h *Hasher) Validate() (err error) { } func (h *Hasher) validate() (err error) { + // The yescrypt variant does not support parallelism and does not encode it, so any other value would fail during + // hashing. + if h.variant == VariantYescrypt && h.p > ParallelismMin { + return fmt.Errorf("%w: parameter 'p' must be 1 for the yescrypt variant but is set to '%d'", algorithm.ErrParameterInvalid, h.p) + } + rp := uint64(h.r) * uint64(h.p) if rp >= 1<<30 { diff --git a/algorithm/scrypt/opts.go b/algorithm/scrypt/opts.go index a82cc15..2e47b54 100644 --- a/algorithm/scrypt/opts.go +++ b/algorithm/scrypt/opts.go @@ -118,7 +118,7 @@ func WithBlockSize(r int) Opt { } // WithP sets the p parameter (parallelism factor) of the resulting scrypt.Digest. -// Minimum is 1, Maximum is 1073741823. Default is 1. +// Minimum is 1, Maximum is 1073741823. Default is 1. The yescrypt variant only supports a value of 1. func WithP(p int) Opt { return func(h *Hasher) (err error) { if p < ParallelismMin || p > ParallelismMax { diff --git a/algorithm/scrypt/regression_test.go b/algorithm/scrypt/regression_test.go index bf3bec0..0d4b474 100644 --- a/algorithm/scrypt/regression_test.go +++ b/algorithm/scrypt/regression_test.go @@ -5,6 +5,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/go-crypt/crypt/algorithm" ) func TestDecodeRejectsParametersThatCannotBeUsed(t *testing.T) { @@ -90,3 +92,50 @@ func TestHashedDigestsRoundTrip(t *testing.T) { }) } } + +func TestNewYescryptRejectsParallelismOtherThanOne(t *testing.T) { + testCases := []struct { + name string + opts []Opt + }{ + {"NewYescrypt", []Opt{WithP(2)}}, + {"VariantAfterParallelism", []Opt{WithP(2), WithVariant(VariantYescrypt)}}, + {"VariantName", []Opt{WithVariantName("yescrypt"), WithP(4)}}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + hasher, err := NewYescrypt(tc.opts...) + + assert.Nil(t, hasher) + assert.ErrorIs(t, err, algorithm.ErrParameterInvalid) + assert.ErrorContains(t, err, "scrypt validation error: parameter is invalid: parameter 'p' must be 1 for the yescrypt variant but is set to '") + }) + } +} + +func TestNewScryptAllowsParallelismOtherThanOne(t *testing.T) { + hasher, err := NewScrypt(WithLN(4), WithR(1), WithP(2)) + require.NoError(t, err) + + digest, err := hasher.Hash("password") + require.NoError(t, err) + + decoded, err := Decode(digest.Encode()) + require.NoError(t, err) + + assert.True(t, decoded.Match("password")) +} + +func TestNewYescryptAllowsParallelismOfOne(t *testing.T) { + hasher, err := NewYescrypt(WithLN(4), WithP(1)) + require.NoError(t, err) + + digest, err := hasher.Hash("password") + require.NoError(t, err) + + decoded, err := Decode(digest.Encode()) + require.NoError(t, err) + + assert.True(t, decoded.Match("password")) +}