From 182e183a2359d06a556f5b7f1d5387dda491dd66 Mon Sep 17 00:00:00 2001 From: Patrick Scheid Date: Fri, 12 Jun 2026 04:44:11 +0200 Subject: [PATCH] Validate scrypt work factor at construction The encrypt path accepted any work factor: values >= 31 overflowed 1 << workFactor, and anything above 20 produced files this library's own decrypt cap refuses to read back. Enforce [1, 20] eagerly, matching the existing decrypt-side maximum. --- Age.Tests/UnitTests.cs | 22 ++++++++++++++++++++++ Age/Recipients/ScryptRecipient.cs | 15 +++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Age.Tests/UnitTests.cs b/Age.Tests/UnitTests.cs index dd69e71..17ece00 100644 --- a/Age.Tests/UnitTests.cs +++ b/Age.Tests/UnitTests.cs @@ -829,6 +829,28 @@ public void Unwrap_Rejects_WorkFactor_Over_20() Assert.Throws(() => recipient.Unwrap(stanza)); } + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(21)] + [InlineData(31)] + [InlineData(64)] + public void Constructor_Rejects_OutOfRange_WorkFactor(int workFactor) + { + // Out-of-range work factors must fail fast (the high end would otherwise + // overflow `1 << workFactor` or produce a file this library can't read). + Assert.Throws(() => new ScryptRecipient("password", workFactor)); + } + + [Theory] + [InlineData(1)] + [InlineData(18)] + [InlineData(20)] + public void Constructor_Accepts_InRange_WorkFactor(int workFactor) + { + _ = new ScryptRecipient("password", workFactor); + } + [Fact] public void Unwrap_Rejects_Wrong_Salt_Size() { diff --git a/Age/Recipients/ScryptRecipient.cs b/Age/Recipients/ScryptRecipient.cs index 12db0a8..f044ef7 100644 --- a/Age/Recipients/ScryptRecipient.cs +++ b/Age/Recipients/ScryptRecipient.cs @@ -16,19 +16,30 @@ public sealed class ScryptRecipient(string passphrase, int workFactor = 18) : IR private const int NonceSize = 12; private const int WrappedKeySize = 32; // 16-byte file key + 16-byte Poly1305 tag + // Validate eagerly so an out-of-range work factor fails at construction + // rather than overflowing `1 << workFactor` or producing a stanza this + // library (which caps decryption at MaxWorkFactor) could never read back. + private readonly int _workFactor = EnsureValidWorkFactor(workFactor); + + private static int EnsureValidWorkFactor(int workFactor) => + workFactor is >= 1 and <= MaxWorkFactor + ? workFactor + : throw new ArgumentOutOfRangeException(nameof(workFactor), workFactor, + $"scrypt work factor must be between 1 and {MaxWorkFactor}"); + public Stanza Wrap(ReadOnlySpan fileKey) { var salt = new byte[SaltSize]; RandomNumberGenerator.Fill(salt); - var wrapKey = DeriveWrapKey(passphrase, salt, workFactor); + var wrapKey = DeriveWrapKey(passphrase, salt, _workFactor); var zeroNonce = new byte[NonceSize]; var body = CryptoHelper.ChaChaEncrypt(wrapKey, zeroNonce, fileKey); CryptographicOperations.ZeroMemory(wrapKey); var saltB64 = Base64Unpadded.Encode(salt); - return new Stanza(StanzaType, [saltB64, workFactor.ToString()], body); + return new Stanza(StanzaType, [saltB64, _workFactor.ToString()], body); } public byte[]? Unwrap(Stanza stanza)