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
}