Preserve encryption compatibility on .NET 10
This commit is contained in:
@@ -12,29 +12,74 @@ namespace Sacs.UnitTests.Net.Standard.Base.Data
|
||||
{
|
||||
// --- Data Members ---
|
||||
|
||||
private static Random random = new Random();
|
||||
private static readonly Random random = new Random(11437);
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
[Test]
|
||||
public void SacsAesIvCrypter_Test ()
|
||||
public void EncryptDecrypt_RoundTripsRandomText()
|
||||
{
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
String data = RandomString(random.Next(10, 2048), true);
|
||||
int dataLength = data.Length;
|
||||
String key = RandomString(random.Next(10, 128), true);
|
||||
int keyLength = key.Length;
|
||||
String encrypted = SacsAesIvCrypter.Encrypt(data, key);
|
||||
int encryptedLength = encrypted.Length;
|
||||
|
||||
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)
|
||||
private static String RandomString(int length, bool includeLowerCase = true, bool includeSymbols = true)
|
||||
{
|
||||
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
if (includeLowerCase) chars += "abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using Sarand.Net.Standard.Base.Helpers;
|
||||
|
||||
namespace Sacs.UnitTests.Net.Standard.Base.Helpers
|
||||
{
|
||||
[TestFixture]
|
||||
public class SacsEncryptionHelper_Tester
|
||||
{
|
||||
// --- Base64 and Key Encoding ---
|
||||
|
||||
[TestCase("AQID", 3, true)]
|
||||
[TestCase("AQID", 4, false)]
|
||||
[TestCase("not base64", 0, false)]
|
||||
[TestCase("", 0, true)]
|
||||
[TestCase(null, 0, false)]
|
||||
public void IsBase64_ValidatesContentAndDecodedLength(String value, Int32 expectedLength, Boolean expected)
|
||||
{
|
||||
Assert.That(SacsEncryptionHelper.IsBase64(value, expectedLength), Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EncodePlainKey_RepeatsAndTruncatesToRequestedLength()
|
||||
{
|
||||
String encoded = SacsEncryptionHelper.EncodePlainKey("abc", 8, Encoding.UTF8);
|
||||
|
||||
Assert.That(Convert.FromBase64String(encoded), Is.EqualTo(Encoding.UTF8.GetBytes("abcabcab")));
|
||||
}
|
||||
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
[TestCase(" ")]
|
||||
public void EncodePlainKey_RejectsMissingKey(String value)
|
||||
{
|
||||
Assert.That(() => SacsEncryptionHelper.EncodePlainKey(value), Throws.TypeOf<NotSupportedException>());
|
||||
}
|
||||
|
||||
// --- Key and IV Generation ---
|
||||
|
||||
[Test]
|
||||
public void InitSymmetricEncryptionKeyIV_ReturnsAes256KeyAndBlockSizedIv()
|
||||
{
|
||||
(String key, String iv) = SacsEncryptionHelper.InitSymmetricEncryptionKeyIV();
|
||||
|
||||
Assert.That(Convert.FromBase64String(key), Has.Length.EqualTo(32));
|
||||
Assert.That(Convert.FromBase64String(iv), Has.Length.EqualTo(16));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InitSymmetricEncryptionKey_ReturnsDifferentKeys()
|
||||
{
|
||||
String first = SacsEncryptionHelper.InitSymmetricEncryptionKey();
|
||||
String second = SacsEncryptionHelper.InitSymmetricEncryptionKey();
|
||||
|
||||
Assert.That(second, Is.Not.EqualTo(first));
|
||||
}
|
||||
|
||||
// --- Encryption ---
|
||||
|
||||
[TestCase("")]
|
||||
[TestCase("Plain ASCII text")]
|
||||
[TestCase("Unicode £ € 漢字 and emoji 😀")]
|
||||
public void EncryptDecrypt_RoundTripsText(String value)
|
||||
{
|
||||
(String key, String iv) = SacsEncryptionHelper.InitSymmetricEncryptionKeyIV();
|
||||
|
||||
String encrypted = SacsEncryptionHelper.Encrypt(value, iv, key);
|
||||
String decrypted = SacsEncryptionHelper.Decrypt(encrypted, iv, key);
|
||||
|
||||
Assert.That(encrypted, Is.Not.EqualTo(value));
|
||||
Assert.That(decrypted, Is.EqualTo(value));
|
||||
}
|
||||
|
||||
// --- The End ---
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Sarand.Net.Standard.Base.App;
|
||||
using Sarand.Net.Standard.Base.App;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
@@ -24,8 +24,7 @@ namespace Sarand.Net.Standard.Base.Data
|
||||
{
|
||||
byte[] salt = new byte[] { 80, 70, 60, 50, 40, 30, 20, 10 };
|
||||
int iterations = 300;
|
||||
DeriveBytes keyGenerator = new Rfc2898DeriveBytes(password, salt, iterations, HashAlgorithmName.SHA256);
|
||||
return keyGenerator.GetBytes(keyBytes);
|
||||
return Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, HashAlgorithmName.SHA256, keyBytes);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Sarand.Net.Standard.Base.Core;
|
||||
using Sarand.Net.Standard.Base.Core;
|
||||
using Sarand.Net.Standard.Base.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -34,7 +34,7 @@ namespace Sarand.Net.Standard.Base.Helpers
|
||||
}
|
||||
public static String EncodePlainKey(String plainKey, int keyLength = 32, Encoding encoding = null)
|
||||
{
|
||||
if (StringUtil.IsEmpty(plainKey))
|
||||
if (String.IsNullOrWhiteSpace(plainKey))
|
||||
throw new NotSupportedException("Plain key must not be null/blank");
|
||||
// Ensure key if 32 bytes long.
|
||||
while (StringUtil.GetLength(plainKey) < keyLength)
|
||||
@@ -56,7 +56,7 @@ namespace Sarand.Net.Standard.Base.Helpers
|
||||
}
|
||||
public static string InitSymmetricEncryptionIV(String key)
|
||||
{
|
||||
Aes cipher = CreateCipher(key);
|
||||
using Aes cipher = CreateCipher(key);
|
||||
var IVBase64 = Convert.ToBase64String(cipher.IV);
|
||||
return IVBase64;
|
||||
}
|
||||
@@ -83,7 +83,6 @@ namespace Sarand.Net.Standard.Base.Helpers
|
||||
|
||||
cipher.Padding = PaddingMode.ISO10126;
|
||||
byte[] rawKey = Convert.FromBase64String(keyBase64);
|
||||
String rawKeyValue = System.Text.Encoding.Default.GetString(rawKey);
|
||||
cipher.Key = rawKey;
|
||||
|
||||
return cipher;
|
||||
@@ -99,10 +98,10 @@ namespace Sarand.Net.Standard.Base.Helpers
|
||||
//-------------------------------------------------------------------
|
||||
public static string Encrypt(string text, string IV, string key)
|
||||
{
|
||||
Aes cipher = CreateCipher(key);
|
||||
using Aes cipher = CreateCipher(key);
|
||||
cipher.IV = Convert.FromBase64String(IV);
|
||||
|
||||
ICryptoTransform cryptTransform = cipher.CreateEncryptor();
|
||||
using ICryptoTransform cryptTransform = cipher.CreateEncryptor();
|
||||
byte[] plaintext = Encoding.UTF8.GetBytes(text);
|
||||
byte[] cipherText = cryptTransform.TransformFinalBlock(plaintext, 0, plaintext.Length);
|
||||
|
||||
@@ -112,10 +111,10 @@ namespace Sarand.Net.Standard.Base.Helpers
|
||||
//-------------------------------------------------------------------
|
||||
public static string Decrypt(string encryptedText, string IV, string key)
|
||||
{
|
||||
Aes cipher = CreateCipher(key);
|
||||
using Aes cipher = CreateCipher(key);
|
||||
cipher.IV = Convert.FromBase64String(IV);
|
||||
|
||||
ICryptoTransform cryptTransform = cipher.CreateDecryptor();
|
||||
using ICryptoTransform cryptTransform = cipher.CreateDecryptor();
|
||||
byte[] encryptedBytes = Convert.FromBase64String(encryptedText);
|
||||
byte[] plainBytes = cryptTransform.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Last updated: 2026-09-16
|
||||
| Component | Scope | Status | Date reviewed | Next steps | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `SacsStandardBase/Base/Utils/AssemblyUtil.cs` | PE/COFF timestamp utility | Hardened and covered | 2026-09-16 | Retain the focused tests; use the non-throwing `TryGetBuildDateTime` API where an unavailable timestamp must be distinguished from `DateTime.MinValue`. | Exact primitive reads replace unchecked partial reads and marshaling. Five tests cover a known timestamp, invalid signature, truncated input, a loaded assembly, and null input. COFF timestamps are not presented as reliable physical build times for deterministic builds. Existing framework APIs already cover assembly identity, version, and metadata, so no duplicate convenience wrappers were added. |
|
||||
| `SaCommonVS.sln` | .NET 10 migration build and tests | Migration complete; passing with warnings | 2026-09-16 | Continue the staged package review with the remaining framework-aligned and separately tested major updates. | All supported projects target .NET 10 only. The completed package and utility updates pass the complete validation. The solution builds with zero errors and all 149 tests pass; BusinessMgr builds with 11 Control and 13 Portal tests passing. Xamarin remains isolated and unchanged. |
|
||||
| `SaCommonVS.sln` | .NET 10 migration build and tests | Migration complete; passing with warnings | 2026-09-16 | Continue the staged package review with the remaining framework-aligned and separately tested major updates. | All supported projects target .NET 10 only. The completed package and utility updates pass the complete validation. The solution builds with zero errors and all 167 tests pass; BusinessMgr builds with 11 Control and 13 Portal tests passing. Xamarin remains isolated and unchanged. |
|
||||
| `SacsStandardBase/Base/Utils/ImageUtils.cs` | ImageSharp wrapper | Covered; package retained pending later functional review | 2026-09-16 | Leave ImageSharp 3.1.12 unchanged and revisit raster handling, licensing, and alternatives separately. | Five in-memory tests cover PNG and JPEG metadata, invalid and truncated input, SVG recognition, format-preserving encoding, proportional landscape and portrait resizing, no-upscale behavior, and invalid constraints. Later options are to license and upgrade to ImageSharp 4.1.2, retain a secure 3.x release, migrate to MIT-licensed SkiaSharp with its native assets and wrapper rewrite, or evaluate Magick.NET/ImageMagick for its broad format and processing support while accounting for native binaries and a heavier dependency footprint. This raster concern is distinct from BusinessMgr's Windows-only report printing. |
|
||||
| `SacsStandardBase` JSON utilities and `SysId` converter | Newtonsoft.Json integration | Covered for package upgrades | 2026-09-14 | Retain the suite for future Newtonsoft.Json updates. | Six tests cover objects, arrays, nulls, serializer settings, valid and invalid parsing paths, dynamic access, `SysId` conversion, independent deep copies, nested collections, and constructor-value replacement. |
|
||||
| `SacsStandardDbas` provider and parameter creation | Microsoft.Data.SqlClient, System.Data.Odbc, and registry integration | Providers aligned with .NET 10; broader coverage complete | 2026-09-15 | Add optional configured database integration tests when a suitable SQL Server or ODBC test database is available. | Microsoft.Data.SqlClient 7.0.3 and System.Data.Odbc 10.0.12 are covered by eleven database-free tests for provider selection, parsed SQL connection security and authentication settings, adapter creation, parameter order, scalar and null types, SQL `DateTime2`, ODBC `DateTime`, precision normalization, and sentinel dates. The suite explicitly verifies SqlClient 7's mandatory-encryption option. |
|
||||
@@ -25,5 +25,5 @@ Last updated: 2026-09-16
|
||||
| `SacsStandardBase/Base/Net/SacsHttpClient.cs` | Class | Zstandard dependency upgraded and covered | 2026-09-15 | Retain the loopback response test for future HTTP and ZstdNet updates. | ZstdNet 1.5.7 replaces 1.4.5. An in-process HTTP test exercises the real response path with native Zstandard compression and verifies successful UTF-8 decompression without external network access. |
|
||||
| `SacsStandardProc/Proc/Excel/SaMsSpreadsheet_OpenXml.cs` | Class | Dependency upgrade complete; implementation remains incomplete | 2026-09-15 | Scope functional completion and dedicated wrapper tests separately if this implementation is to be revived. | DocumentFormat.OpenXml 3.5.1 builds and passes the existing spreadsheet and downstream validation. The Open XML implementation contains numerous pre-existing unsupported or placeholder operations, so this update does not present it as production-complete. |
|
||||
| `SacsStandardProc/Proc/Excel/SaMsSpreadsheet_POI.cs` | Class | Dependency upgrade complete | 2026-09-12 | Retain the focused spreadsheet suite for future NPOI updates; do not adopt NPOI 2.8.0 without an explicit licensing decision. | NPOI 2.7.6 is paired with an explicit `System.Security.Cryptography.Xml` 10.0.12 reference and NuGet reports no vulnerable packages for `SacsStandardProc`. Nullable date and indexed-colour API changes were adapted. Ten focused tests and all 111 Common tests pass; BusinessMgr builds and its 21 tests pass. |
|
||||
| `SacsStandardBase/Base/Helpers/SacsEncryptionHelper.cs` and `SacsStandardBase/Base/Data/SacsAesIvCrypter.cs` | Classes | Reviewed; action required | 2026-09-08 | Design a versioned authenticated-encryption format with legacy migration and tamper tests. | Current CBC encryption is unauthenticated; password-based key derivation uses a fixed salt and low iteration count. |
|
||||
| `eng/validate.ps1` | Repository validation | Complete; passing | 2026-09-16 | Use as the standard validation entry point and extend it when repository-wide checks change. | Builds serially, runs the 11-test focused SQL/ODBC-provider suite, the 10-test focused NPOI suite, and all 149 Common tests; audits both package-owning projects; verifies changed-file line endings and diffs; then invokes BusinessMgr validation. |
|
||||
| `SacsStandardBase/Base/Helpers/SacsEncryptionHelper.cs` and `SacsStandardBase/Base/Data/SacsAesIvCrypter.cs` | Encryption compatibility and hardening | Compatibility warning resolved; architectural action still required | 2026-09-16 | Design a versioned authenticated-encryption format with legacy migration and tamper tests. | Static PBKDF2 now preserves the legacy fixed-salt, 300-iteration SHA-256 derivation without the .NET 10 warning, and fixed legacy ciphertext protects compatibility. Nineteen focused tests cover both helpers. CBC remains unauthenticated, and the password-based format still uses a fixed salt and low iteration count; changing those properties requires a versioned migration rather than an in-place update. |
|
||||
| `eng/validate.ps1` | Repository validation | Complete; passing | 2026-09-16 | Use as the standard validation entry point and extend it when repository-wide checks change. | Builds serially, runs the 11-test focused SQL/ODBC-provider suite, the 10-test focused NPOI suite, and all 167 Common tests; audits both package-owning projects; verifies changed-file line endings and diffs; then invokes BusinessMgr validation. |
|
||||
|
||||
+2
-1
@@ -13,7 +13,7 @@ Last updated: 2026-09-16
|
||||
|
||||
| Priority | Topic | Date noted | Next step | Notes |
|
||||
|---|---|---|---|---|
|
||||
| P2 | Resolve the remaining .NET 10 migration warning in non-MAUI Common code | 2026-09-10 | Replace the obsolete `Rfc2898DeriveBytes` constructor with an explicitly compatible derivation configuration. | The Registry and `AssemblyUtil` warning work is complete. The remaining warning is `SYSLIB0060`; its cryptography change overlaps the existing encryption-hardening topic and requires compatibility tests. |
|
||||
| P1 | Design a versioned authenticated-encryption migration | 2026-09-08 | Inventory persisted and transient ciphertext consumers, define an AES-GCM envelope, and specify legacy-read/new-write migration behavior before implementation. | The compatibility-preserving PBKDF2 warning fix and direct regression coverage are complete. Existing CBC formats are unauthenticated; the password-derived format also uses a fixed salt and 300 iterations. An in-place algorithm change would make existing data unreadable. |
|
||||
| P3 | Review raster image handling and ImageSharp | 2026-09-16 | Retain ImageSharp 3.1.12 for now and revisit the wrapper, licensing, and alternatives as a separately scoped functional review. | Version 3.1.12 has no reported vulnerability and five focused tests cover the current narrow use. Later options are: obtain an eligible free or commercial licence and upgrade to ImageSharp 4.1.2; retain a supported secure 3.x release; migrate to MIT-licensed SkiaSharp, accepting native runtime assets and a wrapper rewrite; or evaluate Magick.NET/ImageMagick as a candidate with broad image-format and processing support but native binaries and a heavier dependency footprint. This review is separate from BusinessMgr's Windows printer integration. |
|
||||
| P1 | Prevent synchronous and asynchronous execution from overlapping in `SaProcess_Base` | 2026-09-09 | Route `RunProcess()` and `RunAsync()` through one guarded start operation and extend the lifecycle tests. | A synchronous call can currently reuse and dispose the asynchronous worker's task data. |
|
||||
| P2 | Retain cancellation as a completed process outcome | 2026-09-09 | Store a durable cancellation result until the next process reset and test post-completion state. | `IsInterrupted` currently loses token-based cancellation after task-data cleanup. |
|
||||
@@ -45,6 +45,7 @@ Last updated: 2026-09-16
|
||||
|
||||
| Topic | Date noted | Date started | Date completed | Outcome |
|
||||
|---|---|---|---|---|
|
||||
| Remove the obsolete PBKDF2 usage and establish encryption compatibility coverage | 2026-09-10 | 2026-09-16 | 2026-09-16 | Replaced the obsolete `Rfc2898DeriveBytes` constructor with static `Pbkdf2` using the same password, fixed salt, 300 iterations, SHA-256, and key length. Fixed legacy ciphertext proves derived-key and stored-data compatibility. Expanded direct coverage across both encryption helpers, corrected whitespace-only key acceptance, and disposed AES and transform resources. The broader versioned authenticated-encryption migration remains separately tracked. |
|
||||
| Harden and cover `AssemblyUtil` PE timestamp handling | 2026-09-10 | 2026-09-16 | 2026-09-16 | Replaced the unchecked partial reads and marshalled COFF structure with exact primitive reads, PE-signature validation, and graceful handling of missing, malformed, or truncated files. Added a public `TryGetBuildDateTime` extension while preserving `GetBuildDateTime`, plus five focused tests. The API documentation notes that deterministic-build COFF timestamps need not represent physical build time. |
|
||||
| Upgrade Microsoft.Data.SqlClient and expand SQL provider coverage | 2026-09-15 | 2026-09-15 | 2026-09-15 | Updated Microsoft.Data.SqlClient from 5.2.3 to 7.0.3. Expanded the database-free provider suite from eight to eleven tests, adding parsed SQL connection security and authentication settings plus complete SQL scalar and typed-null parameter coverage. The tests explicitly verify SqlClient 7's mandatory-encryption option. Full Common and downstream BusinessMgr validation passes. |
|
||||
| Align System.Data.Odbc with .NET 10 and expand provider coverage | 2026-09-15 | 2026-09-15 | 2026-09-15 | Updated the Common ODBC dependency from 6.0.2 to 10.0.12 and removed six redundant direct references from BusinessMgr consumers. Expanded the database-free provider suite from four to eight tests, covering non-SQL connections, adapter creation, parameter order, scalar types, and typed nulls. The tests exposed and prompted correction of missing explicit `Int64`, `Decimal`, `Boolean`, `Guid`, and string mappings. All 141 Common tests and both BusinessMgr suites pass. |
|
||||
|
||||
Reference in New Issue
Block a user