Replace legacy encryption with AES-GCM
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using Sarand.Net.Standard.Base.Data;
|
||||
using Sarand.Net.Standard.Base.Helpers;
|
||||
|
||||
namespace Sacs.UnitTests.Net.Standard.Base.Data
|
||||
{
|
||||
[TestFixture]
|
||||
public class SacsAesGcmCrypter_Tester
|
||||
{
|
||||
// --- Data Members ---
|
||||
|
||||
private static readonly Random random = new Random(11437);
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
[Test]
|
||||
public void EncryptDecrypt_RoundTripsRandomText()
|
||||
{
|
||||
const String purpose = "Tests:SacsAesGcmCrypter";
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
String data = RandomString(random.Next(10, 2048), true);
|
||||
String key = RandomString(random.Next(10, 128), true);
|
||||
String encrypted = SacsAesGcmCrypter.Encrypt(data, key, purpose);
|
||||
|
||||
Assert.That(SacsAesGcmCrypter.Decrypt(encrypted, key, purpose), Is.EqualTo(data));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Decrypt_RejectsLegacyCiphertext()
|
||||
{
|
||||
const String encrypted = "0qlOvuM3L9+yzc6XnuHSv2Jq/1v1pyPq6CaCYnMauW6n+bBc9YOBNPtaNFm+2JGz;zgCog0OpwdN+I8yK5UAU8g==";
|
||||
const String key = "Compatibility-Key-2026!";
|
||||
|
||||
Assert.That(
|
||||
() => SacsAesGcmCrypter.Decrypt(encrypted, key, "Tests:SacsAesGcmCrypter"),
|
||||
Throws.TypeOf<CryptographicException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Decrypt_RejectsUnencryptedText()
|
||||
{
|
||||
const String value = "A value without an appended IV";
|
||||
|
||||
Assert.That(
|
||||
() => SacsAesGcmCrypter.Decrypt(value, "unused-key", "Tests:SacsAesGcmCrypter"),
|
||||
Throws.TypeOf<CryptographicException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Encrypt_UsesUniqueNonces()
|
||||
{
|
||||
const String value = "Repeated value";
|
||||
const String key = "Repeated-key";
|
||||
const String purpose = "Tests:SacsAesGcmCrypter";
|
||||
|
||||
String first = SacsAesGcmCrypter.Encrypt(value, key, purpose);
|
||||
String second = SacsAesGcmCrypter.Encrypt(value, key, purpose);
|
||||
|
||||
Assert.That(first, Is.Not.EqualTo(second));
|
||||
Assert.That(first, Does.StartWith(SacsAuthenticatedEncryption.EnvelopePrefix));
|
||||
Assert.That(SacsAesGcmCrypter.Decrypt(first, key, purpose), Is.EqualTo(value));
|
||||
Assert.That(SacsAesGcmCrypter.Decrypt(second, key, purpose), Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Decrypt_RejectsWrongPurpose()
|
||||
{
|
||||
String encrypted = SacsAesGcmCrypter.Encrypt("Purpose-bound", "purpose-key", "Tests:FirstPurpose");
|
||||
|
||||
Assert.That(
|
||||
() => SacsAesGcmCrypter.Decrypt(encrypted, "purpose-key", "Tests:SecondPurpose"),
|
||||
Throws.TypeOf<AuthenticationTagMismatchException>());
|
||||
}
|
||||
|
||||
// --- Utilities ---
|
||||
|
||||
/// <summary>Generate random alphanumeric string</summary>
|
||||
private static String RandomString(int length, bool includeLowerCase = true, bool includeSymbols = true)
|
||||
{
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
if (includeLowerCase) chars += "abcdefghijklmnopqrstuvwxyz";
|
||||
if (includeSymbols) chars += "`¬!\"£$%^&*()-=_+{}[];:'@#~<>?/.,\\| ";
|
||||
return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
|
||||
}
|
||||
|
||||
// --- The End ---
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using System.Linq;
|
||||
using Sarand.Net.Standard.Base.Data;
|
||||
|
||||
namespace Sacs.UnitTests.Net.Standard.Base.Data
|
||||
{
|
||||
[TestFixture]
|
||||
public class SacsAesIvCrypter_Tester
|
||||
{
|
||||
// --- Data Members ---
|
||||
|
||||
private static readonly Random random = new Random(11437);
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
[Test]
|
||||
public void EncryptDecrypt_RoundTripsRandomText()
|
||||
{
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
String data = RandomString(random.Next(10, 2048), true);
|
||||
String key = RandomString(random.Next(10, 128), true);
|
||||
String encrypted = SacsAesIvCrypter.Encrypt(data, key);
|
||||
|
||||
Assert.That(SacsAesIvCrypter.Decrypt(encrypted, key), Is.EqualTo(data));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Decrypt_ReadsLegacyCiphertext()
|
||||
{
|
||||
const String encrypted = "0qlOvuM3L9+yzc6XnuHSv2Jq/1v1pyPq6CaCYnMauW6n+bBc9YOBNPtaNFm+2JGz;zgCog0OpwdN+I8yK5UAU8g==";
|
||||
const String key = "Compatibility-Key-2026!";
|
||||
|
||||
String decrypted = SacsAesIvCrypter.Decrypt(encrypted, key);
|
||||
|
||||
Assert.That(decrypted, Is.EqualTo("Legacy compatibility payload £ € 漢字"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Decrypt_ReturnsUnencryptedTextUnchanged()
|
||||
{
|
||||
const String value = "A value without an appended IV";
|
||||
|
||||
String decrypted = SacsAesIvCrypter.Decrypt(value, "unused-key");
|
||||
|
||||
Assert.That(decrypted, Is.SameAs(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Encrypt_UsesUniqueInitializationVectors()
|
||||
{
|
||||
const String value = "Repeated value";
|
||||
const String key = "Repeated-key";
|
||||
|
||||
String first = SacsAesIvCrypter.Encrypt(value, key);
|
||||
String second = SacsAesIvCrypter.Encrypt(value, key);
|
||||
|
||||
Assert.That(first, Is.Not.EqualTo(second));
|
||||
Assert.That(SacsAesIvCrypter.Decrypt(first, key), Is.EqualTo(value));
|
||||
Assert.That(SacsAesIvCrypter.Decrypt(second, key), Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Encrypt_ProducesKnownCiphertextForExplicitKeyAndIv()
|
||||
{
|
||||
Byte[] key = Enumerable.Range(0, 32).Select(value => (Byte)value).ToArray();
|
||||
Byte[] iv = Enumerable.Range(16, 16).Select(value => (Byte)value).ToArray();
|
||||
|
||||
Byte[] encrypted = SacsAesIvCrypter.Encrypt("Known vector £ 漢字", key, iv);
|
||||
|
||||
Assert.That(Convert.ToBase64String(encrypted), Is.EqualTo("9yYFmwEr9e00Z3WMjGRWfkgT/XwV8OJ4Encs4x9HiVo="));
|
||||
Assert.That(SacsAesIvCrypter.Decrypt(encrypted, key, iv), Is.EqualTo("Known vector £ 漢字"));
|
||||
}
|
||||
|
||||
// --- Utilities ---
|
||||
|
||||
/// <summary>Generate random alphanumeric string</summary>
|
||||
private static String RandomString(int length, bool includeLowerCase = true, bool includeSymbols = true)
|
||||
{
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
if (includeLowerCase) chars += "abcdefghijklmnopqrstuvwxyz";
|
||||
if (includeSymbols) chars += "`¬!\"£$%^&*()-=_+{}[];:'@#~<>?/.,\\| ";
|
||||
return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
|
||||
}
|
||||
|
||||
// --- The End ---
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using NUnit.Framework;
|
||||
using Sarand.Net.Standard.Base.Helpers;
|
||||
|
||||
namespace Sacs.UnitTests.Net.Standard.Base.Helpers
|
||||
{
|
||||
[TestFixture]
|
||||
public class SacsAuthenticatedEncryption_Tester
|
||||
{
|
||||
// --- Data Members ---
|
||||
|
||||
private static readonly Byte[] Key = Convert.FromHexString("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F");
|
||||
private const String Purpose = "Tests:AuthenticatedEnvelope";
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
[TestCase("")]
|
||||
[TestCase("Plain ASCII text")]
|
||||
[TestCase("Unicode £ € 漢字 and emoji 😀")]
|
||||
public void EncryptDecrypt_RoundTrips(String value)
|
||||
{
|
||||
String encrypted = SacsAuthenticatedEncryption.Encrypt(value, Key, Purpose);
|
||||
|
||||
Assert.That(encrypted, Does.StartWith(SacsAuthenticatedEncryption.EnvelopePrefix));
|
||||
Assert.That(encrypted, Does.Not.Contain("+").And.Not.Contain("/").And.Not.EndWith("="));
|
||||
Assert.That(SacsAuthenticatedEncryption.Decrypt(encrypted, Key, Purpose), Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Encrypt_UsesUniqueNonces()
|
||||
{
|
||||
String first = SacsAuthenticatedEncryption.Encrypt("Repeated value", Key, Purpose);
|
||||
String second = SacsAuthenticatedEncryption.Encrypt("Repeated value", Key, Purpose);
|
||||
|
||||
Assert.That(second, Is.Not.EqualTo(first));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Decrypt_RejectsModifiedEnvelope()
|
||||
{
|
||||
String encrypted = SacsAuthenticatedEncryption.Encrypt("Protected value", Key, Purpose);
|
||||
Int32 modifiedIndex = SacsAuthenticatedEncryption.EnvelopePrefix.Length;
|
||||
Char replacement = encrypted[modifiedIndex] == 'A' ? 'B' : 'A';
|
||||
String modified = encrypted.Substring(0, modifiedIndex) + replacement + encrypted.Substring(modifiedIndex + 1);
|
||||
|
||||
Assert.That(
|
||||
() => SacsAuthenticatedEncryption.Decrypt(modified, Key, Purpose),
|
||||
Throws.TypeOf<AuthenticationTagMismatchException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Decrypt_RejectsWrongKey()
|
||||
{
|
||||
String encrypted = SacsAuthenticatedEncryption.Encrypt("Protected value", Key, Purpose);
|
||||
Byte[] wrongKey = Convert.FromHexString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
|
||||
|
||||
Assert.That(
|
||||
() => SacsAuthenticatedEncryption.Decrypt(encrypted, wrongKey, Purpose),
|
||||
Throws.TypeOf<AuthenticationTagMismatchException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Decrypt_RejectsWrongPurpose()
|
||||
{
|
||||
String encrypted = SacsAuthenticatedEncryption.Encrypt("Protected value", Key, Purpose);
|
||||
|
||||
Assert.That(
|
||||
() => SacsAuthenticatedEncryption.Decrypt(encrypted, Key, "Tests:DifferentPurpose"),
|
||||
Throws.TypeOf<AuthenticationTagMismatchException>());
|
||||
}
|
||||
|
||||
[TestCase("legacy-ciphertext")]
|
||||
[TestCase("SACSENC2.not-valid-base64!")]
|
||||
[TestCase("SACSENC2.AA")]
|
||||
public void Decrypt_RejectsUnsupportedOrMalformedValues(String value)
|
||||
{
|
||||
Assert.That(
|
||||
() => SacsAuthenticatedEncryption.Decrypt(value, Key, Purpose),
|
||||
Throws.TypeOf<CryptographicException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Encrypt_RejectsInvalidKeyLength()
|
||||
{
|
||||
Assert.That(
|
||||
() => SacsAuthenticatedEncryption.Encrypt("value", new Byte[15], Purpose),
|
||||
Throws.TypeOf<ArgumentException>());
|
||||
}
|
||||
|
||||
// --- The End ---
|
||||
}
|
||||
}
|
||||
@@ -36,15 +36,14 @@ namespace Sacs.UnitTests.Net.Standard.Base.Helpers
|
||||
Assert.That(() => SacsEncryptionHelper.EncodePlainKey(value), Throws.TypeOf<NotSupportedException>());
|
||||
}
|
||||
|
||||
// --- Key and IV Generation ---
|
||||
// --- Key Generation ---
|
||||
|
||||
[Test]
|
||||
public void InitSymmetricEncryptionKeyIV_ReturnsAes256KeyAndBlockSizedIv()
|
||||
public void InitSymmetricEncryptionKey_ReturnsAes256Key()
|
||||
{
|
||||
(String key, String iv) = SacsEncryptionHelper.InitSymmetricEncryptionKeyIV();
|
||||
String key = SacsEncryptionHelper.InitSymmetricEncryptionKey();
|
||||
|
||||
Assert.That(Convert.FromBase64String(key), Has.Length.EqualTo(32));
|
||||
Assert.That(Convert.FromBase64String(iv), Has.Length.EqualTo(16));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -63,12 +62,13 @@ namespace Sacs.UnitTests.Net.Standard.Base.Helpers
|
||||
[TestCase("Unicode £ € 漢字 and emoji 😀")]
|
||||
public void EncryptDecrypt_RoundTripsText(String value)
|
||||
{
|
||||
(String key, String iv) = SacsEncryptionHelper.InitSymmetricEncryptionKeyIV();
|
||||
String key = SacsEncryptionHelper.InitSymmetricEncryptionKey();
|
||||
|
||||
String encrypted = SacsEncryptionHelper.Encrypt(value, iv, key);
|
||||
String decrypted = SacsEncryptionHelper.Decrypt(encrypted, iv, key);
|
||||
String encrypted = SacsEncryptionHelper.EncryptAuthenticated(value, key, "Tests:SacsEncryptionHelper");
|
||||
String decrypted = SacsEncryptionHelper.DecryptAuthenticated(encrypted, key, "Tests:SacsEncryptionHelper");
|
||||
|
||||
Assert.That(encrypted, Is.Not.EqualTo(value));
|
||||
Assert.That(encrypted, Does.StartWith(SacsAuthenticatedEncryption.EnvelopePrefix));
|
||||
Assert.That(decrypted, Is.EqualTo(value));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user