From 8bb11ca7435ad5fadac5f6c79a414484b43f6ebd Mon Sep 17 00:00:00 2001 From: Andy Howard Date: Wed, 16 Sep 2026 18:41:44 +0100 Subject: [PATCH] Migrate encryption consumers to AES-GCM --- .../Web/Components/SbmController_Web.cs | 7 +- .../Base/Data/SbmEncryptionPurpose.cs | 21 ++++++ .../Base/Data/SbmWindowsServiceCrypter.cs | 49 ++++--------- .../Base/Data/SbmEncryption_Tester.cs | 71 +++++++++++++++++++ .../App/SbmApplication_SessionData_Tester.cs | 3 - .../App/SbmApplication_SessionData_Tester.cs | 3 - .../Control/App/SbmSessionData_Control.cs | 4 -- .../Components/SbmController_Control.cs | 18 ++--- .../Security/SecurityController-Ajax.cs | 11 ++- .../Portal/App/SbmSessionData_Portal.cs | 4 -- .../SbmController_Portal-Security.cs | 11 +-- .../Portal/Components/SbmController_Portal.cs | 7 +- .../Controllers/Home/DemoController-Ajax.cs | 10 +-- .../Security/SecurityController-Ajax.cs | 7 +- docs/README.md | 1 + docs/component-reviews.md | 3 +- docs/encryption-consumers.md | 54 ++++++++++++++ docs/topics.md | 2 + 18 files changed, 190 insertions(+), 96 deletions(-) create mode 100644 SaBusMgrStandardBase/Base/Data/SbmEncryptionPurpose.cs create mode 100644 SaBusMgrTest_Control/Base/Data/SbmEncryption_Tester.cs create mode 100644 docs/encryption-consumers.md diff --git a/SaBusMgrCoreWeb/Web/Components/SbmController_Web.cs b/SaBusMgrCoreWeb/Web/Components/SbmController_Web.cs index 3c45811..cafcd24 100644 --- a/SaBusMgrCoreWeb/Web/Components/SbmController_Web.cs +++ b/SaBusMgrCoreWeb/Web/Components/SbmController_Web.cs @@ -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. diff --git a/SaBusMgrStandardBase/Base/Data/SbmEncryptionPurpose.cs b/SaBusMgrStandardBase/Base/Data/SbmEncryptionPurpose.cs new file mode 100644 index 0000000..143f405 --- /dev/null +++ b/SaBusMgrStandardBase/Base/Data/SbmEncryptionPurpose.cs @@ -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 --- + } +} diff --git a/SaBusMgrStandardBase/Base/Data/SbmWindowsServiceCrypter.cs b/SaBusMgrStandardBase/Base/Data/SbmWindowsServiceCrypter.cs index 3c268ea..4198a01 100644 --- a/SaBusMgrStandardBase/Base/Data/SbmWindowsServiceCrypter.cs +++ b/SaBusMgrStandardBase/Base/Data/SbmWindowsServiceCrypter.cs @@ -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 --- + /// /// Encryption is shared between Windows Service app and mobile /// private static readonly String requestDataKey = "~{BusinessMgr}#[WindowsService]&/DataCryption/*|RequestData|"; private static readonly String responseDataKey = "{Sarand}+(BusinessMgr)%\\WindowsService/=@:Response:"; - //------------------------------------------------------------------- - // R e q u e s t D a t a - //------------------------------------------------------------------- + // --- Request Data --- + /// Encrypt request data 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); } + /// Decrypt request data 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 --- + /// Encrypt response data 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); } + /// Decrypt response data 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 --- } - //======================================================================= } diff --git a/SaBusMgrTest_Control/Base/Data/SbmEncryption_Tester.cs b/SaBusMgrTest_Control/Base/Data/SbmEncryption_Tester.cs new file mode 100644 index 0000000..b82b698 --- /dev/null +++ b/SaBusMgrTest_Control/Base/Data/SbmEncryption_Tester.cs @@ -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()); + } + + [Test] + public void WindowsServiceCrypter_RejectsLegacyData() + { + Assert.That( + () => SbmWindowsServiceCrypter.DecryptRequestData("legacy-or-plaintext"), + Throws.TypeOf()); + } + + // --- 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 --- + } +} diff --git a/SaBusMgrTest_Control/Web/Control/App/SbmApplication_SessionData_Tester.cs b/SaBusMgrTest_Control/Web/Control/App/SbmApplication_SessionData_Tester.cs index 059112c..50d2eef 100644 --- a/SaBusMgrTest_Control/Web/Control/App/SbmApplication_SessionData_Tester.cs +++ b/SaBusMgrTest_Control/Web/Control/App/SbmApplication_SessionData_Tester.cs @@ -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); diff --git a/SaBusMgrTest_Portal/Web/Portal/App/SbmApplication_SessionData_Tester.cs b/SaBusMgrTest_Portal/Web/Portal/App/SbmApplication_SessionData_Tester.cs index c303949..bacf0a2 100644 --- a/SaBusMgrTest_Portal/Web/Portal/App/SbmApplication_SessionData_Tester.cs +++ b/SaBusMgrTest_Portal/Web/Portal/App/SbmApplication_SessionData_Tester.cs @@ -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); diff --git a/SaBusinessMgrCoreWebControl/Control/App/SbmSessionData_Control.cs b/SaBusinessMgrCoreWebControl/Control/App/SbmSessionData_Control.cs index f57a2ee..da2d594 100644 --- a/SaBusinessMgrCoreWebControl/Control/App/SbmSessionData_Control.cs +++ b/SaBusinessMgrCoreWebControl/Control/App/SbmSessionData_Control.cs @@ -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); - //------------------------------------------------------------------- } //======================================================================= diff --git a/SaBusinessMgrCoreWebControl/Control/Components/SbmController_Control.cs b/SaBusinessMgrCoreWebControl/Control/Components/SbmController_Control.cs index dcd1bba..e327ebd 100644 --- a/SaBusinessMgrCoreWebControl/Control/Components/SbmController_Control.cs +++ b/SaBusinessMgrCoreWebControl/Control/Components/SbmController_Control.cs @@ -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); } //------------------------------------------------------------------- diff --git a/SaBusinessMgrCoreWebControl/Control/Controllers/Security/SecurityController-Ajax.cs b/SaBusinessMgrCoreWebControl/Control/Controllers/Security/SecurityController-Ajax.cs index b0d71f6..2bd7a38 100644 --- a/SaBusinessMgrCoreWebControl/Control/Controllers/Security/SecurityController-Ajax.cs +++ b/SaBusinessMgrCoreWebControl/Control/Controllers/Security/SecurityController-Ajax.cs @@ -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=="; /// /// Generate Request Password link. /// NB For a Portal application request (from the Control system), must ensure that the URL and encryption parameters are switched. @@ -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)); diff --git a/SaBusinessMgrCoreWebPortal/Portal/App/SbmSessionData_Portal.cs b/SaBusinessMgrCoreWebPortal/Portal/App/SbmSessionData_Portal.cs index 4c5de61..fbb78e6 100644 --- a/SaBusinessMgrCoreWebPortal/Portal/App/SbmSessionData_Portal.cs +++ b/SaBusinessMgrCoreWebPortal/Portal/App/SbmSessionData_Portal.cs @@ -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); - //------------------------------------------------------------------- } //======================================================================= diff --git a/SaBusinessMgrCoreWebPortal/Portal/Components/SbmController_Portal-Security.cs b/SaBusinessMgrCoreWebPortal/Portal/Components/SbmController_Portal-Security.cs index 2e7e87b..b1803c1 100644 --- a/SaBusinessMgrCoreWebPortal/Portal/Components/SbmController_Portal-Security.cs +++ b/SaBusinessMgrCoreWebPortal/Portal/Components/SbmController_Portal-Security.cs @@ -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)); diff --git a/SaBusinessMgrCoreWebPortal/Portal/Components/SbmController_Portal.cs b/SaBusinessMgrCoreWebPortal/Portal/Components/SbmController_Portal.cs index cbdf033..6dff013 100644 --- a/SaBusinessMgrCoreWebPortal/Portal/Components/SbmController_Portal.cs +++ b/SaBusinessMgrCoreWebPortal/Portal/Components/SbmController_Portal.cs @@ -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); } //------------------------------------------------------------------- diff --git a/SaBusinessMgrCoreWebPortal/Portal/Controllers/Home/DemoController-Ajax.cs b/SaBusinessMgrCoreWebPortal/Portal/Controllers/Home/DemoController-Ajax.cs index ce8baaf..eec852a 100644 --- a/SaBusinessMgrCoreWebPortal/Portal/Controllers/Home/DemoController-Ajax.cs +++ b/SaBusinessMgrCoreWebPortal/Portal/Controllers/Home/DemoController-Ajax.cs @@ -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("` 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. | diff --git a/docs/encryption-consumers.md b/docs/encryption-consumers.md new file mode 100644 index 0000000..b9ed961 --- /dev/null +++ b/docs/encryption-consumers.md @@ -0,0 +1,54 @@ +# Encryption Consumer Inventory + + + +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. diff --git a/docs/topics.md b/docs/topics.md index cc355cc..50f56d0 100644 --- a/docs/topics.md +++ b/docs/topics.md @@ -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. |