Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Age.Tests/UnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -829,6 +829,28 @@ public void Unwrap_Rejects_WorkFactor_Over_20()
Assert.Throws<AgeHeaderException>(() => 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<ArgumentOutOfRangeException>(() => 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()
{
Expand Down
15 changes: 13 additions & 2 deletions Age/Recipients/ScryptRecipient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> 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)
Expand Down
Loading