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)