using System.Security.Cryptography; using System.Text; using System.Text.Json; using IronLicensing.Client.Models; using Microsoft.Extensions.Options; namespace IronLicensing.Client; /// /// File system-based license cache for server/console applications. /// Uses DPAPI on Windows or file encryption on other platforms. /// 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 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 GetCachedLicenseAsync() { try { if (!File.Exists(_licenseFile)) return Task.FromResult(null); var encryptedData = File.ReadAllBytes(_licenseFile); var decryptedData = Unprotect(encryptedData); if (decryptedData == null) return Task.FromResult(null); var json = Encoding.UTF8.GetString(decryptedData); var license = JsonSerializer.Deserialize(json); if (license == null) return Task.FromResult(null); var signature = ReadProtectedString(_signatureFile); var publicKey = ReadProtectedString(_publicKeyFile); var rawJson = ReadProtectedString(_rawJsonFile); return Task.FromResult(new CachedLicenseData { License = license, Signature = signature, SigningPublicKey = publicKey, RawLicenseJson = rawJson }); } catch { return Task.FromResult(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 GetSigningPublicKeyAsync() { try { return Task.FromResult(ReadProtectedString(_publicKeyFile)); } catch { return Task.FromResult(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 }