Files
LMEMappeCatastaliApp/Services/ManualLicenseService.cs

227 lines
8.3 KiB
C#

using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace LMEMappeCatastaliApp.Services;
public sealed class ManualLicenseService
{
private const string SecureStorageKey = "lme_manual_license_token";
private const string LastObservedUtcPreference = "lme_license_last_observed_utc";
private const string ProductName = "lme-pro-manual";
private const string EncryptionKeyBase64 = "GiTTRSc1y4DseejPaA0NiXgy95DlgEsIe3JKdJ70sVQ=";
private const string PublicKeyPem = """
-----BEGIN PUBLIC KEY-----
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAmjh4E1fpaaonATNDipsR
1iy5cNSStD7fgB7i5xJTSZV36Td1B7hQXfZB8Z7vx9iFJpbCQMePuFiDKNbTUQFf
EKEp8lBq7SHo0LvbKNOSK9RMkqpb3ova9Er14gSCgshivf1c774VXTlt9J7uAWoE
QeQMNKJUSogoMkxEKc4Xmye65lGjJVeiuqdDCJh5o42j8DfGwqOyGX4808vSeixe
3lArRmoq9SGNO05VVunF8NKJyhiRLfropUvC+eFsiJMp3VMEYVn5FUcd8ljO67jv
pnngWh6zCZxs+h0mT0YRU9F3tWHqqsG1PE6NZnspmBdNfUfb9LxKt1RshEhUWiBx
0vvdUtg20nESOvFnieIX64pcx0LqFlDMmEWDMQjZ7KEtwXVphN1mhsryUiS6Cz3b
oKTryH4FQY9iIWNNzdzXujrV5ycE49orFEtrXRPnXBlB97Hl3Dlxi19lvPwxWXQ5
fKXrTJ2rL/WvlMpZ5YOggAmv7P7tA4B+Oi5uMiHzXEDlAgMBAAE=
-----END PUBLIC KEY-----
""";
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
private string? _token;
public ManualLicenseInfo? Current { get; private set; }
public string? LastError { get; private set; }
public bool HasActiveLicense => Current is { } license && license.ExpiresAtUtc > GetEffectiveUtcNow();
public async Task LoadAsync()
{
try
{
_token = await SecureStorage.Default.GetAsync(SecureStorageKey);
}
catch
{
_token = null;
}
ValidateStoredToken();
}
public async Task<ManualLicenseActivationResult> ActivateAsync(string? token)
{
var normalized = token?.Trim() ?? string.Empty;
var result = Validate(normalized);
if (!result.IsValid || result.License is null)
{
LastError = result.Error;
return result;
}
try
{
await SecureStorage.Default.SetAsync(SecureStorageKey, normalized);
}
catch
{
return ManualLicenseActivationResult.Failure("Impossibile salvare la licenza nell'archivio sicuro del dispositivo.");
}
_token = normalized;
Current = result.License;
LastError = null;
ObserveUtcNow();
return result;
}
public Task RemoveAsync()
{
SecureStorage.Default.Remove(SecureStorageKey);
_token = null;
Current = null;
LastError = null;
return Task.CompletedTask;
}
private void ValidateStoredToken()
{
if (string.IsNullOrWhiteSpace(_token))
{
Current = null;
LastError = null;
return;
}
var result = Validate(_token);
Current = result.License;
LastError = result.Error;
ObserveUtcNow();
}
private static ManualLicenseActivationResult Validate(string token)
{
try
{
var parts = token.Split('.');
if (parts.Length != 3 || parts[0] != "LME1")
{
return ManualLicenseActivationResult.Failure("Formato della licenza non riconosciuto.");
}
var envelope = Base64UrlDecode(parts[1]);
var signature = Base64UrlDecode(parts[2]);
using var rsa = RSA.Create();
rsa.ImportFromPem(PublicKeyPem);
if (!rsa.VerifyData(envelope, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1))
{
return ManualLicenseActivationResult.Failure("Firma della licenza non valida.");
}
var payloadBytes = DecryptEnvelope(envelope);
var payload = JsonSerializer.Deserialize<ManualLicensePayload>(payloadBytes, JsonOptions);
if (payload is null
|| payload.Version != 1
|| payload.Product != ProductName
|| !Guid.TryParse(payload.LicenseId, out _)
|| string.IsNullOrWhiteSpace(payload.FirstName)
|| string.IsNullOrWhiteSpace(payload.LastName)
|| string.IsNullOrWhiteSpace(payload.Email)
|| payload.ExpiresAtUtc <= payload.IssuedAtUtc)
{
return ManualLicenseActivationResult.Failure("Contenuto della licenza non valido.");
}
var license = new ManualLicenseInfo(
payload.LicenseId,
payload.FirstName.Trim(),
payload.LastName.Trim(),
payload.Email.Trim(),
payload.IssuedAtUtc,
payload.ExpiresAtUtc);
return license.ExpiresAtUtc <= GetEffectiveUtcNow()
? ManualLicenseActivationResult.Expired(license)
: ManualLicenseActivationResult.Success(license);
}
catch (Exception error) when (error is FormatException or CryptographicException or JsonException or ArgumentException)
{
return ManualLicenseActivationResult.Failure("Licenza danneggiata o non valida.");
}
}
private static byte[] DecryptEnvelope(byte[] envelope)
{
const int nonceLength = 12;
const int tagLength = 16;
if (envelope.Length <= nonceLength + tagLength)
{
throw new CryptographicException("Invalid license envelope.");
}
var nonce = envelope.AsSpan(0, nonceLength);
var tag = envelope.AsSpan(nonceLength, tagLength);
var ciphertext = envelope.AsSpan(nonceLength + tagLength);
var plaintext = new byte[ciphertext.Length];
using var aes = new AesGcm(Convert.FromBase64String(EncryptionKeyBase64), tagLength);
aes.Decrypt(nonce, ciphertext, tag, plaintext);
return plaintext;
}
private static byte[] Base64UrlDecode(string value)
{
var padded = value.Replace('-', '+').Replace('_', '/');
padded = padded.PadRight(padded.Length + (4 - padded.Length % 4) % 4, '=');
return Convert.FromBase64String(padded);
}
private static DateTimeOffset GetEffectiveUtcNow()
{
var now = DateTimeOffset.UtcNow;
var stored = Preferences.Default.Get(LastObservedUtcPreference, string.Empty);
var effective = DateTimeOffset.TryParse(stored, out var lastObserved) && lastObserved > now ? lastObserved : now;
if (!DateTimeOffset.TryParse(stored, out lastObserved) || effective > lastObserved)
{
Preferences.Default.Set(LastObservedUtcPreference, effective.ToString("O"));
}
return effective;
}
private static void ObserveUtcNow()
{
var now = DateTimeOffset.UtcNow;
var effective = GetEffectiveUtcNow();
Preferences.Default.Set(LastObservedUtcPreference, (effective > now ? effective : now).ToString("O"));
}
private sealed record ManualLicensePayload(
[property: JsonPropertyName("version")] int Version,
[property: JsonPropertyName("product")] string Product,
[property: JsonPropertyName("licenseId")] string LicenseId,
[property: JsonPropertyName("firstName")] string FirstName,
[property: JsonPropertyName("lastName")] string LastName,
[property: JsonPropertyName("email")] string Email,
[property: JsonPropertyName("issuedAtUtc")] DateTimeOffset IssuedAtUtc,
[property: JsonPropertyName("expiresAtUtc")] DateTimeOffset ExpiresAtUtc);
}
public sealed record ManualLicenseInfo(
string LicenseId,
string FirstName,
string LastName,
string Email,
DateTimeOffset IssuedAtUtc,
DateTimeOffset ExpiresAtUtc)
{
public string HolderName => $"{FirstName} {LastName}".Trim();
}
public sealed record ManualLicenseActivationResult(bool IsValid, bool IsExpired, ManualLicenseInfo? License, string? Error)
{
public static ManualLicenseActivationResult Success(ManualLicenseInfo license) => new(true, false, license, null);
public static ManualLicenseActivationResult Expired(ManualLicenseInfo license) => new(false, true, license, "La licenza è scaduta.");
public static ManualLicenseActivationResult Failure(string error) => new(false, false, null, error);
}