From 3de1c860c9b7cec0180f7d99033d1824b28e64a7 Mon Sep 17 00:00:00 2001 From: LSXPrime Date: Wed, 20 Nov 2024 22:17:23 +0200 Subject: [PATCH] Feat: Enhance Serialization and Hardware Identification ## Key Changes * **Serialization Improvements:** Introduced a new serialization mechanism with `ILicenseSerializer` and `JsonLicenseSerializer` to enhance flexibility and support different serialization formats. * **Custom Hardware Identification:** Added `IHardwareIdentifier` interface and `DefaultHardwareIdentifier` to allow custom hardware identification logic. * **Simplified Offline Validation Logic:** Refactored offline validation in `LicenseManager` to provide a more streamlined approach and allow bypassing built-in validation logic using `SetBuiltInValidation(bool enable)`. * **Dependency Removal:** Removed the `DeviceId` NuGet package dependency. * **Saving Arguments Conflict:** Updated the `SaveLicense` method in `LicenseBuilder` to `SaveLicenseToPath` to fix conflicts caused by `privateKey` parameter. * **Updated Tests:** Updated the tests to reflect the changes in the library. ## Detailed Changes ### New Files * **Interfaces/IHardwareIdentifier.cs:** Defines the interface for custom hardware identification. * **Interfaces/ILicenseSerializer.cs:** Defines the interface for license serialization. * **Serialization/JsonLicenseSerializer.cs:** Provides a JSON implementation of the `ILicenseSerializer` interface. * **Serialization/Converters/JsonLicenseConverter.cs:** A custom JSON converter for handling license type inheritance. * **Utilities/DefaultHardwareIdentifier.cs:** Provides the default hardware identification logic. ### Modified Files * **LicenseBuilder.cs:** Changed `SaveLicense` to call `LicenseManager.SaveLicenseToPath`. * **LicenseGenerator.cs:** Now uses `IHardwareIdentifier` for retrieving hardware IDs. Has a new internal `SetHardwareIdentifier` method. * **LicenseManager.cs:** * Added `SetSerializer` and `SetHardwareIdentifier` methods. * Added `SetBuiltInValidation` method. * Renamed `SaveLicense` to `SaveLicenseToPath`. * Now uses `_serializer.Serialize` for serialization. * **LicenseValidator.cs:** * Added `SetSerializer` and `SetHardwareIdentifier` methods. * Uses injected `_serializer` for deserialization. * `ValidateNodeLockedLicense` method now uses the injected `_hardwareIdentifier`. * **Exceptions/InvalidLicenseFormatException.cs:** Added constructors with `innerException` parameter. * **Aegis.csproj:** * Updated version to 1.3.0. * Removed the `DeviceId` package reference. * **Utilities/HardwareUtils.cs:** Functionality moved to the new `DefaultHardwareIdentifier` class. File deleted. * **Utilities/LicenseUtils.cs:** No functional changes, but the `LoadLicenseSecrets` method documentation was updated. * **Utilities/SecurityUtils.cs:** No functional changes, but the `CalculateSha256Hash` method is now public. ## Upgrade Considerations * If you were relying on the specific hardware identification implementation of the previous version using `HardwareUtils`, you will need to inject your custom implementation of `IHardwareIdentifier` or use the new provided `DefaultHardwareIdentifier`. * If you were using a different serialization mechanism, you can now implement the `ILicenseSerializer` interface and inject it using `LicenseManager.SetSerializer`. --- .github/workflows/greetings.yml | 19 ++++ .github/workflows/stale.yml | 37 ++++++ README.md | 105 ++++++++++++++++-- src/Aegis.Server/Aegis.Server.csproj | 6 +- src/Aegis/Aegis.csproj | 7 +- .../InvalidLicenseFormatException.cs | 11 +- src/Aegis/Interfaces/IHardwareIdentifier.cs | 7 ++ src/Aegis/Interfaces/ILicenseSerializer.cs | 9 ++ src/Aegis/LicenseBuilder.cs | 2 +- src/Aegis/LicenseGenerator.cs | 11 +- src/Aegis/LicenseManager.cs | 88 ++++++++++----- src/Aegis/LicenseValidator.cs | 21 +++- .../Converters/JsonLicenseConverter.cs | 43 +++++++ .../Serialization/JsonLicenseSerializer.cs | 32 ++++++ .../Utilities/DefaultHardwareIdentifier.cs | 47 ++++++++ src/Aegis/Utilities/HardwareUtils.cs | 30 ----- tests/Aegis.Tests/LicenseManagerTests.cs | 10 +- tests/Aegis.Tests/LicenseValidatorTests.cs | 2 +- 18 files changed, 401 insertions(+), 86 deletions(-) create mode 100644 .github/workflows/greetings.yml create mode 100644 .github/workflows/stale.yml create mode 100644 src/Aegis/Interfaces/IHardwareIdentifier.cs create mode 100644 src/Aegis/Interfaces/ILicenseSerializer.cs create mode 100644 src/Aegis/Serialization/Converters/JsonLicenseConverter.cs create mode 100644 src/Aegis/Serialization/JsonLicenseSerializer.cs create mode 100644 src/Aegis/Utilities/DefaultHardwareIdentifier.cs delete mode 100644 src/Aegis/Utilities/HardwareUtils.cs diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 0000000..b6c5b95 --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,19 @@ +name: Greetings + +on: pull_request_target + +jobs: + greeting: + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + pr-message: | + Welcome to Aegis repository! We appreciate you taking the time to contribute. + + We're excited to review your pull request and look forward to collaborating with you. Please let us know if you have any questions or need any assistance. + + Thank you for your contribution! \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..d247572 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,37 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '44 8 * * *' + +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: | + This issue has been automatically marked as stale because it has not had recent activity. + It will be closed if no further activity occurs. + + If this issue is still relevant, please leave a comment indicating that you would like it to remain open. + Thank you for your contributions. + stale-pr-message: | + This pull request has been automatically marked as stale because it has not had recent activity. + It will be closed if no further activity occurs. + + If you are still working on this pull request, please leave a comment indicating that you would like it to remain open. + Thank you for your contributions. + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' \ No newline at end of file diff --git a/README.md b/README.md index 1d310d8..6035fd8 100644 --- a/README.md +++ b/README.md @@ -271,11 +271,12 @@ adapt the provided code to your specific needs. ### Custom Validation Rules -Aegis allows you to implement custom validation rules to enforce specific licensing requirements beyond the built-in validation logic. This provides greater flexibility and control over your licensing system. +Aegis allows you to implement custom validation rules to enforce specific licensing requirements beyond the built-in validation logic. +This provides greater flexibility and control over your licensing system. 1. **Implementing a Custom Rule** - - To create a custom validation rule, implement the `IValidationRule` interface: + + To create a custom validation rule, implement the `IValidationRule` interface: ```csharp using Aegis.Interfaces; @@ -304,15 +305,16 @@ Aegis allows you to implement custom validation rules to enforce specific licens 2. **Registering the Rule** - Once you have implemented your custom rule, register it with the `LicenseValidator`: - + Once you have implemented your custom rule, register it with the `LicenseValidator`: + ```csharp LicenseValidator.AddValidationRule(new MyCustomRule()); ``` 3. **Example: Advanced Hardware Validation for Node-Locked Licenses** - This example demonstrates a more robust hardware validation for node-locked licenses, considering multiple hardware factors. + This example demonstrates a more robust hardware validation for node-locked licenses, considering multiple hardware + factors. ```csharp using Aegis.Interfaces; @@ -349,19 +351,100 @@ Aegis allows you to implement custom validation rules to enforce specific licens } ``` - **Usage:** - + **Usage:** + ```csharp LicenseValidator.AddValidationRule(new AdvancedHardwareRule()); // ... during license loading ... var license = await LicenseManager.LoadLicenseAsync("license.bin"); ``` - - This example showcases how you can create a custom rule to enhance the security of your node-locked licenses by validating against multiple hardware identifiers. - You can adapt this example and implement your own logic for combining and validating different hardware factors to suit your specific needs. Remember to provide clear documentation and error messages within your custom rules to make them easy to understand and maintain. + This example showcases how you can create a custom rule to enhance the security of your node-locked licenses by + validating against multiple hardware identifiers. + + You can adapt this example and implement your own logic for combining and validating different hardware factors to + suit your specific needs. Remember to provide clear documentation and error messages within your custom rules to make + them easy to understand and maintain. + +### Custom Hardware Identification + +By default, Aegis uses a `DefaultHardwareIdentifier` that combines Machine Name, User Name, OS Version, and MAC Address. +To implement a custom hardware identifier: + +1. **Create a class that implements `IHardwareIdentifier`:** + + ```csharp + using Aegis.Interfaces; + + public class MyCustomHardwareIdentifier : IHardwareIdentifier + { + public string GetHardwareIdentifier() + { + // Your custom logic to retrieve the hardware identifier. + // Example: CPU ID, Motherboard serial number, etc. + return "..."; + } + + public bool ValidateHardwareIdentifier(string hardwareIdentifier) + { + // Your custom logic to validate the hardware identifier. + return GetHardwareIdentifier() == hardwareIdentifier; + } + } + ``` + +2. **Set the custom identifier:** + + ```csharp + LicenseManager.SetHardwareIdentifier(new MyCustomHardwareIdentifier()); + ``` + +### Custom Serialization + +Aegis uses a `JsonLicenseSerializer` by default. To implement a custom serializer: + +1. **Create a class that implements `ILicenseSerializer`:** + + ```csharp + using Aegis.Interfaces; + using Aegis.Models; + + public class MyCustomSerializer : ILicenseSerializer + { + public string Serialize(BaseLicense license) + { + // Your custom serialization logic. + // Example: XML serialization, binary serialization, etc. + return "..."; + } + + public BaseLicense? Deserialize(string data) + { + // Your custom deserialization logic. + return ...; + } + } + ``` + +2. **Set the custom serializer:** + + ```csharp + LicenseManager.SetSerializer(new MyCustomSerializer()); + ``` + +### Disabling Built-in Validation + +You can disable Aegis's built-in validation logic if you want to rely solely on your custom validation rules or have a +different validation process. + +```csharp +LicenseManager.SetBuiltInValidation(false); +``` +This will prevent `LicenseManager` from performing its default validation checks for license type, expiry date, and +other built-in criteria. You will be responsible for implementing all necessary validation logic in your custom rules or +through other means. ### Architecture diff --git a/src/Aegis.Server/Aegis.Server.csproj b/src/Aegis.Server/Aegis.Server.csproj index 944d126..5949561 100644 --- a/src/Aegis.Server/Aegis.Server.csproj +++ b/src/Aegis.Server/Aegis.Server.csproj @@ -4,8 +4,8 @@ net8.0 enable enable - 1.1.0 - 1.1.0 + 1.3.0 + 1.3.0 Aegis.Server Aegis is a robust and flexible .NET licensing library that simplifies the implementation of various licensing models for your applications. It offers strong security features, both online and offline validation, and easy integration with your existing projects. Securely manage your software licenses with Aegis. Copyright (c) 2024 LSXPrime @@ -16,7 +16,7 @@ Aegis.png README.md license, key, licensing, protect, shield - 1.2.0 + 1.3.0 https://github.com/LSXPrime/Aegis/releases diff --git a/src/Aegis/Aegis.csproj b/src/Aegis/Aegis.csproj index 21f8cef..76ef397 100644 --- a/src/Aegis/Aegis.csproj +++ b/src/Aegis/Aegis.csproj @@ -4,8 +4,8 @@ net8.0 enable enable - 1.2.0 - 1.2.0 + 1.3.0 + 1.3.0 Aegis Aegis is a robust and flexible .NET licensing library that simplifies the implementation of various licensing models for your applications. It offers strong security features, both online and offline validation, and easy integration with your existing projects. Securely manage your software licenses with Aegis. Copyright (c) 2024 LSXPrime @@ -16,12 +16,11 @@ Aegis.png README.md license, key, licensing, protect, shield - 1.2.0 + 1.3.0 https://github.com/LSXPrime/Aegis/releases - diff --git a/src/Aegis/Exceptions/InvalidLicenseFormatException.cs b/src/Aegis/Exceptions/InvalidLicenseFormatException.cs index e35ce39..ed62eac 100644 --- a/src/Aegis/Exceptions/InvalidLicenseFormatException.cs +++ b/src/Aegis/Exceptions/InvalidLicenseFormatException.cs @@ -1,3 +1,12 @@ namespace Aegis.Exceptions; -public class InvalidLicenseFormatException(string message) : LicenseValidationException(message); \ No newline at end of file +public class InvalidLicenseFormatException : LicenseValidationException +{ + public InvalidLicenseFormatException(string message) : base(message) + { + } + + public InvalidLicenseFormatException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/Aegis/Interfaces/IHardwareIdentifier.cs b/src/Aegis/Interfaces/IHardwareIdentifier.cs new file mode 100644 index 0000000..e224d22 --- /dev/null +++ b/src/Aegis/Interfaces/IHardwareIdentifier.cs @@ -0,0 +1,7 @@ +namespace Aegis.Interfaces; + +public interface IHardwareIdentifier +{ + string GetHardwareIdentifier(); + bool ValidateHardwareIdentifier(string hardwareIdentifier); +} \ No newline at end of file diff --git a/src/Aegis/Interfaces/ILicenseSerializer.cs b/src/Aegis/Interfaces/ILicenseSerializer.cs new file mode 100644 index 0000000..31c374e --- /dev/null +++ b/src/Aegis/Interfaces/ILicenseSerializer.cs @@ -0,0 +1,9 @@ +using Aegis.Models; + +namespace Aegis.Interfaces; + +public interface ILicenseSerializer +{ + string Serialize(BaseLicense license); + BaseLicense? Deserialize(string data); +} \ No newline at end of file diff --git a/src/Aegis/LicenseBuilder.cs b/src/Aegis/LicenseBuilder.cs index e82d8ad..be86033 100644 --- a/src/Aegis/LicenseBuilder.cs +++ b/src/Aegis/LicenseBuilder.cs @@ -79,7 +79,7 @@ public static BaseLicense WithLicenseKey(this BaseLicense baseLicense, string li /// The license object that was saved. public static BaseLicense SaveLicense(this BaseLicense baseLicense, string filePath) { - LicenseManager.SaveLicense(baseLicense, filePath); + LicenseManager.SaveLicenseToPath(baseLicense, filePath); return baseLicense; } } \ No newline at end of file diff --git a/src/Aegis/LicenseGenerator.cs b/src/Aegis/LicenseGenerator.cs index 3c83d7a..d1de7e2 100644 --- a/src/Aegis/LicenseGenerator.cs +++ b/src/Aegis/LicenseGenerator.cs @@ -1,10 +1,13 @@ -using Aegis.Models; +using Aegis.Interfaces; +using Aegis.Models; using Aegis.Utilities; namespace Aegis; public static class LicenseGenerator { + private static IHardwareIdentifier _hardwareIdentifier = new DefaultHardwareIdentifier(); + /// /// Generates a standard license. /// @@ -35,7 +38,7 @@ public static TrialLicense GenerateTrialLicense(TimeSpan trialPeriod) /// A new NodeLockedLicense object. public static NodeLockedLicense GenerateNodeLockedLicense(string? hardwareId = null) { - hardwareId ??= HardwareUtils.GetHardwareId(); + hardwareId ??= _hardwareIdentifier.GetHardwareIdentifier(); return new NodeLockedLicense(hardwareId); } @@ -71,4 +74,8 @@ public static ConcurrentLicense GenerateConcurrentLicense(string userName, int m { return new ConcurrentLicense(userName, maxActiveUsersCount); } + + + + internal static void SetHardwareIdentifier(IHardwareIdentifier hardwareIdentifier) => _hardwareIdentifier = hardwareIdentifier; } \ No newline at end of file diff --git a/src/Aegis/LicenseManager.cs b/src/Aegis/LicenseManager.cs index a92e5c2..2c37201 100644 --- a/src/Aegis/LicenseManager.cs +++ b/src/Aegis/LicenseManager.cs @@ -1,10 +1,13 @@ using System.Globalization; using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; using Aegis.Enums; using Aegis.Exceptions; +using Aegis.Interfaces; using Aegis.Models; +using Aegis.Serialization; using Aegis.Utilities; [assembly: InternalsVisibleTo("Aegis.Tests")] @@ -21,6 +24,8 @@ public static class LicenseManager private static readonly string DisconnectEndpoint = $"{_serverBaseEndpoint}/disconnect"; private static TimeSpan _heartbeatInterval = TimeSpan.FromMinutes(5); // Should be less than server timeout private static Timer? _heartbeatTimer; + private static ILicenseSerializer _serializer = new JsonLicenseSerializer(); + private static bool _builtInValidation = true; public static BaseLicense? Current { get; private set; } /// @@ -57,15 +62,45 @@ public static void SetHeartbeatInterval(TimeSpan heartbeatInterval) _heartbeatInterval = heartbeatInterval; } + /// + /// Sets the serializer for license serialization. + /// + /// The serializer that implements the interface. + /// Thrown if the serializer is null. + public static void SetSerializer(ILicenseSerializer serializer) + { + ArgumentNullException.ThrowIfNull(serializer); + _serializer = serializer; + LicenseValidator.SetSerializer(_serializer); + } + + /// + /// Sets the hardware identifier for license validation and generation. + /// + /// The hardware identifier that implements the interface. + /// Thrown if the identifier is null. + public static void SetHardwareIdentifier(IHardwareIdentifier identifier) + { + ArgumentNullException.ThrowIfNull(identifier); + LicenseValidator.SetHardwareIdentifier(identifier); + LicenseGenerator.SetHardwareIdentifier(identifier); + } + + /// + /// Sets whether to use built-in validation or not. + /// + /// Whether to use built-in validation or not. + public static void SetBuiltInValidation(bool value) => _builtInValidation = value; + /// /// Saves a license to a file. /// /// The type of license to save. /// The license object to save. /// The path to the file to save the license to. - /// The private key for signing. + /// The private key for signing. /// Thrown if the license or file path is null. - public static void SaveLicense(T license, string filePath, string? privateKey = null) where T : BaseLicense + public static void SaveLicenseToPath(T license, string filePath, string? secretKey = null) where T : BaseLicense { ArgumentNullException.ThrowIfNull(license); ArgumentNullException.ThrowIfNull(filePath); @@ -74,7 +109,7 @@ public static void SaveLicense(T license, string filePath, string? privateKey throw new ArgumentException("Invalid file path.", nameof(filePath)); // Save the combined data to the specified file - File.WriteAllBytes(filePath, SaveLicense(license, privateKey)); + File.WriteAllBytes(filePath, SaveLicense(license, secretKey)); } /// @@ -90,8 +125,7 @@ public static byte[] SaveLicense(T license, string? privateKey = null) where ArgumentNullException.ThrowIfNull(license); // Serialize the license object - var licenseData = - JsonSerializer.SerializeToUtf8Bytes(license, new JsonSerializerOptions { WriteIndented = true }); + var licenseData = Encoding.UTF8.GetBytes(_serializer.Serialize(license)); // Generate a unique AES secret key var aesKey = SecurityUtils.GenerateAesKey(); @@ -145,7 +179,7 @@ public static byte[] SaveLicense(T license, string? privateKey = null) where { if (!LicenseValidator.VerifyLicenseData(licenseData, out var license, true) || license == null) throw new InvalidLicenseFormatException("Invalid license file format."); - + // Set the current license based on type license = license.Type switch { @@ -185,7 +219,7 @@ public static void ThrowIfNotAllowed(string featureName) if (!IsFeatureEnabled(featureName)) throw new FeatureNotLicensedException($"Feature '{featureName}' is not allowed in your licensing model."); } - + /// /// Closes connection to the licensing server and releases any resources. /// @@ -205,7 +239,7 @@ public static async Task CloseAsync() } // Helper methods - + /// /// Validates the license asynchronously. /// @@ -269,24 +303,28 @@ private static async Task ValidateLicenseOnlineAsync(BaseLicense license, byte[] private static void ValidateLicenseOffline(BaseLicense license, byte[] licenseData, Dictionary? validationParams = null) { - var isLicenseValid = license.Type switch + var isLicenseValid = false; + if (_builtInValidation) { - LicenseType.Standard => LicenseValidator.ValidateStandardLicense(licenseData, - validationParams?["UserName"]!, validationParams?["SerialNumber"]!), - LicenseType.Trial => LicenseValidator.ValidateTrialLicense(licenseData), - LicenseType.NodeLocked => LicenseValidator.ValidateNodeLockedLicense(licenseData, - validationParams?["HardwareId"]), - LicenseType.Subscription => LicenseValidator.ValidateSubscriptionLicense(licenseData), - LicenseType.Floating => LicenseValidator.ValidateFloatingLicense(licenseData, - validationParams?["UserName"]!, int.Parse(validationParams?["MaxActiveUsersCount"]!)), - LicenseType.Concurrent => LicenseValidator.ValidateConcurrentLicense(licenseData, - validationParams?["UserName"]!, int.Parse(validationParams?["MaxActiveUsersCount"]!)), - _ => false - }; - - if (isLicenseValid) - isLicenseValid = LicenseValidator.ValidateLicenseRules(license, validationParams); + isLicenseValid = license.Type switch + { + LicenseType.Standard => LicenseValidator.ValidateStandardLicense(licenseData, + validationParams?["UserName"]!, validationParams?["SerialNumber"]!), + LicenseType.Trial => LicenseValidator.ValidateTrialLicense(licenseData), + LicenseType.NodeLocked => LicenseValidator.ValidateNodeLockedLicense(licenseData, + validationParams?["HardwareId"]), + LicenseType.Subscription => LicenseValidator.ValidateSubscriptionLicense(licenseData), + LicenseType.Floating => LicenseValidator.ValidateFloatingLicense(licenseData, + validationParams?["UserName"]!, int.Parse(validationParams?["MaxActiveUsersCount"]!)), + LicenseType.Concurrent => LicenseValidator.ValidateConcurrentLicense(licenseData, + validationParams?["UserName"]!, int.Parse(validationParams?["MaxActiveUsersCount"]!)), + _ => false + }; + } + if (isLicenseValid || !_builtInValidation) + isLicenseValid = LicenseValidator.ValidateLicenseRules(license, validationParams); + if (!isLicenseValid) throw new LicenseValidationException("License validation failed."); } @@ -365,7 +403,7 @@ private static async Task SendDisconnectAsync() throw new HeartbeatException( $"Concurrent user disconnect failed: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}"); } - + private static byte[] CombineLicenseData(byte[] hash, byte[] signature, byte[] encryptedData, byte[] aesKey) { var combinedData = new byte[ diff --git a/src/Aegis/LicenseValidator.cs b/src/Aegis/LicenseValidator.cs index 96a234f..9c3c6d6 100644 --- a/src/Aegis/LicenseValidator.cs +++ b/src/Aegis/LicenseValidator.cs @@ -1,7 +1,9 @@ -using System.Text.Json; +using System.Text; +using System.Text.Json; using Aegis.Exceptions; using Aegis.Interfaces; using Aegis.Models; +using Aegis.Serialization; using Aegis.Utilities; namespace Aegis; @@ -10,6 +12,19 @@ public static class LicenseValidator { private static readonly List ValidationRules = []; private static readonly Dictionary ValidationRuleGroups = new(); + private static ILicenseSerializer _serializer = new JsonLicenseSerializer(); + private static IHardwareIdentifier _hardwareIdentifier = new DefaultHardwareIdentifier(); + + internal static void SetSerializer(ILicenseSerializer serializer) + { + ArgumentNullException.ThrowIfNull(serializer); + _serializer = serializer; + } + + internal static void SetHardwareIdentifier(IHardwareIdentifier hardwareIdentifier) + { + _hardwareIdentifier = hardwareIdentifier; + } public static void AddValidationRule(IValidationRule rule) { @@ -86,7 +101,7 @@ public static bool ValidateNodeLockedLicense(byte[] licenseData, string? hardwar return licenseObj is NodeLockedLicense license && (!license.ExpirationDate.HasValue || !(license.ExpirationDate < DateTime.UtcNow)) && - HardwareUtils.ValidateHardwareId(hardwareId ?? license.HardwareId); + _hardwareIdentifier.ValidateHardwareIdentifier(hardwareId ?? license.HardwareId); } /// @@ -177,7 +192,7 @@ internal static bool VerifyLicenseData(byte[] licenseData, out BaseLicense? lice var decryptedData = SecurityUtils.DecryptData(encryptedData, aesKey); // Deserialize the license object - license = JsonSerializer.Deserialize(decryptedData); + license = _serializer.Deserialize(Encoding.UTF8.GetString(decryptedData)); return license != null; } diff --git a/src/Aegis/Serialization/Converters/JsonLicenseConverter.cs b/src/Aegis/Serialization/Converters/JsonLicenseConverter.cs new file mode 100644 index 0000000..a7c191b --- /dev/null +++ b/src/Aegis/Serialization/Converters/JsonLicenseConverter.cs @@ -0,0 +1,43 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Aegis.Enums; +using Aegis.Exceptions; +using Aegis.Models; + +namespace Aegis.Serialization.Converters; + +public class JsonLicenseConverter : JsonConverter +{ + public override BaseLicense Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Read the license type discriminator + Utf8JsonReader readerClone = reader; + if (!readerClone.Read() || readerClone.TokenType != JsonTokenType.StartObject) + throw new JsonException("Expected start of object."); + + if(!readerClone.Read() || readerClone.TokenType != JsonTokenType.PropertyName || readerClone.GetString() != "Type") + throw new JsonException("Expected Type property."); + + readerClone.Read(); + + var licenseType = (LicenseType)Enum.Parse(typeof(LicenseType), readerClone.GetString()!); + + // Deserialize based on the discriminator + return licenseType switch + { + LicenseType.Standard => JsonSerializer.Deserialize(ref reader, options)!, + LicenseType.Trial => JsonSerializer.Deserialize(ref reader, options)!, + LicenseType.NodeLocked => JsonSerializer.Deserialize(ref reader, options)!, + LicenseType.Subscription => JsonSerializer.Deserialize(ref reader, options)!, + LicenseType.Floating => JsonSerializer.Deserialize(ref reader, options)!, + LicenseType.Concurrent => JsonSerializer.Deserialize(ref reader, options)!, + _ => throw new InvalidLicenseFormatException($"Unknown license type: {licenseType}") + }; + } + + + public override void Write(Utf8JsonWriter writer, BaseLicense value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +} \ No newline at end of file diff --git a/src/Aegis/Serialization/JsonLicenseSerializer.cs b/src/Aegis/Serialization/JsonLicenseSerializer.cs new file mode 100644 index 0000000..563cb6f --- /dev/null +++ b/src/Aegis/Serialization/JsonLicenseSerializer.cs @@ -0,0 +1,32 @@ +using System.Text.Json; +using Aegis.Exceptions; +using Aegis.Interfaces; +using Aegis.Models; + +namespace Aegis.Serialization; + +public class JsonLicenseSerializer(JsonSerializerOptions? options = null) : ILicenseSerializer +{ + private readonly JsonSerializerOptions _options = options ?? new JsonSerializerOptions + { + WriteIndented = true, + }; + + public string Serialize(BaseLicense license) + { + return JsonSerializer.Serialize(license, _options); + } + + public BaseLicense? Deserialize(string data) + { + ArgumentNullException.ThrowIfNull(data); + try + { + return JsonSerializer.Deserialize(data, _options); + } + catch (JsonException ex) + { + throw new InvalidLicenseFormatException("Invalid license format.", ex); + } + } +} \ No newline at end of file diff --git a/src/Aegis/Utilities/DefaultHardwareIdentifier.cs b/src/Aegis/Utilities/DefaultHardwareIdentifier.cs new file mode 100644 index 0000000..03756c3 --- /dev/null +++ b/src/Aegis/Utilities/DefaultHardwareIdentifier.cs @@ -0,0 +1,47 @@ +using System.Net.NetworkInformation; +using Aegis.Interfaces; + +namespace Aegis.Utilities; + +public class DefaultHardwareIdentifier : IHardwareIdentifier +{ + /// + /// Gets a unique hardware identifier for the current machine. + /// + /// A string representing the hardware identifier. + public string GetHardwareIdentifier() + { + return $"{Environment.MachineName}-{Environment.UserName}-{Environment.OSVersion}-{GetMacAddress()}"; + } + + /// + /// Validates a hardware identifier against the current machine's hardware identifier. + /// + /// The hardware identifier to validate. + /// True if the hardware identifier matches, false otherwise. + public bool ValidateHardwareIdentifier(string hardwareId) + { + return GetHardwareIdentifier() == hardwareId; + } + + private static string GetMacAddress() + { + var values = NetworkInterface.GetAllNetworkInterfaces() + .Where(x => (x.NetworkInterfaceType != NetworkInterfaceType.Wireless80211) && x.Name != "docker0") + .Select(x => x.GetPhysicalAddress().ToString()) + .Where(x => x != "000000000000") + .Select(x => + { + if (x.Length != 12 && x.Length != 16) + return x; + + var parts = Enumerable.Range(0, x.Length / 2).Select(i => x.Substring(i * 2, 2)); + return string.Join(":", parts.ToArray()); + }) + .ToArray(); + + return values.Length > 0 + ? string.Join(",", values.ToArray()) + : "000000000000"; + } +} \ No newline at end of file diff --git a/src/Aegis/Utilities/HardwareUtils.cs b/src/Aegis/Utilities/HardwareUtils.cs deleted file mode 100644 index 8f7922d..0000000 --- a/src/Aegis/Utilities/HardwareUtils.cs +++ /dev/null @@ -1,30 +0,0 @@ -using DeviceId; - -namespace Aegis.Utilities; - -public static class HardwareUtils -{ - /// - /// Gets a unique hardware identifier for the current machine. - /// - /// A string representing the hardware identifier. - public static string GetHardwareId() - { - return new DeviceIdBuilder() - .AddMachineName() - .AddUserName() - .AddOsVersion() - .AddMacAddress(true, true) - .ToString(); - } - - /// - /// Validates a hardware identifier against the current machine's hardware identifier. - /// - /// The hardware identifier to validate. - /// True if the hardware identifier matches, false otherwise. - public static bool ValidateHardwareId(string hardwareId) - { - return GetHardwareId() == hardwareId; - } -} \ No newline at end of file diff --git a/tests/Aegis.Tests/LicenseManagerTests.cs b/tests/Aegis.Tests/LicenseManagerTests.cs index a06212b..8532075 100644 --- a/tests/Aegis.Tests/LicenseManagerTests.cs +++ b/tests/Aegis.Tests/LicenseManagerTests.cs @@ -40,7 +40,7 @@ public void SaveLicense_SavesLicenseToFileCorrectly() var filePath = Path.GetTempFileName(); // Use a temporary file // Act - LicenseManager.SaveLicense(license, filePath); + LicenseManager.SaveLicenseToPath(license, filePath); // Assert Assert.True(File.Exists(filePath)); @@ -69,7 +69,7 @@ public void SaveLicense_ThrowsExceptionForNullFilePath() var license = GenerateLicense(); // Act & Assert - Assert.Throws(() => LicenseManager.SaveLicense(license, null!)); + Assert.Throws(() => LicenseManager.SaveLicenseToPath(license, null!)); } [Fact] @@ -80,7 +80,7 @@ public void SaveLicense_ThrowsExceptionForEmptyFilePath() var license = GenerateLicense(); // Act & Assert - Assert.Throws(() => LicenseManager.SaveLicense(license, "")); + Assert.Throws(() => LicenseManager.SaveLicenseToPath(license, "")); } [Fact] @@ -92,7 +92,7 @@ public void SaveLicense_ThrowsExceptionForInvalidFilePath() const string filePath = "Invalid/File/Path"; // This should be an invalid path on most systems // Act & Assert - Assert.Throws(() => LicenseManager.SaveLicense(license, filePath)); + Assert.Throws(() => LicenseManager.SaveLicenseToPath(license, filePath)); } [Fact] @@ -102,7 +102,7 @@ public async Task LoadLicenseAsync_LoadsLicenseFromFileCorrectly() LoadSecretKeys(); var license = GenerateLicense(); var filePath = Path.GetTempFileName(); - LicenseManager.SaveLicense(license, filePath); + LicenseManager.SaveLicenseToPath(license, filePath); // Act var loadedLicense = await LicenseManager.LoadLicenseAsync(filePath); diff --git a/tests/Aegis.Tests/LicenseValidatorTests.cs b/tests/Aegis.Tests/LicenseValidatorTests.cs index 240d8ec..0ec73bf 100644 --- a/tests/Aegis.Tests/LicenseValidatorTests.cs +++ b/tests/Aegis.Tests/LicenseValidatorTests.cs @@ -157,7 +157,7 @@ public void ValidateNodeLockedLicense_ReturnsTrue_ForValidLicense() { // Arrange LoadSecretKeys(); - var hardwareId = HardwareUtils.GetHardwareId(); + var hardwareId = new DefaultHardwareIdentifier().GetHardwareIdentifier(); var license = GenerateNodeLockedLicense(hardwareId); var licenseData = LicenseManager.SaveLicense(license);