Skip to content

Commit 87c82ff

Browse files
authored
Merge pull request #31 from PandaTechAM/development
AES256 is now replaced by a new AES-SIV implementation for determinis…
2 parents 51f0d64 + 2745309 commit 87c82ff

File tree

9 files changed

+694
-18
lines changed

9 files changed

+694
-18
lines changed

Pandatech.Crypto.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/UserDictionary/Words/=aead/@EntryIndexedValue">True</s:Boolean>
23
<s:Boolean x:Key="/Default/UserDictionary/Words/=decryptor/@EntryIndexedValue">True</s:Boolean>
34
<s:Boolean x:Key="/Default/UserDictionary/Words/=Pandatech/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

Readme.md

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,26 @@
22

33
## Introduction
44

5-
PandaTech.Crypto is a **wrapper library** that consolidates several widely used cryptographic libraries and tools into
6-
one
7-
**simple-to-use package**. It eliminates the need for multiple dependencies, excessive `using` directives, and
8-
duplicated
9-
code, offering an **intuitive API** to streamline **most popular** cryptographic tasks.
5+
**PandaTech.Crypto** is a **wrapper library** that consolidates several widely used cryptographic libraries and tools
6+
into one **simple-to-use package**. This means no more juggling multiple dependencies, heavy `using` directives, or
7+
scattered code to handle everyday cryptographic tasks. The library provides an **intuitive API** that streamlines the *
8+
*most popular** operations:
9+
10+
- AES encryption (including a straightforward **AES256-SIV** implementation not natively offered by Microsoft or
11+
BouncyCastle),
12+
- Hashing (Argon2Id, SHA2, SHA3),
13+
- GZip compression,
14+
- Secure random generation,
15+
- Password validation/strength checks,
16+
- Masking of sensitive data.
1017

1118
Whether you need to **encrypt data**, **hash passwords**, or **generate secure random tokens**, PandaTech.Crypto
12-
provides
13-
lightweight abstractions over popular cryptographic solutions, ensuring simplicity and usability without sacrificing
14-
performance.
19+
provides lightweight abstractions over popular cryptographic solutions, ensuring simplicity and usability without
20+
sacrificing performance.
1521

16-
The **Argon2Id** password hashing is optimized to run efficiently even in **resource-constrained environments** (e.g.,
17-
hash
18-
generation under 500ms on a container with 1 vCore and 1GB of RAM). Other operations such as **AES encryption**, **SHA**
19-
hashing, and **GZip** compression are lightweight enough for almost any environment.
22+
**Argon2Id** password hashing is optimized to run efficiently even in **resource-constrained environments** (e.g., under
23+
500 ms on a container with 1 vCore and 1 GB of RAM). Other operations—such as **AES encryption**, **SHA** hashing, and *
24+
*GZip** compression—are lightweight enough for almost any environment.
2025

2126
## Installation
2227

@@ -30,7 +35,7 @@ Install-Package Pandatech.Crypto
3035

3136
### Configuring in Program.cs
3237

33-
Use the following code to configure AES256 and Argon2Id in your `Program.cs`:
38+
Use the following code to configure AES256/AES256-SIV and Argon2Id in your `Program.cs`:
3439

3540
```csharp
3641
using Pandatech.Crypto.Helpers;
@@ -54,7 +59,13 @@ app.Run();
5459

5560
```
5661

57-
### AES256 Class
62+
### AES256 Class (Old, Deprecated)
63+
64+
> Warning
65+
> `Aes256` is now deprecated because it used a SHA3 hash for deterministic output, which can weaken overall security.
66+
> For
67+
> new development, use `Aes256Siv` instead.
68+
> For existing data, see the `AesMigration` class below to migrate old ciphertext to the new SIV format.
5869
5970
**Encryption/Decryption methods with hashing**
6071

@@ -112,6 +123,51 @@ string decryptedText = Encoding.UTF8.GetString(outputStream.ToArray());
112123
encrypting emails in your software and also want that emails to be unique. With our Aes256 class by default your
113124
emails will be unique as in front will be the unique hash.
114125

126+
### AES256Siv (New, Recommended)
127+
128+
**AES-SIV** (RFC 5297) is the new recommended approach in PandaTech.Crypto for deterministic AES encryption.
129+
It **does not** rely on storing a large hash for uniqueness, instead uses a **synthetic IV** approach to provide both
130+
authentication and deterministic encryption.
131+
132+
```csharp
133+
// Encrypt
134+
byte[] sivCipher = Aes256Siv.Encrypt("your-plaintext");
135+
136+
// Decrypt
137+
string decrypted = Aes256Siv.Decrypt(sivCipher);
138+
```
139+
140+
**Notes:**
141+
142+
- Deterministic: Encrypting the same plaintext with the same key always produces the same ciphertext.
143+
144+
- Security: AES-SIV is an AEAD mode, providing both authenticity (tamper detection) and deterministic encryption.
145+
- Stream-based usage is also available via `Encrypt(Stream in, Stream out, string? key = null) and Decrypt(Stream in,
146+
Stream out, string? key = null)`.
147+
148+
### AesMigration
149+
150+
If you have data encrypted with the old `Aes256` approach—either hashed or non-hashed—and want to convert it to the new
151+
`Aes256Siv` format, **AesMigration** can help:
152+
153+
```csharp
154+
using Pandatech.Crypto.Helpers;
155+
156+
// Convert a single ciphertext that was hashed (Aes256.Encrypt(...))
157+
byte[] newCipher = AesMigration.MigrateFromOldHashed(oldCiphertext);
158+
159+
// Convert multiple hashed ciphertexts:
160+
List<byte[]> newCipherList = AesMigration.MigrateFromOldHashed(oldCipherList);
161+
```
162+
163+
Similarly for **non-hashed** old ciphertext:
164+
165+
```csharp
166+
byte[] newCipher = AesMigration.MigrateFromOldNonHashed(oldCiphertext);
167+
```
168+
169+
The library provides nullable-friendly variants too (`MigrateFromOldHashedNullable`, etc.).
170+
115171
### Argon2id Class
116172

117173
**Default Configurations**

src/Pandatech.Crypto/Extensions/WebAppExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public static class WebAppExtensions
88
public static WebApplicationBuilder AddAes256Key(this WebApplicationBuilder builder, string aesKey)
99
{
1010
Aes256.RegisterKey(aesKey);
11+
Aes256Siv.RegisterKey(aesKey);
1112
return builder;
1213
}
1314

src/Pandatech.Crypto/Helpers/Aes256.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
namespace Pandatech.Crypto.Helpers;
55

6+
[Obsolete(
7+
"This class is deprecated due to security concerns. Use Aes256Siv instead. For migration purposes we let this obsolete class with AesMigration class to make migration easier. Later this class will be removed.")]
68
public static class Aes256
79
{
810
private const int KeySize = 256;
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Security.Cryptography;
3+
using System.Text;
4+
using Org.BouncyCastle.Crypto;
5+
using Org.BouncyCastle.Crypto.Engines;
6+
using Org.BouncyCastle.Crypto.Macs;
7+
using Org.BouncyCastle.Crypto.Modes;
8+
using Org.BouncyCastle.Crypto.Parameters;
9+
using Org.BouncyCastle.Utilities;
10+
11+
namespace Pandatech.Crypto.Helpers;
12+
13+
public static class Aes256Siv
14+
{
15+
private static string? GlobalKey { get; set; }
16+
17+
internal static void RegisterKey(string key)
18+
{
19+
ValidateKey(key);
20+
GlobalKey = key;
21+
}
22+
23+
public static byte[] Encrypt(string plaintext, string? key = null)
24+
{
25+
var bytes = Encoding.UTF8.GetBytes(plaintext);
26+
return Encrypt(bytes, key);
27+
}
28+
29+
public static byte[] Encrypt(byte[] plaintext, string? key = null)
30+
{
31+
if (plaintext.Length == 0)
32+
{
33+
return [];
34+
}
35+
36+
var keyBytes = GetKeyBytes(key);
37+
var macKey = keyBytes[..16];
38+
var encKey = keyBytes[16..];
39+
40+
var siv = ComputeS2V(macKey, plaintext);
41+
var cipher = AesCtr(encKey, siv, plaintext);
42+
return Arrays.Concatenate(siv, cipher);
43+
}
44+
45+
public static void Encrypt(Stream input, Stream output, string? key = null)
46+
{
47+
ArgumentNullException.ThrowIfNull(input);
48+
ArgumentNullException.ThrowIfNull(output);
49+
50+
using var ms = new MemoryStream();
51+
input.CopyTo(ms);
52+
var encrypted = Encrypt(ms.ToArray(), key);
53+
output.Write(encrypted, 0, encrypted.Length);
54+
}
55+
56+
public static string Decrypt(byte[] ciphertext, string? key = null)
57+
{
58+
var plain = DecryptToBytes(ciphertext, key);
59+
return Encoding.UTF8.GetString(plain);
60+
}
61+
62+
public static byte[] DecryptToBytes(byte[] ciphertext, string? key = null)
63+
{
64+
var keyBytes = GetKeyBytes(key);
65+
66+
switch (ciphertext.Length)
67+
{
68+
case 0:
69+
return [];
70+
case < 16:
71+
throw new ArgumentException("At least 16 bytes are required for the SIV.");
72+
}
73+
74+
var macKey = keyBytes[..16];
75+
var encKey = keyBytes[16..];
76+
77+
var siv = ciphertext[..16];
78+
var encrypted = ciphertext[16..];
79+
80+
var plain = AesCtr(encKey, siv, encrypted);
81+
var expectedSiv = ComputeS2V(macKey, plain);
82+
83+
if (!CryptographicOperations.FixedTimeEquals(siv, expectedSiv))
84+
throw new CryptographicException("Invalid SIV / authentication tag.");
85+
86+
return plain;
87+
}
88+
89+
public static void Decrypt(Stream input, Stream output, string? key = null)
90+
{
91+
ArgumentNullException.ThrowIfNull(input);
92+
ArgumentNullException.ThrowIfNull(output);
93+
94+
using var ms = new MemoryStream();
95+
input.CopyTo(ms);
96+
var decrypted = DecryptToBytes(ms.ToArray(), key);
97+
output.Write(decrypted, 0, decrypted.Length);
98+
}
99+
100+
private static byte[] ComputeS2V(byte[] macKey, byte[] data)
101+
{
102+
var cmac = new CMac(new AesEngine());
103+
cmac.Init(new KeyParameter(macKey));
104+
var D = CmacHash(cmac, new byte[16]);
105+
106+
if (data.Length >= 16)
107+
{
108+
var block = new byte[16];
109+
Array.Copy(data, data.Length - 16, block, 0, 16);
110+
for (var i = 0; i < 16; i++)
111+
block[i] ^= D[i];
112+
return CmacHash(cmac, block);
113+
}
114+
else
115+
{
116+
D = DoubleBlock(D);
117+
var block = Pad(data);
118+
for (var i = 0; i < 16; i++)
119+
block[i] ^= D[i];
120+
return CmacHash(cmac, block);
121+
}
122+
}
123+
124+
private static byte[] AesCtr(byte[] key, byte[] iv, byte[] input)
125+
{
126+
var cipher = new BufferedBlockCipher(new SicBlockCipher(new AesEngine()));
127+
cipher.Init(true, new ParametersWithIV(new KeyParameter(key), iv));
128+
return cipher.DoFinal(input);
129+
}
130+
131+
private static byte[] CmacHash(IMac cmac, byte[] input)
132+
{
133+
cmac.Reset();
134+
cmac.BlockUpdate(input, 0, input.Length);
135+
var output = new byte[cmac.GetMacSize()];
136+
cmac.DoFinal(output, 0);
137+
return output;
138+
}
139+
140+
private static byte[] Pad(byte[] input)
141+
{
142+
var padded = new byte[16];
143+
Array.Copy(input, padded, input.Length);
144+
padded[input.Length] = 0x80;
145+
return padded;
146+
}
147+
148+
private static byte[] DoubleBlock(byte[] block)
149+
{
150+
var output = new byte[16];
151+
var carry = 0;
152+
for (var i = 15; i >= 0; i--)
153+
{
154+
var val = (block[i] & 0xFF) << 1;
155+
val |= carry;
156+
output[i] = (byte)(val & 0xFF);
157+
carry = (val >> 8) & 1;
158+
}
159+
160+
if ((block[0] & 0x80) != 0)
161+
output[15] ^= 0x87;
162+
return output;
163+
}
164+
165+
private static byte[] GetKeyBytes(string? overrideKey)
166+
{
167+
if (!string.IsNullOrEmpty(overrideKey))
168+
{
169+
ValidateKey(overrideKey);
170+
return Convert.FromBase64String(overrideKey);
171+
}
172+
173+
if (GlobalKey is null)
174+
{
175+
throw new InvalidOperationException("AES256 Key not configured. Call RegisterKey(...) or provide a key.");
176+
}
177+
178+
return Convert.FromBase64String(GlobalKey);
179+
}
180+
181+
private static void ValidateKey([NotNull] string? key)
182+
{
183+
if (string.IsNullOrWhiteSpace(key) || !IsBase64String(key))
184+
throw new ArgumentException("Key must be valid Base64.");
185+
if (Convert.FromBase64String(key)
186+
.Length != 32)
187+
throw new ArgumentException("Key must be 32 bytes (256 bits).");
188+
}
189+
190+
private static bool IsBase64String(string input)
191+
{
192+
var buffer = new Span<byte>(new byte[input.Length]);
193+
return Convert.TryFromBase64String(input, buffer, out _);
194+
}
195+
}

0 commit comments

Comments
 (0)