Skip to content

Commit e29f1ed

Browse files
committed
Refactor KeyDerivation and add IMediator interface
Refactored `KeyDerivation` to use HKDF (RFC 5869) for secure and flexible key derivation. Introduced `HkdfExtract` and `HkdfExpand` methods, added optional `salt` support, and improved memory safety with `CryptographicOperations.ZeroMemory`. Legacy `EasyExtensions.Crypto.KeyDerivation` was removed. Updated unit tests to validate the new implementation, ensuring deterministic outputs and proper handling of varying lengths. Assertions were improved for readability and consistency. Enhanced documentation with detailed XML comments for the `KeyDerivation` class and methods. Added the `IMediator` interface to support the mediator pattern, enabling decoupled communication between components. Documented its role and usage. Refactored code to use modern C# features, improving readability, maintainability, and thread safety.
1 parent d3b617a commit e29f1ed

3 files changed

Lines changed: 124 additions & 63 deletions

File tree

Sources/EasyExtensions.Crypto.Tests/KeyDerivationTests.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ public class KeyDerivationTests
1515
[Test]
1616
public void DeriveSubkey_Returns_Requested_Length([Values(0, 1, 16, 32, 48, 64, 100)] int len)
1717
{
18-
var bytes = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, len);
18+
var bytes = KeyDerivation.DeriveSubkey(Master1, PurposeA, len);
1919
Assert.That(bytes, Is.Not.Null);
20-
Assert.That(bytes.Length, Is.EqualTo(len));
20+
Assert.That(bytes, Has.Length.EqualTo(len));
2121
if (len > 0)
2222
{
2323
// Not all zeros
@@ -28,31 +28,31 @@ public void DeriveSubkey_Returns_Requested_Length([Values(0, 1, 16, 32, 48, 64,
2828
[Test]
2929
public void Deterministic_For_Same_Inputs()
3030
{
31-
var a1 = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, 48);
32-
var a2 = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, 48);
31+
var a1 = KeyDerivation.DeriveSubkey(Master1, PurposeA, 48);
32+
var a2 = KeyDerivation.DeriveSubkey(Master1, PurposeA, 48);
3333
Assert.That(a1, Is.EqualTo(a2));
3434
}
3535

3636
[Test]
3737
public void Different_Master_Produces_Different_Key()
3838
{
39-
var a = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, 48);
40-
var b = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master2, PurposeA, 48);
39+
var a = KeyDerivation.DeriveSubkey(Master1, PurposeA, 48);
40+
var b = KeyDerivation.DeriveSubkey(Master2, PurposeA, 48);
4141
Assert.That(a, Is.Not.EqualTo(b));
4242
}
4343

4444
[Test]
4545
public void Different_Purpose_Produces_Different_Key()
4646
{
47-
var a = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, 48);
48-
var b = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeB, 48);
47+
var a = KeyDerivation.DeriveSubkey(Master1, PurposeA, 48);
48+
var b = KeyDerivation.DeriveSubkey(Master1, PurposeB, 48);
4949
Assert.That(a, Is.Not.EqualTo(b));
5050
}
5151

5252
[Test]
5353
public void Base64_Matches_Raw_Encoding()
5454
{
55-
var bytes = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, 32);
55+
var bytes = KeyDerivation.DeriveSubkey(Master1, PurposeA, 32);
5656
var b64A = EasyExtensions.Crypto.KeyDerivation.DeriveSubkeyBase64(Master1, PurposeA, 32);
5757
var b64B = Convert.ToBase64String(bytes);
5858
Assert.That(b64A, Is.EqualTo(b64B));
@@ -62,9 +62,9 @@ public void Base64_Matches_Raw_Encoding()
6262
public void Length32_Vs_Length64_Prefixes_Differ_By_Design()
6363
{
6464
// length 32 uses HMAC(purpose) directly
65-
var l32 = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, 32);
65+
var l32 = KeyDerivation.DeriveSubkey(Master1, PurposeA, 32);
6666
// length 64 uses HMAC(purpose||1) + HMAC(purpose||2)
67-
var l64 = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, 64);
67+
var l64 = KeyDerivation.DeriveSubkey(Master1, PurposeA, 64);
6868
Assert.That(l32, Is.Not.EqualTo(l64.Take(32).ToArray()));
6969
}
7070

@@ -73,8 +73,8 @@ public void Longer_Length_Extends_With_Deterministic_Blocks()
7373
{
7474
// For lengths > 32, result is concatenation of counter-based blocks starting at 1,
7575
// so prefix of 64 should match prefix of 96.
76-
var l64 = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, 64);
77-
var l96 = EasyExtensions.Crypto.KeyDerivation.DeriveSubkey(Master1, PurposeA, 96);
76+
var l64 = KeyDerivation.DeriveSubkey(Master1, PurposeA, 64);
77+
var l96 = KeyDerivation.DeriveSubkey(Master1, PurposeA, 96);
7878
Assert.That(l96.Take(64).ToArray(), Is.EqualTo(l64));
7979
}
8080
}

Sources/EasyExtensions.Crypto/KeyDerivation.cs

Lines changed: 104 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,137 @@
11
using System;
2-
using System.Linq;
32
using System.Text;
43
using System.Security.Cryptography;
54

65
namespace EasyExtensions.Crypto
76
{
87
/// <summary>
9-
/// Provides methods for deriving cryptographic subkeys from a master key and a specified purpose using HMAC-SHA256.
8+
/// Key derivation using HKDF (RFC 5869) over HMAC-SHA256.
9+
/// Provides deterministic subkeys from a master key and context info (purpose), with optional salt.
1010
/// </summary>
11-
/// <remarks>The KeyDerivation class enables deterministic generation of subkeys for different application
12-
/// scenarios, such as per-user or per-purpose secrets, by combining a master key with a unique purpose string. This
13-
/// approach helps ensure that derived keys are unique and isolated for each intended use. All members are static
14-
/// and thread-safe.</remarks>
1511
public static class KeyDerivation
1612
{
13+
private const int HmacOutputLength = 32; // HMAC-SHA256 output size
14+
1715
/// <summary>
18-
/// Derives a deterministic subkey from the specified master key and purpose using HMAC-SHA256.
19-
/// Do not use this method for password hashing or storage.
16+
/// HKDF (RFC 5869) over HMAC-SHA256: masterKey + info (+ optional salt) -> subkey.
2017
/// </summary>
21-
/// <remarks>This method uses HMAC-SHA256 to derive a subkey that is unique to the given purpose
22-
/// and master key. If the requested length exceeds 32 bytes, additional HMAC blocks are concatenated to reach
23-
/// the desired length. The method is suitable for generating cryptographic keys for different contexts from a
24-
/// single master key. The caller is responsible for ensuring that the master key and purpose are chosen
25-
/// appropriately for their security requirements.</remarks>
26-
/// <param name="masterKey">The master key as a UTF-8 encoded string. Cannot be null.</param>
27-
/// <param name="purpose">A string that specifies the intended purpose of the derived subkey. This value differentiates subkeys
28-
/// derived from the same master key. Cannot be null.</param>
29-
/// <param name="lengthBytes">The desired length, in bytes, of the derived subkey. Must be a positive integer.</param>
30-
/// <returns>A byte array containing the derived subkey of the specified length. The same inputs will always produce the
31-
/// same output.</returns>
32-
public static byte[] DeriveSubkey(string masterKey, string purpose, int lengthBytes)
18+
public static byte[] DeriveSubkey(
19+
ReadOnlySpan<byte> masterKey,
20+
ReadOnlySpan<byte> info,
21+
int lengthBytes,
22+
ReadOnlySpan<byte> salt = default)
3323
{
34-
// masterKey + purpose -> HMAC-SHA256 -> target length
35-
var masterBytes = Encoding.UTF8.GetBytes(masterKey);
36-
var purposeBytes = Encoding.UTF8.GetBytes(purpose);
24+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(lengthBytes);
3725

38-
using var hmac = new HMACSHA256(masterBytes);
39-
var hash = hmac.ComputeHash(purposeBytes);
26+
// RFC 5869: Extract
27+
var prk = HkdfExtract(masterKey, salt);
28+
try
29+
{
30+
// RFC 5869: Expand
31+
return HkdfExpand(prk, info, lengthBytes);
32+
}
33+
finally
34+
{
35+
CryptographicOperations.ZeroMemory(prk);
36+
}
37+
}
4038

41-
if (lengthBytes <= hash.Length)
39+
private static byte[] HkdfExtract(ReadOnlySpan<byte> ikm, ReadOnlySpan<byte> salt)
40+
{
41+
// If salt is not provided, use zeros of HashLen bytes (RFC 5869 §2.2).
42+
byte[] saltKey = salt.IsEmpty ? new byte[HmacOutputLength] : salt.ToArray();
43+
try
44+
{
45+
using var hmac = new HMACSHA256(saltKey);
46+
// ComputeHash allocates; we pass a copy of ikm to avoid pinning external span
47+
return hmac.ComputeHash(ikm.ToArray());
48+
}
49+
finally
50+
{
51+
CryptographicOperations.ZeroMemory(saltKey);
52+
}
53+
}
54+
55+
private static byte[] HkdfExpand(byte[] prk, ReadOnlySpan<byte> info, int lengthBytes)
56+
{
57+
int n = (int)Math.Ceiling(lengthBytes / (double)HmacOutputLength);
58+
if (n <= 0 || n > 255)
4259
{
43-
var result = new byte[lengthBytes];
44-
Array.Copy(hash, result, lengthBytes);
45-
return result;
60+
throw new ArgumentOutOfRangeException(nameof(lengthBytes), "HKDF length is too large.");
4661
}
4762

48-
// If you need more than 32 bytes, you can simply "pull" more blocks
49-
// with different counters. This allows for generating longer keys
50-
// while still being deterministic and tied to the master key and purpose.
51-
var buffer = new byte[lengthBytes];
63+
var okm = new byte[lengthBytes];
64+
var infoBytes = info.ToArray();
65+
var t = Array.Empty<byte>();
5266
int offset = 0;
53-
int counter = 1;
5467

55-
while (offset < lengthBytes)
68+
using var hmac = new HMACSHA256(prk);
69+
70+
for (int i = 1; i <= n; i++)
5671
{
57-
var counterBytes = BitConverter.GetBytes(counter++);
58-
var blockInput = purposeBytes.Concat(counterBytes).ToArray();
59-
var block = hmac.ComputeHash(blockInput);
72+
// T(i) = HMAC(PRK, T(i-1) || info || i)
73+
var data = new byte[t.Length + infoBytes.Length + 1];
74+
Buffer.BlockCopy(t, 0, data, 0, t.Length);
75+
Buffer.BlockCopy(infoBytes, 0, data, t.Length, infoBytes.Length);
76+
data[^1] = (byte)i;
77+
78+
t = hmac.ComputeHash(data);
6079

61-
int toCopy = Math.Min(block.Length, lengthBytes - offset);
62-
Array.Copy(block, 0, buffer, offset, toCopy);
80+
int toCopy = Math.Min(HmacOutputLength, lengthBytes - offset);
81+
Buffer.BlockCopy(t, 0, okm, offset, toCopy);
6382
offset += toCopy;
6483
}
6584

66-
return buffer;
85+
CryptographicOperations.ZeroMemory(t);
86+
CryptographicOperations.ZeroMemory(infoBytes);
87+
return okm;
88+
}
89+
90+
/// <summary>
91+
/// String-based wrapper for compatibility: masterKey + purpose -> subkey.
92+
/// </summary>
93+
public static byte[] DeriveSubkey(
94+
string masterKey,
95+
string purpose,
96+
int lengthBytes,
97+
string? salt = null)
98+
{
99+
ArgumentNullException.ThrowIfNull(masterKey);
100+
ArgumentNullException.ThrowIfNull(purpose);
101+
102+
var masterBytes = Encoding.UTF8.GetBytes(masterKey);
103+
var infoBytes = Encoding.UTF8.GetBytes(purpose);
104+
byte[]? saltBytes = salt is null ? null : Encoding.UTF8.GetBytes(salt);
105+
106+
try
107+
{
108+
return DeriveSubkey(
109+
masterBytes,
110+
infoBytes,
111+
lengthBytes,
112+
saltBytes is null ? [] : saltBytes);
113+
}
114+
finally
115+
{
116+
CryptographicOperations.ZeroMemory(masterBytes);
117+
CryptographicOperations.ZeroMemory(infoBytes);
118+
if (saltBytes is not null)
119+
{
120+
CryptographicOperations.ZeroMemory(saltBytes);
121+
}
122+
}
67123
}
68124

69125
/// <summary>
70-
/// Derives a subkey from the specified master key and purpose, and returns the result as a Base64-encoded
71-
/// string.
126+
/// Base64 helper.
72127
/// </summary>
73-
/// <param name="masterKey">The master key used as the basis for subkey derivation. Cannot be null or empty.</param>
74-
/// <param name="purpose">A string that specifies the intended purpose of the derived subkey. Used to ensure subkeys are unique for
75-
/// different purposes. Cannot be null or empty.</param>
76-
/// <param name="lengthBytes">The desired length, in bytes, of the derived subkey. Must be a positive integer.</param>
77-
/// <returns>A Base64-encoded string representing the derived subkey.</returns>
78-
public static string DeriveSubkeyBase64(string masterKey, string purpose, int lengthBytes)
128+
public static string DeriveSubkeyBase64(
129+
string masterKey,
130+
string purpose,
131+
int lengthBytes,
132+
string? salt = null)
79133
{
80-
var bytes = DeriveSubkey(masterKey, purpose, lengthBytes);
134+
var bytes = DeriveSubkey(masterKey, purpose, lengthBytes, salt);
81135
return Convert.ToBase64String(bytes);
82136
}
83137
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
namespace EasyExtensions.Mediator.Contracts
22
{
3+
/// <summary>
4+
/// Defines a mediator that coordinates sending requests and publishing notifications between components.
5+
/// </summary>
6+
/// <remarks>Implementations of this interface typically provide a central point for handling commands,
7+
/// queries, and events, decoupling the sender from the receiver. IMediator combines the capabilities of ISender and
8+
/// IPublisher, allowing both request/response and publish/subscribe patterns. Thread safety and lifetime management
9+
/// depend on the specific implementation.</remarks>
310
public interface IMediator : ISender, IPublisher { }
411
}

0 commit comments

Comments
 (0)