Migrate encryption consumers to AES-GCM
This commit is contained in:
@@ -5,6 +5,7 @@ using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Sarand.BusinessMgr.Net.Core.Web.App;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.App;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Data;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Mgr.Control;
|
||||
using Sarand.Net.Standard.Base.App;
|
||||
using Sarand.Net.Standard.Base.Core;
|
||||
@@ -119,13 +120,9 @@ namespace Sarand.BusinessMgr.Net.Core.Web.Components
|
||||
//-------------------------------------------------------------------
|
||||
// Online encryption parameters.
|
||||
//-------------------------------------------------------------------
|
||||
// Portal Request Password encryption parameters (key & IV) are defined in
|
||||
// Portal Request Password encryption key is defined in
|
||||
// the base Controller so that they can be shared with the Control application.
|
||||
// NB IV value is generated from the key and fixed so that it doesn't change when the application restarts.
|
||||
// IV Must be regenerated if key is changed.
|
||||
protected static string Base64Key_RequestPassword_Portal { get; } = SacsEncryptionHelper.EncodePlainKey("[5Abus1neSSmGR~Request#Password}", SacsEncryptionHelper.KeyLength);
|
||||
//private static String Base64IV_RequestPassword_Generated_Portal = SacsEncryptionHelper.InitSymmetricEncryptionIV(Base64Key_RequestPassword_Portal);
|
||||
protected static string Base64IV_RequestPassword_Portal { get; } = "8TRhBayBJZmACQ6pld1F9A==";
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Session.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace Sarand.BusinessMgr.Net.Standard.Base.Data
|
||||
{
|
||||
public static class SbmEncryptionPurpose
|
||||
{
|
||||
// --- Definitions ---
|
||||
|
||||
public const String WindowsServiceRequest = "BusinessMgr:WindowsService:Request";
|
||||
public const String WindowsServiceResponse = "BusinessMgr:WindowsService:Response";
|
||||
public const String ControlKeepLoggedIn = "BusinessMgr:Control:KeepLoggedIn";
|
||||
public const String PortalKeepLoggedIn = "BusinessMgr:Portal:KeepLoggedIn";
|
||||
public const String ControlPasswordReset = "BusinessMgr:Control:PasswordReset";
|
||||
public const String PortalPasswordReset = "BusinessMgr:Portal:PasswordReset";
|
||||
public const String PortalDemoRegistration = "BusinessMgr:Portal:DemoRegistration";
|
||||
public const String ControlSessionParameters = "BusinessMgr:Control:SessionParameters";
|
||||
public const String PortalSessionParameters = "BusinessMgr:Portal:SessionParameters";
|
||||
|
||||
// --- The End ---
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,47 @@
|
||||
using Sarand.Net.Standard.Base.App;
|
||||
using Sarand.Net.Standard.Base.Data;
|
||||
using Sarand.Net.Standard.Base.Utils;
|
||||
using System;
|
||||
|
||||
namespace Sarand.BusinessMgr.Net.Standard.Base.Data
|
||||
{
|
||||
//=======================================================================
|
||||
public static class SbmWindowsServiceCrypter
|
||||
{
|
||||
//-------------------------------------------------------------------
|
||||
// Data members.
|
||||
// --- Data Members ---
|
||||
|
||||
/// <summary>
|
||||
/// Encryption is shared between Windows Service app and mobile
|
||||
/// </summary>
|
||||
private static readonly String requestDataKey = "<Sarand>~{BusinessMgr}#[WindowsService]&/DataCryption/*|RequestData|";
|
||||
private static readonly String responseDataKey = "{Sarand}+(BusinessMgr)%\\WindowsService/=<DataCryption>@:Response:";
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// R e q u e s t D a t a
|
||||
//-------------------------------------------------------------------
|
||||
// --- Request Data ---
|
||||
|
||||
/// <summary>Encrypt request data</summary>
|
||||
public static String EncryptRequestData(String value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SacsAesIvCrypter.Encrypt(value, requestDataKey);
|
||||
}
|
||||
catch (Exception exc) { SaLogger.Debug($"EncryptRequestData {ExceptionUtil.GetErrorStateMessage(exc)}"); }
|
||||
return value;
|
||||
return SacsAesGcmCrypter.Encrypt(value, requestDataKey, SbmEncryptionPurpose.WindowsServiceRequest);
|
||||
}
|
||||
|
||||
/// <summary>Decrypt request data</summary>
|
||||
public static String DecryptRequestData(String value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SacsAesIvCrypter.Decrypt(value, requestDataKey);
|
||||
}
|
||||
catch (Exception exc) { SaLogger.Debug($"DecryptRequestData {ExceptionUtil.GetErrorStateMessage(exc)}"); }
|
||||
return value;
|
||||
return SacsAesGcmCrypter.Decrypt(value, requestDataKey, SbmEncryptionPurpose.WindowsServiceRequest);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// R e s p o n s e D a t a
|
||||
//-------------------------------------------------------------------
|
||||
// --- Response Data ---
|
||||
|
||||
/// <summary>Encrypt response data</summary>
|
||||
public static String EncryptResponseData(String value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SacsAesIvCrypter.Encrypt(value, responseDataKey);
|
||||
}
|
||||
catch (Exception exc) { SaLogger.Debug($"EncryptRequestData {ExceptionUtil.GetErrorStateMessage(exc)}"); }
|
||||
return value;
|
||||
return SacsAesGcmCrypter.Encrypt(value, responseDataKey, SbmEncryptionPurpose.WindowsServiceResponse);
|
||||
}
|
||||
|
||||
/// <summary>Decrypt response data</summary>
|
||||
public static String DecryptResponseData(String value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return SacsAesIvCrypter.Decrypt(value, responseDataKey);
|
||||
}
|
||||
catch (Exception exc) { SaLogger.Debug($"EncryptRequestData {ExceptionUtil.GetErrorStateMessage(exc)}"); }
|
||||
return value;
|
||||
return SacsAesGcmCrypter.Decrypt(value, responseDataKey, SbmEncryptionPurpose.WindowsServiceResponse);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// --- The End ---
|
||||
}
|
||||
//=======================================================================
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Security.Cryptography;
|
||||
using NUnit.Framework;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Data;
|
||||
using Sarand.Net.Standard.Base.Helpers;
|
||||
using Sacs.Web.Components;
|
||||
|
||||
namespace Sarand.BusinessMgr.Test.Base.Data
|
||||
{
|
||||
[TestFixture]
|
||||
public class SbmEncryption_Tester
|
||||
{
|
||||
// --- Service Encryption ---
|
||||
|
||||
[Test]
|
||||
public void WindowsServiceCrypter_RoundTripsRequestAndResponseData()
|
||||
{
|
||||
const String value = "Service payload £ € 漢字";
|
||||
|
||||
String request = SbmWindowsServiceCrypter.EncryptRequestData(value);
|
||||
String response = SbmWindowsServiceCrypter.EncryptResponseData(value);
|
||||
|
||||
Assert.That(request, Does.StartWith(SacsAuthenticatedEncryption.EnvelopePrefix));
|
||||
Assert.That(response, Does.StartWith(SacsAuthenticatedEncryption.EnvelopePrefix));
|
||||
Assert.That(response, Is.Not.EqualTo(request));
|
||||
Assert.That(SbmWindowsServiceCrypter.DecryptRequestData(request), Is.EqualTo(value));
|
||||
Assert.That(SbmWindowsServiceCrypter.DecryptResponseData(response), Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WindowsServiceCrypter_RejectsWrongMessageDirection()
|
||||
{
|
||||
String request = SbmWindowsServiceCrypter.EncryptRequestData("Request payload");
|
||||
|
||||
Assert.That(
|
||||
() => SbmWindowsServiceCrypter.DecryptResponseData(request),
|
||||
Throws.TypeOf<AuthenticationTagMismatchException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WindowsServiceCrypter_RejectsLegacyData()
|
||||
{
|
||||
Assert.That(
|
||||
() => SbmWindowsServiceCrypter.DecryptRequestData("legacy-or-plaintext"),
|
||||
Throws.TypeOf<CryptographicException>());
|
||||
}
|
||||
|
||||
// --- Parameter Encryption ---
|
||||
|
||||
[Test]
|
||||
public void ControllerParameterEncryption_RoundTripsAuthenticatedValues()
|
||||
{
|
||||
String key = SacsEncryptionHelper.EncodePlainKey("BusinessMgr-Test-Key", SacsEncryptionHelper.KeyLength);
|
||||
NameValueCollection parameters = new NameValueCollection
|
||||
{
|
||||
{ "alpha", "One" },
|
||||
{ "unicode", "£ € 漢字" }
|
||||
};
|
||||
|
||||
String encrypted = SaController.EncryptParameters(parameters, key, SbmEncryptionPurpose.ControlSessionParameters);
|
||||
NameValueCollection decrypted = SaController.DecryptParameters(encrypted, key, SbmEncryptionPurpose.ControlSessionParameters);
|
||||
|
||||
Assert.That(encrypted, Does.StartWith(SacsAuthenticatedEncryption.EnvelopePrefix));
|
||||
Assert.That(decrypted["alpha"], Is.EqualTo("One"));
|
||||
Assert.That(decrypted["unicode"], Is.EqualTo("£ € 漢字"));
|
||||
}
|
||||
|
||||
// --- The End ---
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ namespace Sarand.BusinessMgr.Test.Web.Control.App
|
||||
// Populate User Session.
|
||||
SbmSessionData_Control sessionData = new SbmSessionData_Control(sessionContext_Core, userAccount);
|
||||
Assert.That(sessionData, Is.Not.Null);
|
||||
Assert.That(!StringUtil.IsBlank(sessionData.Base64IV_ControlApp), Is.True);
|
||||
Assert.That(sessionData.SessionIdx >= 100000, Is.True);
|
||||
Assert.That(sessionData.DataIdx >= 1000000, Is.True);
|
||||
|
||||
@@ -73,8 +72,6 @@ namespace Sarand.BusinessMgr.Test.Web.Control.App
|
||||
Assert.That(sessionDataDeserialized, Is.Not.Null);
|
||||
|
||||
// Validate deserialised data.
|
||||
Assert.That(!StringUtil.IsBlank(sessionDataDeserialized.Base64IV_ControlApp), Is.True);
|
||||
Assert.That(sessionDataDeserialized.Base64IV_ControlApp, Is.EqualTo(sessionData.Base64IV_ControlApp));
|
||||
Assert.That(sessionDataDeserialized.SessionIdx, Is.EqualTo(sessionData.SessionIdx));
|
||||
Assert.That(sessionDataDeserialized.DataIdx > sessionData.DataIdx, Is.True);
|
||||
|
||||
|
||||
@@ -118,9 +118,6 @@ namespace Sarand.BusinessMgr.Test.Web.Portal.App
|
||||
string sessionDataJsonDeserialized = JsonConvert.SerializeObject(sessionDataDeserialized);
|
||||
|
||||
// Validate deserialised data.
|
||||
Assert.That(!StringUtil.IsBlank(sessionData.Base64IV_PortalApp), Is.True);
|
||||
Assert.That(!StringUtil.IsBlank(sessionDataDeserialized.Base64IV_PortalApp), Is.True);
|
||||
Assert.That(sessionDataDeserialized.Base64IV_PortalApp, Is.EqualTo(sessionData.Base64IV_PortalApp));
|
||||
Assert.That(sessionData.SessionIdx >= 1000, Is.True);
|
||||
Assert.That(sessionDataDeserialized.SessionIdx, Is.EqualTo(sessionData.SessionIdx));
|
||||
Assert.That(sessionData.DataIdx >= 1000000, Is.True);
|
||||
|
||||
@@ -148,10 +148,6 @@ namespace Sarand.BusinessMgr.Web.Control.App
|
||||
}
|
||||
public SystemData System { get; private set; } = new SystemData();
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Control application encryption IV value - for consistent encryption during session.
|
||||
public String Base64IV_ControlApp { get; private set; } = SacsEncryptionHelper.InitSymmetricEncryptionIV(SbmController_Control.Base64Key_ControlApp);
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
}
|
||||
//=======================================================================
|
||||
|
||||
@@ -5,6 +5,7 @@ using Sarand.BusinessMgr.Net.Standard.Base.App;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Bus.Control;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Consoles;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Core;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Data;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Mgr.Control;
|
||||
using Sarand.BusinessMgr.Web.Control.App;
|
||||
using Sarand.BusinessMgr.Web.Control.Consoles;
|
||||
@@ -203,16 +204,10 @@ namespace Sarand.BusinessMgr.Web.Control.Components
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// 'Keep Logged In' cookie and encryption config.
|
||||
// NB - IV value is generated from the key and fixed so that it doesn't change when the application restarts
|
||||
// to ensure that cookie value can be decrypted even if application restarts in the meantime.
|
||||
// - Generating IV from same key results in different value each time.
|
||||
// - IV Must be regenerated if key is changed: uncomment 'Generated' variable and copy resulting value to IV variable.
|
||||
public static class KeepLoggedIn
|
||||
{
|
||||
public static string CookieName = "BCKLI";
|
||||
public static string Base64Key = SacsEncryptionHelper.EncodePlainKey("$BusinessMgr+KeEp-1OgG3d=1n}*32?", SacsEncryptionHelper.KeyLength);
|
||||
public static string Base64IV = "fpHMZchDkrJzgjVMrjpdHg==";
|
||||
//public static String Base64IV_Generated = SacsEncryptionHelper.InitSymmetricEncryptionIV(KeepLoggedIn.Base64Key);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -222,7 +217,7 @@ namespace Sarand.BusinessMgr.Web.Control.Components
|
||||
{
|
||||
NameValueCollection keepLoggedInParams = new NameValueCollection();
|
||||
keepLoggedInParams.Add(ParamName_KeepLoggedIn_UserAccountId, SysId.ToString(SessionData_Control.User.UserAccountId));
|
||||
return EncryptParameters(keepLoggedInParams, KeepLoggedIn.Base64IV, KeepLoggedIn.Base64Key);
|
||||
return EncryptParameters(keepLoggedInParams, KeepLoggedIn.Base64Key, SbmEncryptionPurpose.ControlKeepLoggedIn);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -249,7 +244,7 @@ namespace Sarand.BusinessMgr.Web.Control.Components
|
||||
KeepLoggedInParams keepLoggedInParams = new KeepLoggedInParams();
|
||||
if (!encryptedParams.IsEmpty())
|
||||
{
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, KeepLoggedIn.Base64IV, KeepLoggedIn.Base64Key);
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, KeepLoggedIn.Base64Key, SbmEncryptionPurpose.ControlKeepLoggedIn);
|
||||
keepLoggedInParams.UserAccountId = new SysId(linkParams.Get(ParamName_KeepLoggedIn_UserAccountId).ToInt());
|
||||
}
|
||||
if (DebugMode && SysId.IsValid(keepLoggedInParams.UserAccountId)) SaLogger.Info($"{SysUtil.Fn} UserAccountId: {SysId.ToLong(keepLoggedInParams.UserAccountId)}.");
|
||||
@@ -259,12 +254,11 @@ namespace Sarand.BusinessMgr.Web.Control.Components
|
||||
//-------------------------------------------------------------------
|
||||
// Parameter encryption, eg. for images.
|
||||
//-------------------------------------------------------------------
|
||||
// Encryption key is the same for all users each time the application is started,
|
||||
// but a different IV value is generated for each session so that the links/parameters cannot be shared.
|
||||
// Encryption key is the same for all users each day. Purpose-bound authentication prevents cross-context reuse.
|
||||
public static string Base64Key_ControlApp { get; } = SacsEncryptionHelper.EncodePlainKey($"BusinessMgr=Control&Encryption-{DateTime.Today.Format("ddMMyy")}", SacsEncryptionHelper.KeyLength);
|
||||
protected string EncryptSessionParams(NameValueCollection parameters)
|
||||
{
|
||||
return HttpUtility.UrlEncode(EncryptParameters(parameters, SessionData_Control.Base64IV_ControlApp, Base64Key_ControlApp));
|
||||
return HttpUtility.UrlEncode(EncryptParameters(parameters, Base64Key_ControlApp, SbmEncryptionPurpose.ControlSessionParameters));
|
||||
}
|
||||
public static string EncryptSessionParams(SbmController_Control controller, NameValueCollection parameters)
|
||||
{
|
||||
@@ -272,7 +266,7 @@ namespace Sarand.BusinessMgr.Web.Control.Components
|
||||
}
|
||||
protected NameValueCollection ExtractSessionParams(string encryptedParams)
|
||||
{
|
||||
return DecryptParameters(encryptedParams, SessionData_Control.Base64IV_ControlApp, Base64Key_ControlApp);
|
||||
return DecryptParameters(encryptedParams, Base64Key_ControlApp, SbmEncryptionPurpose.ControlSessionParameters);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.App;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Bus.Control;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Data;
|
||||
using Sarand.BusinessMgr.Web.Control.Components;
|
||||
using Sarand.BusinessMgr.Web.Control.Helpers;
|
||||
using Sarand.BusinessMgr.Web.Control.Models;
|
||||
@@ -385,11 +386,7 @@ namespace Sarand.BusinessMgr.Web.Control.Controllers
|
||||
private static String ParamName_EmailAddress { get; } = "ema";
|
||||
private static String ParamName_ExpiresAt { get; } = "exp";
|
||||
private static String ParamName_Token { get; } = "tok";
|
||||
// NB IV value is generated from the key and fixed so that it doesn't change when the application restarts.
|
||||
// IV Must be regenerated if key is changed.
|
||||
private static String Base64Key_RequestPassword_Control { get; } = SacsEncryptionHelper.EncodePlainKey("{~Saxonite@@Request^Password=}", SacsEncryptionHelper.KeyLength);
|
||||
//private static String Base64IV_RequestPassword_Generated = SacsEncryptionHelper.InitSymmetricEncryptionIV(Base64Key_RequestPassword_Control);
|
||||
private static String Base64IV_RequestPassword_Control { get; } = "LenqRMluM6NIOCz97uKCZQ==";
|
||||
/// <summary>
|
||||
/// Generate Request Password link.
|
||||
/// <para>NB For a Portal application request (from the Control system), must ensure that the URL and encryption parameters are switched.</para>
|
||||
@@ -399,7 +396,7 @@ namespace Sarand.BusinessMgr.Web.Control.Controllers
|
||||
// Base URL.
|
||||
String baseUrl = onlineContext ? controller.AppConfig.URL.Portal.Root : controller.GetRedirectLocation(RedirectLocation.WebAppBase);
|
||||
String requestPassword_Base64Key = onlineContext ? Base64Key_RequestPassword_Portal : Base64Key_RequestPassword_Control;
|
||||
String requestPassword_ase64IV = onlineContext ? Base64IV_RequestPassword_Portal : Base64IV_RequestPassword_Control;
|
||||
String requestPasswordPurpose = onlineContext ? SbmEncryptionPurpose.PortalPasswordReset : SbmEncryptionPurpose.ControlPasswordReset;
|
||||
|
||||
NameValueCollection parameters = new NameValueCollection();
|
||||
parameters.Add(ParamName_UserAccountId, SysId.ToString(userAccountId));
|
||||
@@ -408,7 +405,7 @@ namespace Sarand.BusinessMgr.Web.Control.Controllers
|
||||
parameters.Add(ParamName_Token, requestPasswordToken);
|
||||
String link =
|
||||
baseUrl + "Security/ChangePassword?" +
|
||||
ParamName_RequestPasswordEncrypted + "=" + HttpUtility.UrlEncode(EncryptParameters(parameters, requestPassword_ase64IV, requestPassword_Base64Key));
|
||||
ParamName_RequestPasswordEncrypted + "=" + HttpUtility.UrlEncode(EncryptParameters(parameters, requestPassword_Base64Key, requestPasswordPurpose));
|
||||
return link;
|
||||
}
|
||||
internal static int GetRequestPasswordEmailLinkExpiresMinutes(SbmController_Control controller)
|
||||
@@ -461,7 +458,7 @@ namespace Sarand.BusinessMgr.Web.Control.Controllers
|
||||
RequestPasswordParams requestPasswordParams = new RequestPasswordParams();
|
||||
if (!StringUtil.IsEmpty(encryptedParams))
|
||||
{
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, Base64IV_RequestPassword_Control, Base64Key_RequestPassword_Control);
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, Base64Key_RequestPassword_Control, SbmEncryptionPurpose.ControlPasswordReset);
|
||||
requestPasswordParams.UserAccountId = new SysId(StringUtil.ToInt(linkParams.Get(ParamName_UserAccountId)));
|
||||
requestPasswordParams.EmailAddress = linkParams.Get(ParamName_EmailAddress);
|
||||
requestPasswordParams.ExpiresAt = DateUtil.ParseXmlDateTime(linkParams.Get(ParamName_ExpiresAt));
|
||||
|
||||
@@ -296,10 +296,6 @@ namespace Sarand.BusinessMgr.Web.Portal.App
|
||||
}
|
||||
public UserProfileData Profile { get; protected set; } = new UserProfileData();
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// Portal application encryption IV value - for consistent encryption during session.
|
||||
public String Base64IV_PortalApp { get; private set; } = SacsEncryptionHelper.InitSymmetricEncryptionIV(SbmController_Portal.Base64Key_PortalApp);
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
}
|
||||
//=======================================================================
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.App;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Bus;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Data;
|
||||
using Sarand.BusinessMgr.Web.Portal.App;
|
||||
using Sarand.BusinessMgr.Web.Portal.Helpers;
|
||||
using Sarand.Net.Standard.Base.App;
|
||||
@@ -254,17 +255,11 @@ namespace Sarand.BusinessMgr.Web.Portal.Components
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
// 'Keep Logged In' cookie and encryption config.
|
||||
// NB - IV value is generated from the key and fixed so that it doesn't change when the application restarts
|
||||
// to ensure that cookie value can be decrypted even if application restarts in the meantime.
|
||||
// - Generating IV from same key results in different value each time.
|
||||
// - IV Must be regenerated if key is changed: uncomment 'Generated' variable and copy resulting value to IV variable.
|
||||
// NB - 'Keep Logged In' is termed 'Remember Me' in the Portal application.
|
||||
public static class KeepLoggedIn
|
||||
{
|
||||
public static string CookieName = "BMKLI";
|
||||
public static string Base64Key = SacsEncryptionHelper.EncodePlainKey("<=SaBusMgr^P0rt4L{KeepLoggedIn}~", SacsEncryptionHelper.KeyLength);
|
||||
//private static String KeepLoggedIn.Base64IV_Generated = SacsEncryptionHelper.InitSymmetricEncryptionIV(KeepLoggedIn.Base64Key);
|
||||
public static string Base64IV = "m47AMcMgACp3aoQVc0Y92A==";
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -282,7 +277,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Components
|
||||
keepLoggedInParams.Add(ParamName_KeepLoggedIn_SecurityItemId, SysId.ToString(SessionData_Portal.User.SecurityItemId));
|
||||
keepLoggedInParams.Add(ParamName_KeepLoggedIn_AffiliateIdUser, SysId.ToString(SessionData_Portal.User.AffiliateId));
|
||||
keepLoggedInParams.Add(ParamName_KeepLoggedIn_AssociationIdOrganisation, SysId.ToString(SessionData_Portal.Organisation.AssociationId));
|
||||
return EncryptParameters(keepLoggedInParams, KeepLoggedIn.Base64IV, KeepLoggedIn.Base64Key);
|
||||
return EncryptParameters(keepLoggedInParams, KeepLoggedIn.Base64Key, SbmEncryptionPurpose.PortalKeepLoggedIn);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
@@ -325,7 +320,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Components
|
||||
KeepLoggedInParams keepLoggedInParams = new KeepLoggedInParams();
|
||||
if (!encryptedParams.IsEmpty())
|
||||
{
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, KeepLoggedIn.Base64IV, KeepLoggedIn.Base64Key);
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, KeepLoggedIn.Base64Key, SbmEncryptionPurpose.PortalKeepLoggedIn);
|
||||
keepLoggedInParams.RepositoryContext = linkParams.Get(ParamName_KeepLoggedIn_RepositoryContext);
|
||||
keepLoggedInParams.EmailAddress = linkParams.Get(ParamName_KeepLoggedIn_EmailAddress);
|
||||
keepLoggedInParams.SecurityItemId = SysId.ToSysId(linkParams.Get(ParamName_KeepLoggedIn_SecurityItemId));
|
||||
|
||||
@@ -6,6 +6,7 @@ using Sarand.BusinessMgr.Net.Standard.Base.App;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Bus;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Consoles;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Core;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Data;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Mgr;
|
||||
using Sarand.BusinessMgr.Web.Portal.App;
|
||||
using Sarand.Net.Standard.Base.App;
|
||||
@@ -160,11 +161,11 @@ namespace Sarand.BusinessMgr.Web.Portal.Components
|
||||
//-------------------------------------------------------------------
|
||||
// Parameter encryption, eg. for images.
|
||||
//-------------------------------------------------------------------
|
||||
// Encryption key is the same for all users but a different IV value is generated for each session so that the links/parameters cannot be shared.
|
||||
// Encryption key is shared by the application. Purpose-bound authentication prevents cross-context reuse.
|
||||
public static string Base64Key_PortalApp { get; } = SacsEncryptionHelper.EncodePlainKey("{SaBusMgr=Portal-App#Encryption}", SacsEncryptionHelper.KeyLength);
|
||||
protected string EncryptPortalParams(NameValueCollection parameters)
|
||||
{
|
||||
return HttpUtility.UrlEncode(EncryptParameters(parameters, SessionData_Portal.Base64IV_PortalApp, Base64Key_PortalApp));
|
||||
return HttpUtility.UrlEncode(EncryptParameters(parameters, Base64Key_PortalApp, SbmEncryptionPurpose.PortalSessionParameters));
|
||||
}
|
||||
public static string EncryptPortalParams(SbmController_Portal controller, NameValueCollection parameters)
|
||||
{
|
||||
@@ -172,7 +173,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Components
|
||||
}
|
||||
protected NameValueCollection ExtractPortalParams(string encryptedParams)
|
||||
{
|
||||
return DecryptParameters(encryptedParams, SessionData_Portal.Base64IV_PortalApp, Base64Key_PortalApp);
|
||||
return DecryptParameters(encryptedParams, Base64Key_PortalApp, SbmEncryptionPurpose.PortalSessionParameters);
|
||||
}
|
||||
|
||||
//-------------------------------------------------------------------
|
||||
|
||||
@@ -266,12 +266,8 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Home
|
||||
//-------------------------------------------------------------------
|
||||
// Demo Account Registration link.
|
||||
|
||||
// Portal Demo Account Registration encryption parameters (key & IV).
|
||||
// NB IV value is generated from the key and fixed so that it doesn't change when the application restarts.
|
||||
// IV Must be regenerated if key is changed.
|
||||
// Portal Demo Account Registration encryption key.
|
||||
protected static string Base64Key_RegisterDemoAccount_Portal { get; } = SacsEncryptionHelper.EncodePlainKey("<s58usin3S5m6R#Register@Demo~Account]", SacsEncryptionHelper.KeyLength);
|
||||
//private static String Base64IV_RegisterDemoAccount_Generated_Portal = SacsEncryptionHelper.InitSymmetricEncryptionIV(Base64Key_RegisterDemoAccount_Portal);
|
||||
protected static string Base64IV_RegisterDemoAccount_Portal { get; } = "0PWPChEJgMzjiZ2yM8E2Kw==";
|
||||
|
||||
// Link parameters.
|
||||
private static String ParamName_RegisterDemoAccountEncrypted { get; } = "rda";
|
||||
@@ -286,7 +282,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Home
|
||||
parameters.Add(ParamName_Token, registerDemoAccountToken.ToString());
|
||||
String link =
|
||||
$"{GetRedirectLocation(RedirectLocation.WebAppBase)}Demo/Register?" +
|
||||
$"{ParamName_RegisterDemoAccountEncrypted}={HttpUtility.UrlEncode(EncryptParameters(parameters, Base64IV_RegisterDemoAccount_Portal, Base64Key_RegisterDemoAccount_Portal))}";
|
||||
$"{ParamName_RegisterDemoAccountEncrypted}={HttpUtility.UrlEncode(EncryptParameters(parameters, Base64Key_RegisterDemoAccount_Portal, SbmEncryptionPurpose.PortalDemoRegistration))}";
|
||||
return link;
|
||||
}
|
||||
private int GetRegisterDemoAccountLinkExpiresMinutes()
|
||||
@@ -335,7 +331,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Home
|
||||
RegisterDemoAccountParams registerDemoAccountParams = new RegisterDemoAccountParams();
|
||||
if (!StringUtil.IsEmpty(encryptedParams))
|
||||
{
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, Base64IV_RegisterDemoAccount_Portal, Base64Key_RegisterDemoAccount_Portal);
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, Base64Key_RegisterDemoAccount_Portal, SbmEncryptionPurpose.PortalDemoRegistration);
|
||||
registerDemoAccountParams.EmailAddress = linkParams.Get(ParamName_EmailAddress);
|
||||
registerDemoAccountParams.ExpiresAt = DateUtil.ParseXmlDateTime(linkParams.Get(ParamName_ExpiresAt));
|
||||
if (!StringUtil.IsBlank(linkParams.Get(ParamName_Token)))
|
||||
|
||||
@@ -3,6 +3,7 @@ using Sarand.BusinessMgr.Net.Standard.Base.App;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Bus;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Consoles;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Core;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Data;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Helpers;
|
||||
using Sarand.BusinessMgr.Net.Standard.Base.Mgr;
|
||||
using Sarand.BusinessMgr.Web.Portal.App;
|
||||
@@ -551,7 +552,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
|
||||
private static String ParamName_AffiliateId { get; } = "afi";
|
||||
private static String ParamName_ExpiresAt { get; } = "exp";
|
||||
private static String ParamName_Token { get; } = "tok";
|
||||
// Portal Request Password encryption parameters (key & IV) are defined in the base Controller
|
||||
// Portal Request Password encryption key is defined in the base Controller
|
||||
// so that they can be shared with the Console application.
|
||||
private String GenerateRequestPasswordLink(String repositoryContext, SysId securityItemId, SysId affiliateId, String emailAddress, String requestPasswordToken)
|
||||
{
|
||||
@@ -564,7 +565,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
|
||||
parameters.Add(ParamName_Token, requestPasswordToken);
|
||||
String link =
|
||||
GetRedirectLocation(RedirectLocation.WebAppBase) + "Security/ChangePassword?" +
|
||||
ParamName_RequestPasswordEncrypted + "=" + HttpUtility.UrlEncode(EncryptParameters(parameters, Base64IV_RequestPassword_Portal, Base64Key_RequestPassword_Portal));
|
||||
ParamName_RequestPasswordEncrypted + "=" + HttpUtility.UrlEncode(EncryptParameters(parameters, Base64Key_RequestPassword_Portal, SbmEncryptionPurpose.PortalPasswordReset));
|
||||
return link;
|
||||
}
|
||||
private int GetRequestPasswordEmailLinkExpiresMinutes()
|
||||
@@ -627,7 +628,7 @@ namespace Sarand.BusinessMgr.Web.Portal.Controllers.Security
|
||||
RequestPasswordParams requestPasswordParams = new RequestPasswordParams();
|
||||
if (!StringUtil.IsEmpty(encryptedParams))
|
||||
{
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, Base64IV_RequestPassword_Portal, Base64Key_RequestPassword_Portal);
|
||||
NameValueCollection linkParams = DecryptParameters(encryptedParams, Base64Key_RequestPassword_Portal, SbmEncryptionPurpose.PortalPasswordReset);
|
||||
requestPasswordParams.RepositoryContext = linkParams.Get(ParamName_RepositoryContext);
|
||||
requestPasswordParams.EmailAddress = linkParams.Get(ParamName_EmailAddress);
|
||||
requestPasswordParams.SecurityItemId = new SysId(StringUtil.ToInt(linkParams.Get(ParamName_SecurityItemId)));
|
||||
|
||||
@@ -8,6 +8,7 @@ An optional informational overview of sibling workspaces is available in `../../
|
||||
|
||||
- `topics.md` tracks prioritised work, work in progress, and completed work.
|
||||
- `component-reviews.md` records which projects, paths, and classes have been reviewed.
|
||||
- `encryption-consumers.md` inventories BusinessMgr encryption uses and their migration constraints.
|
||||
|
||||
## Tracking Rules
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ Last updated: 2026-09-16
|
||||
|
||||
| Component | Scope | Status | Date reviewed | Next steps | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| `BusinessMgr.sln` | .NET 10 restore, Debug build, and tests | Migrated; passing with warnings | 2026-09-16 | Perform application smoke tests, then release-validate. | All eight projects target `net10.0`. Razor runtime compilation 10.0.12, System.Drawing.Common 10.0.12, and the tested Common database dependencies are aligned with .NET 10. Control tests pass 11 of 11 and Portal tests pass 13 of 13. |
|
||||
| BusinessMgr encrypted cookies, links, session parameters, and service traffic | Authenticated-encryption migration | Clean-break migration implemented | 2026-09-16 | Smoke-test cookies, emailed links, session URLs, and end-to-end service traffic; later externalise and rotate retained in-code keys. | All consumers use Common's purpose-bound `SACSENC2.` AES-GCM envelope with fresh nonces. Fixed/session IVs, CBC, legacy reads, and plaintext fallback are removed. Existing encrypted values are intentionally invalidated; coordinated service deployment is required. See [`encryption-consumers.md`](encryption-consumers.md). |
|
||||
| `BusinessMgr.sln` | .NET 10 restore, Debug build, and tests | Migrated; passing with warnings | 2026-09-16 | Perform application smoke tests, then release-validate. | All eight projects target `net10.0`. Razor runtime compilation 10.0.12, System.Drawing.Common 10.0.12, and the tested Common database dependencies are aligned with .NET 10. Control tests pass 15 of 15 and Portal tests pass 13 of 13. |
|
||||
| `SaBusMgrCoreWeb` and web applications | Razor runtime compilation | Dependency aligned with .NET 10 | 2026-09-16 | Retain the package-owner audits and cover runtime view compilation in application smoke testing. | Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation 10.0.12 is referenced by the shared web project and both applications. The solution build and both test suites pass, and all three package audits report no known vulnerabilities. |
|
||||
| `SaBusMgrCoreProcs/Procs/Reports` | System.Drawing.Common and legacy report printing | Dependency aligned; Windows-only architecture review deferred | 2026-09-16 | Revisit only when report printing is required or cross-platform report output is planned. | System.Drawing.Common 10.0.12 builds and audits cleanly. The Windows-only subsystem uses `Graphics`, printing, fonts, brushes, text measurement, and footer rendering, but the abstract `SbmReport` currently has no implementations or call sites. Future options remain isolation/removal, a dedicated Windows-targeted project with rendering tests, or replacement with SkiaSharp-based image/PDF reporting. |
|
||||
| `SaBusMgrStandardBase/Base/Helpers/SbmPasswordHelper.cs` and `SaBusinessMgrCoreWebControl/Control/Helpers/SbmcPasswordHelper.cs` | Password hashing | Dependency upgrade complete; legacy hashes compatible | 2026-09-13 | Preserve the compatibility tests when password-hashing defaults or account migration behavior changes. | Both helpers use `PasswordHasher<TUser>` from `Microsoft.Extensions.Identity.Core` 10.0.12. Current hashes round-trip, wrong passwords fail, and legacy Identity V3 hashes remain valid while being identified for rehash. |
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Encryption Consumer Inventory
|
||||
|
||||
<style>
|
||||
table th,
|
||||
table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
</style>
|
||||
|
||||
Date reviewed: 2026-09-16
|
||||
|
||||
## Implemented Clean-Break Decision
|
||||
|
||||
All listed consumers now write and read only Common's `SACSENC2.` AES-GCM envelope. Each operation uses a fresh 12-byte nonce carried in the envelope, a 16-byte authentication tag, and a consumer-specific purpose. The existing in-code keys are retained as agreed; fixed IV constants and per-session IV state are removed.
|
||||
|
||||
No legacy data needs to remain decryptable, so there are no dual readers or retention windows. Existing login cookies and session URLs are invalidated, outstanding password-reset and demo-registration links must be reissued, and coupled Windows-service clients and servers must be deployed together. Authentication failures fail closed and are never treated as plaintext.
|
||||
|
||||
Moving keys into protected configuration and adding key identifiers and rotation remains a future hardening task. Existing expiry and one-time-token checks remain necessary because authenticated encryption does not enforce either behavior.
|
||||
|
||||
## Superseded Staged-Migration Analysis
|
||||
|
||||
The inventory below records the earlier compatibility-preserving analysis. Its dual-read and staged-retirement recommendations were superseded by the clean-break decision above and are not implementation instructions.
|
||||
|
||||
The shared envelope and rollout proposal is owned by Common in [`docs/authenticated-encryption-migration.md`](../../../Common-VS/Workspace/docs/authenticated-encryption-migration.md). This document records BusinessMgr-specific uses and migration constraints.
|
||||
|
||||
## Consumers
|
||||
|
||||
| Consumer | Locations | Data lifetime | Current key/IV behavior | Migration requirement |
|
||||
|---|---|---|---|---|
|
||||
| Windows-service request and response traffic | `SaBusMgrStandardBase/Base/Data/SbmWindowsServiceCrypter.cs`, `Base/WebService/SbmWebServiceClient.cs`, and `Base/WebService/SbmWebServiceResponse.cs` | Transient transport data, but independently deployed clients and servers may overlap versions | Separate embedded password strings for requests and responses; the former `SacsAesIvCrypter` supplied a random IV and legacy fixed-salt PBKDF2 key | Deploy dual readers everywhere before version 2 writers. If clients cannot be upgraded atomically, add explicit protocol capability/version negotiation. Keep request and response purposes and keys separate. Never fall back to returning ciphertext as plaintext after an authenticated-envelope failure. |
|
||||
| Control keep-logged-in cookie | `SaBusinessMgrCoreWebControl/Control/Components/SbmController_Control.cs` | Persisted in the browser for 1–30 configured days (10 days by default) and survives application restarts | Embedded repeated/truncated key and fixed IV | Write version 2 cookies after dual-read deployment. Retain legacy reads for at least 30 days after the writer switch, then expire or reject remaining legacy cookies. Bind associated data to the Control cookie purpose. |
|
||||
| Portal keep-logged-in cookie | `SaBusinessMgrCoreWebPortal/Portal/Components/SbmController_Portal-Security.cs` | Persisted in the browser for 1–30 configured days (10 days by default) and survives application restarts | Separate embedded repeated/truncated key and fixed IV | Apply the same staged cookie migration independently from Control and retain legacy reads for at least 30 days. Bind associated data to the Portal cookie purpose and avoid accepting Control ciphertext. |
|
||||
| Control and Portal password-reset links | Both `SecurityController-Ajax.cs` implementations and `SaBusMgrCoreWeb/Web/Components/SbmController_Web.cs` | Persisted externally in email for 30–240 configured minutes (60 minutes by default) | Embedded keys and fixed IVs, with separate Control and Portal values | Dual-read links before changing writers. Retain legacy reads for at least four hours plus deployment overlap. Authenticate the application, link purpose, and version; continue validating the embedded expiry and one-time token. |
|
||||
| Portal demo-registration links | `SaBusinessMgrCoreWebPortal/Portal/Controllers/Home/DemoController-Ajax.cs` | Persisted externally for 30 minutes to 24 configured hours (60 minutes by default) | Dedicated embedded key and fixed IV | Migrate independently with dual reads for at least 24 hours plus deployment overlap. Use a distinct purpose from password-reset links. |
|
||||
| Control and Portal session parameter URLs | `SbmController_Control.cs`, `SbmController_Portal.cs`, session-data classes, and their downstream controller/view call sites | Session-scoped; keys change daily and IVs are generated per session | Date-derived embedded key plus a per-session IV | These can move after the shared parser is available. Existing links may be invalidated at deployment or accepted through legacy reads for the session lifetime. Version 2 must generate a new nonce per value rather than reuse the session IV. |
|
||||
|
||||
## Cross-Cutting Findings
|
||||
|
||||
- The generic `SaController.EncryptParameters` and `DecryptParameters` methods hide whether data is a cookie, email link, or session URL. Version 2 APIs need an explicit purpose so authenticated values cannot be replayed across contexts.
|
||||
- Several keys and fixed IVs are embedded in source. They must be replaced by environment-provided key rings, with non-secret key identifiers in the envelope and no production secrets committed to either repository.
|
||||
- Current decrypt wrappers often catch an exception and return an empty collection or even the original input. An authenticated version 2 value that fails verification must never be retried as legacy or treated as plaintext.
|
||||
- Keep-logged-in cookie values contain account and organisation identifiers. Existing logging includes complete cookie values in debug messages; ciphertext should not be logged during the migration.
|
||||
- Existing expiry and token checks remain necessary. Authenticated encryption prevents undetected modification but does not itself make a link single-use or enforce expiration.
|
||||
|
||||
## Recommended Delivery Order
|
||||
|
||||
1. Implement and test the shared Common envelope and key-ring abstractions without changing BusinessMgr writes.
|
||||
2. Add BusinessMgr configuration binding and secret deployment for separate Control, Portal, service-request, and service-response keys.
|
||||
3. Deploy dual readers for every consumer.
|
||||
4. Switch session URLs first because their compatibility window is shortest.
|
||||
5. Switch password-reset and demo-registration links, retaining legacy reads through their maximum expiry window.
|
||||
6. Switch keep-logged-in cookies, retaining legacy reads through their configured maximum lifetime.
|
||||
7. Switch service traffic only after all independently deployed peers support version 2 or negotiate capabilities.
|
||||
8. Remove legacy readers, embedded keys, and fixed IVs only after telemetry confirms that legacy traffic has ceased and the relevant retention windows have elapsed.
|
||||
@@ -13,6 +13,7 @@ Last updated: 2026-09-16
|
||||
|
||||
| Priority | Topic | Date noted | Next step | Notes |
|
||||
|---|---|---|---|---|
|
||||
| P2 | Externalise and rotate BusinessMgr encryption keys | 2026-09-16 | Move the retained in-code keys to protected configuration or a secret store and define coordinated key rotation. | The clean-break AES-GCM migration is complete. Existing key strings were retained by explicit decision; fixed/session IVs and legacy support were removed. |
|
||||
| P3 deferred | Review legacy report rendering and printing | 2026-09-10 | Revisit only when report printing is required or cross-platform output is planned. | System.Drawing.Common is aligned at 10.0.12, but `SbmReport` remains Windows-only and currently has no implementations or external call sites. Later options are to isolate inactive source and remove the package; isolate active printing in a Windows-targeted project with rendering tests; or redesign reporting around SkiaSharp and generated image/PDF output. ImageSharp is not a direct replacement for printer integration. |
|
||||
|
||||
## .NET 10 Dependencies and Sequence
|
||||
@@ -34,6 +35,7 @@ Last updated: 2026-09-16
|
||||
|
||||
| Topic | Date noted | Date started | Date completed | Outcome |
|
||||
|---|---|---|---|---|
|
||||
| Migrate encrypted cookies, links, session parameters, and service traffic to authenticated encryption | 2026-09-16 | 2026-09-16 | 2026-09-16 | Switched every listed consumer to purpose-bound `SACSENC2.` AES-GCM with a fresh nonce per value. Removed fixed and session IVs, legacy reads, and service plaintext fallback. Existing encrypted values are intentionally invalidated and service peers require coordinated deployment; see [`encryption-consumers.md`](encryption-consumers.md). |
|
||||
| Align System.Drawing.Common with .NET 10 | 2026-09-16 | 2026-09-16 | 2026-09-16 | Updated System.Drawing.Common from 6.0.0 to 10.0.12 in `SaBusMgrCoreProcs`. The complete solution builds, Control passes 11 tests, Portal passes 13 tests, and the package audit reports no known vulnerabilities. Further architectural concern about the dormant Windows-only report code is deferred. |
|
||||
| Align Razor runtime compilation with .NET 10 | 2026-09-16 | 2026-09-16 | 2026-09-16 | Updated Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation from 6.0.36 to 10.0.12 in the shared web project and both web applications. The complete solution builds; Control passes 11 tests and Portal passes 13 tests. Automated validation now audits all three web package owners, and all report no known vulnerable packages. |
|
||||
| Validate the Common Microsoft.Data.SqlClient 7 upgrade | 2026-09-15 | 2026-09-15 | 2026-09-15 | Common now supplies Microsoft.Data.SqlClient 7.0.3 transitively. BusinessMgr builds successfully and both NUnit suites pass without requiring consumer source or package changes. |
|
||||
|
||||
Reference in New Issue
Block a user