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 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(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); }