Merge pull request #581 from docker/feat/scrypt-work-factor-option · docker/secrets-engine@b4e2589 · GitHub
Skip to content

Commit b4e2589

Browse files
authored
Merge pull request #581 from docker/feat/scrypt-work-factor-option
feat(posixage): expose scrypt work factor as an option
2 parents 2a0ef13 + de23fc3 commit b4e2589

3 files changed

Lines changed: 320 additions & 7 deletions

File tree

‎store/posixage/internal/secretfile/keys.go‎

Lines changed: 51 additions & 6 deletions

‎store/posixage/store.go‎

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ func (f *fileStore[T]) Save(ctx context.Context, id store.ID, s store.Secret) er
357357
// (e.g., age + password). However, multiple keys of the same type are
358358
// allowed (e.g., password + password).
359359
for k, encryptionKeys := range keyGroups {
360-
recipients, err := secretfile.GetRecipients(k, encryptionKeys)
360+
recipients, err := secretfile.GetRecipients(k, encryptionKeys, secretfile.WithScryptWorkFactor(f.scryptWorkFactor))
361361
if err != nil {
362362
return err
363363
}
@@ -394,6 +394,9 @@ type config struct {
394394
logger logging.Logger
395395
registeredDecryptionFunc []promptCaller
396396
registeredEncryptionFuncs []promptCaller
397+
// scryptWorkFactor is the scrypt work factor (2^logN) applied to
398+
// password-protected secrets. A zero value uses the age default.
399+
scryptWorkFactor int
397400
}
398401

399402
type Options func(c *config) error
@@ -407,6 +410,68 @@ func WithLogger(l logging.Logger) Options {
407410
}
408411
}
409412

413+
// WithScryptWorkFactor sets the scrypt work factor (2^logN) used when
414+
// encrypting password-protected secrets. A higher value increases the cost of
415+
// brute-forcing the passphrase at the expense of encryption and decryption
416+
// time.
417+
//
418+
// age fixes the other scrypt parameters at r=8, p=1, so only N=2^logN varies.
419+
// Each increment of logN doubles both the derivation time and the memory
420+
// required: derivation time is roughly linear in N (age calibrates logN=18 at
421+
// about one second on a modern machine), and memory usage is exactly 2^logN
422+
// KiB. The following table is anchored to age's own calibration; absolute times
423+
// are approximate and vary by CPU, but the ratios are exact powers of two:
424+
//
425+
// logN memory ~time/unlock attacker cost vs. 18
426+
// ---- -------- ------------ --------------------
427+
// 8 256 KiB ~1 ms 1024x cheaper
428+
// 10 1 MiB ~4 ms 256x cheaper
429+
// 12 4 MiB ~16 ms 64x cheaper
430+
// 14 16 MiB ~60 ms 16x cheaper
431+
// 15 32 MiB ~125 ms 8x cheaper
432+
// 16 64 MiB ~250 ms 4x cheaper
433+
// 17 128 MiB ~500 ms 2x cheaper (OWASP minimum)
434+
// 18 256 MiB ~1 s baseline (age default)
435+
// 20 1 GiB ~4 s 4x dearer
436+
// 22 4 GiB ~16 s 16x dearer (MaxScryptWorkFactor)
437+
//
438+
// Guidance on choosing a value:
439+
//
440+
// - The work factor is only a linear multiplier on the attacker's cost per
441+
// guess; the passphrase's own entropy dominates security. A low-entropy,
442+
// guessable passphrase is not rescued by a high work factor, and genuinely
443+
// high-entropy random key material is safe even at a low one.
444+
// - OWASP's Password Storage guidance recommends a minimum of N=2^17 (logN
445+
// 17) with r=8, p=1 for human-chosen passphrases. Values at or below logN
446+
// 12 fit in CPU cache, lose scrypt's memory-hardness, and degrade toward a
447+
// plain fast hash that GPUs and ASICs can attack in parallel; reserve them
448+
// for high-entropy key material or tests.
449+
// - Pick the highest logN whose derivation time is tolerable on the weakest
450+
// device that must unlock a secret (each unlock pays the cost once). Note
451+
// that memory is consumed per concurrent derivation, so logN=20 costs 1 GiB
452+
// per in-flight unlock and can exhaust constrained hosts.
453+
// - Lowering the work factor is a deliberate security downgrade; make sure the
454+
// inputs are high-entropy before doing so.
455+
//
456+
// Valid values are 1..[secretfile.MaxScryptWorkFactor]. Values above the
457+
// maximum are rejected because the default age decryption work-factor ceiling
458+
// is 22; files written with a higher factor could not be decrypted by standard
459+
// age tooling. If unset, the age default (logN=18) is used.
460+
//
461+
// This option only affects password keys; it is a no-op for age and ssh keys.
462+
// Because the work factor is recorded in each file's header, changing it does
463+
// not affect the ability to decrypt secrets written with a previous value; the
464+
// new factor applies only to secrets written after the change.
465+
func WithScryptWorkFactor(logN int) Options {
466+
return func(c *config) error {
467+
if logN < 1 || logN > secretfile.MaxScryptWorkFactor {
468+
return fmt.Errorf("scrypt work factor out of range (1..%d): %d", secretfile.MaxScryptWorkFactor, logN)
469+
}
470+
c.scryptWorkFactor = logN
471+
return nil
472+
}
473+
}
474+
410475
type encryptionFuncs interface {
411476
EncryptionPassword | EncryptionSSH | EncryptionAgeX25519
412477
}

‎store/posixage/store_test.go‎

Lines changed: 203 additions & 0 deletions

0 commit comments

Comments
 (0)