diff --git a/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj
index 40c87d5..8af08c1 100644
--- a/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj
+++ b/IdentityShroud.Core.Tests/IdentityShroud.Core.Tests.csproj
@@ -30,8 +30,4 @@
-
-
-
-
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs b/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs
index 0fb0a42..bf4d0a6 100644
--- a/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs
+++ b/IdentityShroud.Core.Tests/JwtSignatureGeneratorTests.cs
@@ -72,8 +72,8 @@ public class JwtSignatureGeneratorTests
var rsa = RSA.Create();
var parameters = new RSAParameters
{
- Modulus = WebEncoders.Base64UrlDecode(jwk.Modulus),
- Exponent = WebEncoders.Base64UrlDecode(jwk.Exponent)
+ Modulus = WebEncoders.Base64UrlDecode(jwk.Modulus!),
+ Exponent = WebEncoders.Base64UrlDecode(jwk.Exponent!)
};
rsa.ImportParameters(parameters);
diff --git a/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs b/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs
deleted file mode 100644
index 6392676..0000000
--- a/IdentityShroud.Core.Tests/Security/AesGcmHelperTests.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System.Security.Cryptography;
-using System.Text;
-using IdentityShroud.Core.Security;
-
-namespace IdentityShroud.Core.Tests.Security;
-
-public class AesGcmHelperTests
-{
- [Fact]
- public void EncryptDecryptCycleWorks()
- {
- string input = "Hello, world!";
-
- var encryptionKey = RandomNumberGenerator.GetBytes(32);
-
- var cypher = AesGcmHelper.EncryptAesGcm(Encoding.UTF8.GetBytes(input), encryptionKey);
- var output = AesGcmHelper.DecryptAesGcm(cypher, encryptionKey);
-
- Assert.Equal(input, Encoding.UTF8.GetString(output));
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs b/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs
index b855732..68ab90d 100644
--- a/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs
+++ b/IdentityShroud.Core.Tests/Services/EncryptionServiceTests.cs
@@ -1,3 +1,4 @@
+using System.Buffers.Text;
using System.Security.Cryptography;
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Services;
@@ -9,18 +10,43 @@ public class EncryptionServiceTests
[Fact]
public void RoundtripWorks()
{
+ // Note this code will tend to only test the latest verion.
+
// setup
- string key = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
var secretProvider = Substitute.For();
- secretProvider.GetSecret("Master").Returns(key);
+ secretProvider.GetSecret("Master").Returns("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
- EncryptionService sut = new(secretProvider);
- byte[] input = RandomNumberGenerator.GetBytes(16);
+ ReadOnlySpan input = "Hello, World!"u8;
// act
- var cipher = sut.Encrypt(input);
- var result = sut.Decrypt(cipher);
+ EncryptionService sut = new(secretProvider);
+ byte[] cipher = sut.Encrypt(input.ToArray());
+ byte[] result = sut.Decrypt(cipher);
+ // verify
Assert.Equal(input, result);
}
+
+ [Fact]
+ public void DecodeV1_Success()
+ {
+ // When introducing a new version we need version specific tests to
+ // make sure decoding of legacy data still works.
+
+ // setup
+ Span cipher =
+ [
+ 1, 198, 55, 58, 56, 110, 238, 59, 158, 214, 85, 241, 26, 44, 140, 229, 128, 111, 167, 154, 160, 177, 152,
+ 193, 74, 4, 235, 82, 207, 87, 32, 10, 239, 4, 246, 25, 21, 249, 25, 59, 160, 101
+ ];
+ var secretProvider = Substitute.For();
+ secretProvider.GetSecret("Master").Returns("IGd9yUMusjNW0ezv8ink3QWlAHKFH45d21LyrbJTokw=");
+
+ // act
+ EncryptionService sut = new(secretProvider);
+ byte[] result = sut.Decrypt(cipher.ToArray());
+
+ // verify
+ Assert.Equal("Hello, World!"u8, result);
+ }
}
\ No newline at end of file
diff --git a/IdentityShroud.Core.Tests/UnitTest1.cs b/IdentityShroud.Core.Tests/UnitTest1.cs
index e2b5a05..7a12bc4 100644
--- a/IdentityShroud.Core.Tests/UnitTest1.cs
+++ b/IdentityShroud.Core.Tests/UnitTest1.cs
@@ -66,9 +66,9 @@ public static class JwtReader
return new JsonWebToken()
{
Header = JsonSerializer.Deserialize(
- Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, 0, firstDot))),
+ Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, 0, firstDot)))!,
Payload = JsonSerializer.Deserialize(
- Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, firstDot + 1, secondDot - (firstDot + 1)))),
+ Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(jwt, firstDot + 1, secondDot - (firstDot + 1))))!,
Signature = WebEncoders.Base64UrlDecode(jwt, secondDot + 1, jwt.Length - (secondDot + 1))
};
}
diff --git a/IdentityShroud.Core/Contracts/IEncryptionService.cs b/IdentityShroud.Core/Contracts/IEncryptionService.cs
index a737732..388304b 100644
--- a/IdentityShroud.Core/Contracts/IEncryptionService.cs
+++ b/IdentityShroud.Core/Contracts/IEncryptionService.cs
@@ -2,6 +2,6 @@ namespace IdentityShroud.Core.Contracts;
public interface IEncryptionService
{
- byte[] Encrypt(byte[] plain);
+ byte[] Encrypt(ReadOnlyMemory plain);
byte[] Decrypt(ReadOnlyMemory cipher);
}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/AesGcmHelper.cs b/IdentityShroud.Core/Security/AesGcmHelper.cs
deleted file mode 100644
index bfa5809..0000000
--- a/IdentityShroud.Core/Security/AesGcmHelper.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using System.Security.Cryptography;
-
-namespace IdentityShroud.Core.Security;
-
-public static class AesGcmHelper
-{
-
- public static byte[] EncryptAesGcm(byte[] plaintext, byte[] key)
- {
- int tagSize = AesGcm.TagByteSizes.MaxSize;
- using var aes = new AesGcm(key, tagSize);
-
- Span nonce = stackalloc byte[AesGcm.NonceByteSizes.MaxSize];
- RandomNumberGenerator.Fill(nonce);
- Span ciphertext = stackalloc byte[plaintext.Length];
- Span tag = stackalloc byte[tagSize];
-
- aes.Encrypt(nonce, plaintext, ciphertext, tag);
-
- // Return concatenated nonce|ciphertext|tag
- var result = new byte[nonce.Length + ciphertext.Length + tag.Length];
- nonce.CopyTo(result.AsSpan(0, nonce.Length));
- ciphertext.CopyTo(result.AsSpan(nonce.Length, ciphertext.Length));
- tag.CopyTo(result.AsSpan(nonce.Length + ciphertext.Length, tag.Length));
- return result;
- }
-
- // --------------------------------------------------------------------
- // DecryptAesGcm
- // • key – 32‑byte (256‑bit) secret key (same key used for encryption)
- // • payload – byte[] containing nonce‖ciphertext‖tag
- // • returns – the original plaintext bytes
- // --------------------------------------------------------------------
- public static byte[] DecryptAesGcm(ReadOnlyMemory payload, byte[] key)
- {
- if (key == null) throw new ArgumentNullException(nameof(key));
- if (key.Length != 32) // 256‑bit key
- throw new ArgumentException("Key must be 256 bits (32 bytes) for AES‑256‑GCM.", nameof(key));
-
- // ----------------------------------------------------------------
- // 1️⃣ Extract the three components.
- // ----------------------------------------------------------------
- // AesGcm.NonceByteSizes.MaxSize = 12 bytes (standard GCM nonce length)
- // AesGcm.TagByteSizes.MaxSize = 16 bytes (128‑bit authentication tag)
- int nonceSize = AesGcm.NonceByteSizes.MaxSize; // 12
- int tagSize = AesGcm.TagByteSizes.MaxSize; // 16
-
- if (payload.Length < nonceSize + tagSize)
- throw new ArgumentException("Payload is too short to contain nonce, ciphertext, and tag.", nameof(payload));
-
- ReadOnlySpan nonce = payload.Span[..nonceSize];
- ReadOnlySpan ciphertext = payload.Span.Slice(nonceSize, payload.Length - nonceSize - tagSize);
- ReadOnlySpan tag = payload.Span.Slice(payload.Length - tagSize, tagSize);
-
- byte[] plaintext = new byte[ciphertext.Length];
-
- using var aes = new AesGcm(key, tagSize);
- try
- {
- aes.Decrypt(nonce, ciphertext, tag, plaintext);
- }
- catch (CryptographicException ex)
- {
- // Tag verification failed → tampering or wrong key/nonce.
- throw new InvalidOperationException("Decryption failed – authentication tag mismatch.", ex);
- }
-
- return plaintext;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Security/RsaHelper.cs b/IdentityShroud.Core/Security/RsaHelper.cs
deleted file mode 100644
index ab49ebd..0000000
--- a/IdentityShroud.Core/Security/RsaHelper.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System.Security.Cryptography;
-
-namespace IdentityShroud.Core.Security;
-
-public static class RsaHelper
-{
- ///
- /// Load RSA private key from PKCS#8 format
- ///
- public static RSA LoadFromPkcs8(byte[] pkcs8Key)
- {
- var rsa = RSA.Create();
- rsa.ImportPkcs8PrivateKey(pkcs8Key, out _);
- return rsa;
- }
-}
\ No newline at end of file
diff --git a/IdentityShroud.Core/Services/EncryptionService.cs b/IdentityShroud.Core/Services/EncryptionService.cs
index a4455e0..8aa5bed 100644
--- a/IdentityShroud.Core/Services/EncryptionService.cs
+++ b/IdentityShroud.Core/Services/EncryptionService.cs
@@ -1,3 +1,4 @@
+using System.Security.Cryptography;
using IdentityShroud.Core.Contracts;
using IdentityShroud.Core.Security;
@@ -8,20 +9,85 @@ namespace IdentityShroud.Core.Services;
///
public class EncryptionService : IEncryptionService
{
- private readonly byte[] encryptionKey;
+ private record struct AlgVersion(int NonceSize, int TagSize);
+
+ private AlgVersion[] _versions =
+ [
+ new(0, 0), // version 0 does not realy exist
+ new (12, 16), // version 1
+ ];
+
+ private readonly byte[] _encryptionKey;
public EncryptionService(ISecretProvider secretProvider)
{
- encryptionKey = Convert.FromBase64String(secretProvider.GetSecret("Master"));
+ _encryptionKey = Convert.FromBase64String(secretProvider.GetSecret("Master"));
+ if (_encryptionKey.Length != 32) // 256‑bit key
+ throw new Exception("Key must be 256 bits (32 bytes) for AES‑256‑GCM.");
}
- public byte[] Encrypt(byte[] plain)
+ public byte[] Encrypt(ReadOnlyMemory plaintext)
{
- return AesGcmHelper.EncryptAesGcm(plain, encryptionKey);
+ const int versionNumber = 1;
+ AlgVersion versionParams = _versions[versionNumber];
+
+ int resultSize = 1 + versionParams.NonceSize + versionParams.TagSize + plaintext.Length;
+ // allocate buffer for complete response
+ var result = new byte[resultSize];
+
+ result[0] = (byte)versionNumber;
+
+ // make the spans that point to the parts of the result where their data is located
+ var nonce = result.AsSpan(1, versionParams.NonceSize);
+ var tag = result.AsSpan(1 + versionParams.NonceSize, versionParams.TagSize);
+ var cipher = result.AsSpan(1 + versionParams.NonceSize + versionParams.TagSize);
+
+ // use the spans to place the data directly in its place
+ RandomNumberGenerator.Fill(nonce);
+ using var aes = new AesGcm(_encryptionKey, versionParams.TagSize);
+ aes.Encrypt(nonce, plaintext.Span, cipher, tag);
+
+ return result;
}
- public byte[] Decrypt(ReadOnlyMemory cipher)
+ public byte[] Decrypt(ReadOnlyMemory input)
{
- return AesGcmHelper.DecryptAesGcm(cipher, encryptionKey);
+
+ // ----------------------------------------------------------------
+ // 1️⃣ Extract the three components.
+ // ----------------------------------------------------------------
+ // AesGcm.NonceByteSizes.MaxSize = 12 bytes (standard GCM nonce length)
+ // AesGcm.TagByteSizes.MaxSize = 16 bytes (128‑bit authentication tag)
+ //int nonceSize = AesGcm.NonceByteSizes.MaxSize; // 12
+ //int tagSize = AesGcm.TagByteSizes.MaxSize; // 16
+ var payload = input.Span;
+ int versionNumber = (int)payload[0];
+ if (versionNumber != 1)
+ throw new ArgumentException("Invalid payloag");
+
+ AlgVersion versionParams = _versions[versionNumber];
+
+
+ if (payload.Length < 1 + versionParams.NonceSize + versionParams.TagSize)
+ throw new ArgumentException("Payload is too short to contain nonce, ciphertext, and tag.", nameof(payload));
+
+ ReadOnlySpan nonce = payload.Slice(1, versionParams.NonceSize);
+ ReadOnlySpan tag = payload.Slice(1 + versionParams.NonceSize, versionParams.TagSize);
+ ReadOnlySpan cipher = payload.Slice(1 + versionParams.NonceSize + versionParams.TagSize);
+
+ byte[] plaintext = new byte[cipher.Length];
+
+ using var aes = new AesGcm(_encryptionKey, versionParams.TagSize);
+ try
+ {
+ aes.Decrypt(nonce, cipher, tag, plaintext);
+ }
+ catch (CryptographicException ex)
+ {
+ // Tag verification failed → tampering or wrong key/nonce.
+ throw new InvalidOperationException("Decryption failed – authentication tag mismatch.", ex);
+ }
+
+ return plaintext;
}
}
\ No newline at end of file
diff --git a/IdentityShroud.sln.DotSettings.user b/IdentityShroud.sln.DotSettings.user
index 01fd911..d90a7ba 100644
--- a/IdentityShroud.sln.DotSettings.user
+++ b/IdentityShroud.sln.DotSettings.user
@@ -14,6 +14,7 @@
ForceIncluded
ForceIncluded
ForceIncluded
+ ForceIncluded
ForceIncluded
ForceIncluded
ForceIncluded
diff --git a/README.md b/README.md
index f8839d9..fa9605a 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,5 @@
# IdentityShroud
-
-
-
IdentityShroud is a .NET project for identity management and protection.
## Build and Test