Add net9.0 support for server/console applications
- Add net9.0 to TargetFrameworks for non-MAUI apps - Create FileSystemLicenseCache for file-based license storage - Create Platforms/Net9/MachineIdentifier using Environment APIs - Add conditional compilation for MAUI vs non-MAUI platforms - Update ServiceCollectionExtensions for conditional DI registration - Bump version to 1.2.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,9 +17,16 @@ public static class ServiceCollectionExtensions
|
|||||||
// Core services
|
// Core services
|
||||||
services.AddHttpClient<ILicenseManager, LicenseManager>();
|
services.AddHttpClient<ILicenseManager, LicenseManager>();
|
||||||
services.AddSingleton<IMachineIdentifier, MachineIdentifier>();
|
services.AddSingleton<IMachineIdentifier, MachineIdentifier>();
|
||||||
services.AddSingleton<ILicenseCache, SecureStorageLicenseCache>();
|
|
||||||
services.AddSingleton<ISignatureVerifier, RsaSignatureVerifier>();
|
services.AddSingleton<ISignatureVerifier, RsaSignatureVerifier>();
|
||||||
|
|
||||||
|
#if MAUI
|
||||||
|
// Use SecureStorage on MAUI platforms
|
||||||
|
services.AddSingleton<ILicenseCache, SecureStorageLicenseCache>();
|
||||||
|
#else
|
||||||
|
// Use file system cache on server/console apps
|
||||||
|
services.AddSingleton<ILicenseCache, FileSystemLicenseCache>();
|
||||||
|
#endif
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
241
FileSystemLicenseCache.cs
Normal file
241
FileSystemLicenseCache.cs
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using IronLicensing.Client.Models;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace IronLicensing.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// File system-based license cache for server/console applications.
|
||||||
|
/// Uses DPAPI on Windows or file encryption on other platforms.
|
||||||
|
/// </summary>
|
||||||
|
public class FileSystemLicenseCache : ILicenseCache
|
||||||
|
{
|
||||||
|
private readonly string _cacheDirectory;
|
||||||
|
private readonly string _licenseFile;
|
||||||
|
private readonly string _signatureFile;
|
||||||
|
private readonly string _publicKeyFile;
|
||||||
|
private readonly string _rawJsonFile;
|
||||||
|
private readonly byte[] _entropy;
|
||||||
|
|
||||||
|
public FileSystemLicenseCache(IOptions<LicensingOptions> options)
|
||||||
|
{
|
||||||
|
var productSlug = options.Value.ProductSlug ?? "ironlicensing";
|
||||||
|
|
||||||
|
// Use a product-specific directory in the user's local app data
|
||||||
|
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
_cacheDirectory = Path.Combine(appDataPath, "IronLicensing", productSlug);
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_cacheDirectory);
|
||||||
|
|
||||||
|
_licenseFile = Path.Combine(_cacheDirectory, "license.dat");
|
||||||
|
_signatureFile = Path.Combine(_cacheDirectory, "signature.dat");
|
||||||
|
_publicKeyFile = Path.Combine(_cacheDirectory, "pubkey.dat");
|
||||||
|
_rawJsonFile = Path.Combine(_cacheDirectory, "raw.dat");
|
||||||
|
|
||||||
|
// Use product slug as additional entropy for encryption
|
||||||
|
_entropy = Encoding.UTF8.GetBytes(productSlug + "_ironlicensing_cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CachedLicenseData?> GetCachedLicenseAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(_licenseFile))
|
||||||
|
return Task.FromResult<CachedLicenseData?>(null);
|
||||||
|
|
||||||
|
var encryptedData = File.ReadAllBytes(_licenseFile);
|
||||||
|
var decryptedData = Unprotect(encryptedData);
|
||||||
|
if (decryptedData == null)
|
||||||
|
return Task.FromResult<CachedLicenseData?>(null);
|
||||||
|
|
||||||
|
var json = Encoding.UTF8.GetString(decryptedData);
|
||||||
|
var license = JsonSerializer.Deserialize<LicenseInfo>(json);
|
||||||
|
if (license == null)
|
||||||
|
return Task.FromResult<CachedLicenseData?>(null);
|
||||||
|
|
||||||
|
var signature = ReadProtectedString(_signatureFile);
|
||||||
|
var publicKey = ReadProtectedString(_publicKeyFile);
|
||||||
|
var rawJson = ReadProtectedString(_rawJsonFile);
|
||||||
|
|
||||||
|
return Task.FromResult<CachedLicenseData?>(new CachedLicenseData
|
||||||
|
{
|
||||||
|
License = license,
|
||||||
|
Signature = signature,
|
||||||
|
SigningPublicKey = publicKey,
|
||||||
|
RawLicenseJson = rawJson
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Task.FromResult<CachedLicenseData?>(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SaveLicenseAsync(LicenseInfo license, string? signature, string? signingPublicKey)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(license);
|
||||||
|
WriteProtectedString(_licenseFile, json);
|
||||||
|
WriteProtectedString(_rawJsonFile, json);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(signature))
|
||||||
|
{
|
||||||
|
WriteProtectedString(_signatureFile, signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(signingPublicKey))
|
||||||
|
{
|
||||||
|
WriteProtectedString(_publicKeyFile, signingPublicKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore storage errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string?> GetSigningPublicKeyAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Task.FromResult(ReadProtectedString(_publicKeyFile));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Task.FromResult<string?>(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SaveSigningPublicKeyAsync(string publicKey)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
WriteProtectedString(_publicKeyFile, publicKey);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore storage errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ClearAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(_licenseFile)) File.Delete(_licenseFile);
|
||||||
|
if (File.Exists(_signatureFile)) File.Delete(_signatureFile);
|
||||||
|
if (File.Exists(_publicKeyFile)) File.Delete(_publicKeyFile);
|
||||||
|
if (File.Exists(_rawJsonFile)) File.Delete(_rawJsonFile);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore storage errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteProtectedString(string filePath, string value)
|
||||||
|
{
|
||||||
|
var data = Encoding.UTF8.GetBytes(value);
|
||||||
|
var protectedData = Protect(data);
|
||||||
|
File.WriteAllBytes(filePath, protectedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ReadProtectedString(string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var protectedData = File.ReadAllBytes(filePath);
|
||||||
|
var data = Unprotect(protectedData);
|
||||||
|
if (data == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] Protect(byte[] data)
|
||||||
|
{
|
||||||
|
#if WINDOWS
|
||||||
|
// Use DPAPI on Windows
|
||||||
|
return ProtectedData.Protect(data, _entropy, DataProtectionScope.CurrentUser);
|
||||||
|
#else
|
||||||
|
// On other platforms, use AES encryption with a machine-derived key
|
||||||
|
return EncryptWithDerivedKey(data);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[]? Unprotect(byte[] data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#if WINDOWS
|
||||||
|
return ProtectedData.Unprotect(data, _entropy, DataProtectionScope.CurrentUser);
|
||||||
|
#else
|
||||||
|
return DecryptWithDerivedKey(data);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !WINDOWS
|
||||||
|
private byte[] EncryptWithDerivedKey(byte[] data)
|
||||||
|
{
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = DeriveKey();
|
||||||
|
aes.GenerateIV();
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor();
|
||||||
|
var encrypted = encryptor.TransformFinalBlock(data, 0, data.Length);
|
||||||
|
|
||||||
|
// Prepend IV to encrypted data
|
||||||
|
var result = new byte[aes.IV.Length + encrypted.Length];
|
||||||
|
Array.Copy(aes.IV, 0, result, 0, aes.IV.Length);
|
||||||
|
Array.Copy(encrypted, 0, result, aes.IV.Length, encrypted.Length);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[]? DecryptWithDerivedKey(byte[] data)
|
||||||
|
{
|
||||||
|
if (data.Length < 16)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
using var aes = Aes.Create();
|
||||||
|
aes.Key = DeriveKey();
|
||||||
|
|
||||||
|
// Extract IV from data
|
||||||
|
var iv = new byte[16];
|
||||||
|
Array.Copy(data, 0, iv, 0, 16);
|
||||||
|
aes.IV = iv;
|
||||||
|
|
||||||
|
var encrypted = new byte[data.Length - 16];
|
||||||
|
Array.Copy(data, 16, encrypted, 0, encrypted.Length);
|
||||||
|
|
||||||
|
using var decryptor = aes.CreateDecryptor();
|
||||||
|
return decryptor.TransformFinalBlock(encrypted, 0, encrypted.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] DeriveKey()
|
||||||
|
{
|
||||||
|
// Derive a key from machine-specific information
|
||||||
|
var keyMaterial = Environment.MachineName + "|" +
|
||||||
|
Environment.UserName + "|" +
|
||||||
|
Convert.ToBase64String(_entropy);
|
||||||
|
|
||||||
|
using var sha256 = SHA256.Create();
|
||||||
|
return sha256.ComputeHash(Encoding.UTF8.GetBytes(keyMaterial));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -1,20 +1,29 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
|
<!-- Include net9.0 for server/console apps, plus MAUI platforms -->
|
||||||
|
<TargetFrameworks>net9.0;net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
|
||||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
|
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
|
||||||
<UseMaui>true</UseMaui>
|
|
||||||
|
<!-- Only use MAUI for platform-specific targets -->
|
||||||
|
<UseMaui Condition="$(TargetFramework.Contains('-'))">true</UseMaui>
|
||||||
<SingleProject>true</SingleProject>
|
<SingleProject>true</SingleProject>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<PackageOutputPath>C:\Users\logik\Dropbox\Nugets</PackageOutputPath>
|
<PackageOutputPath Condition="$([MSBuild]::IsOSPlatform('windows'))">C:\Users\logik\Dropbox\Nugets</PackageOutputPath>
|
||||||
|
<PackageOutputPath Condition="$([MSBuild]::IsOSPlatform('osx'))">$(HOME)/Library/CloudStorage/Dropbox/Nugets</PackageOutputPath>
|
||||||
|
|
||||||
|
<!-- Define MAUI constant for MAUI platforms -->
|
||||||
|
<DefineConstants Condition="$(TargetFramework.Contains('-'))">$(DefineConstants);MAUI</DefineConstants>
|
||||||
|
<!-- Define WINDOWS constant for Windows -->
|
||||||
|
<DefineConstants Condition="$(TargetFramework.Contains('windows'))">$(DefineConstants);WINDOWS</DefineConstants>
|
||||||
|
|
||||||
<!-- NuGet Package Properties -->
|
<!-- NuGet Package Properties -->
|
||||||
<PackageId>IronLicensing.Client</PackageId>
|
<PackageId>IronLicensing.Client</PackageId>
|
||||||
<Version>1.0.0</Version>
|
<Version>1.2.0</Version>
|
||||||
<Authors>David H Friedel Jr</Authors>
|
<Authors>David H Friedel Jr</Authors>
|
||||||
<Company>MarketAlly</Company>
|
<Company>MarketAlly</Company>
|
||||||
<Description>Client SDK for IronLicensing - Software Licensing Platform</Description>
|
<Description>Client SDK for IronLicensing - Software Licensing Platform. Supports .NET 9.0 server/console apps and MAUI mobile apps.</Description>
|
||||||
<PackageTags>licensing;software;activation;license-key</PackageTags>
|
<PackageTags>licensing;software;activation;license-key</PackageTags>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageIcon>nuget_il.png</PackageIcon>
|
<PackageIcon>nuget_il.png</PackageIcon>
|
||||||
@@ -27,9 +36,26 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
|
<!-- MAUI only for platform-specific targets -->
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" Condition="$(TargetFramework.Contains('-'))" />
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.*" />
|
||||||
|
<PackageReference Include="System.Text.Json" Version="9.0.*" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Exclude MAUI-specific files from net9.0 -->
|
||||||
|
<ItemGroup Condition="!$(TargetFramework.Contains('-'))">
|
||||||
|
<Compile Remove="SecureStorageLicenseCache.cs" />
|
||||||
|
<Compile Remove="Platforms\Android\**" />
|
||||||
|
<Compile Remove="Platforms\iOS\**" />
|
||||||
|
<Compile Remove="Platforms\MacCatalyst\**" />
|
||||||
|
<Compile Remove="Platforms\Windows\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Exclude net9.0-specific files from MAUI platforms -->
|
||||||
|
<ItemGroup Condition="$(TargetFramework.Contains('-'))">
|
||||||
|
<Compile Remove="FileSystemLicenseCache.cs" />
|
||||||
|
<Compile Remove="Platforms\Net9\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace IronLicensing.Client;
|
namespace IronLicensing.Client;
|
||||||
|
|
||||||
@@ -12,10 +13,17 @@ public partial class MachineIdentifier : IMachineIdentifier
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(combined))
|
if (string.IsNullOrEmpty(combined))
|
||||||
{
|
{
|
||||||
// Fallback to a device-specific identifier
|
#if MAUI
|
||||||
|
// Fallback to a device-specific identifier (MAUI platforms)
|
||||||
combined = DeviceInfo.Current.Idiom.ToString() + "|" +
|
combined = DeviceInfo.Current.Idiom.ToString() + "|" +
|
||||||
DeviceInfo.Current.Manufacturer + "|" +
|
DeviceInfo.Current.Manufacturer + "|" +
|
||||||
DeviceInfo.Current.Model;
|
DeviceInfo.Current.Model;
|
||||||
|
#else
|
||||||
|
// Fallback for server/console apps
|
||||||
|
combined = Environment.MachineName + "|" +
|
||||||
|
Environment.UserName + "|" +
|
||||||
|
RuntimeInformation.OSDescription;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
using var sha256 = SHA256.Create();
|
using var sha256 = SHA256.Create();
|
||||||
@@ -25,12 +33,26 @@ public partial class MachineIdentifier : IMachineIdentifier
|
|||||||
|
|
||||||
public string GetMachineName()
|
public string GetMachineName()
|
||||||
{
|
{
|
||||||
|
#if MAUI
|
||||||
return DeviceInfo.Current.Name ?? Environment.MachineName ?? "Unknown";
|
return DeviceInfo.Current.Name ?? Environment.MachineName ?? "Unknown";
|
||||||
|
#else
|
||||||
|
return Environment.MachineName ?? "Unknown";
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPlatform()
|
public string GetPlatform()
|
||||||
{
|
{
|
||||||
|
#if MAUI
|
||||||
return DeviceInfo.Current.Platform.ToString();
|
return DeviceInfo.Current.Platform.ToString();
|
||||||
|
#else
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
return "Windows";
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
return "macOS";
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
return "Linux";
|
||||||
|
return RuntimeInformation.OSDescription;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Platform-specific implementation
|
// Platform-specific implementation
|
||||||
|
|||||||
44
Platforms/Net9/MachineIdentifier.cs
Normal file
44
Platforms/Net9/MachineIdentifier.cs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace IronLicensing.Client;
|
||||||
|
|
||||||
|
public partial class MachineIdentifier
|
||||||
|
{
|
||||||
|
private partial List<string> GetMachineComponents()
|
||||||
|
{
|
||||||
|
var components = new List<string>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Machine name
|
||||||
|
var machineName = Environment.MachineName;
|
||||||
|
if (!string.IsNullOrEmpty(machineName))
|
||||||
|
{
|
||||||
|
components.Add(machineName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User name (for additional uniqueness)
|
||||||
|
var userName = Environment.UserName;
|
||||||
|
if (!string.IsNullOrEmpty(userName))
|
||||||
|
{
|
||||||
|
components.Add(userName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// OS description
|
||||||
|
components.Add(RuntimeInformation.OSDescription);
|
||||||
|
|
||||||
|
// Processor architecture
|
||||||
|
components.Add(RuntimeInformation.ProcessArchitecture.ToString());
|
||||||
|
|
||||||
|
// Runtime identifier
|
||||||
|
components.Add(RuntimeInformation.RuntimeIdentifier);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback - at minimum use machine name
|
||||||
|
components.Add(Environment.MachineName ?? "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
return components;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user