using System.Security.Cryptography; using System.Text.Json; using App.Core.Interfaces.Licensing; using App.Core.Services.Licensing; namespace App.Infrastructure.Licensing; /// /// Stocke l'état de l'essai gratuit chiffré par DPAPI dans un fichier séparé de la base SQLite /// (voir docs/DATABASE_SCHEMA.md §Licence) : supprimer/remplacer app.db ne doit jamais /// réinitialiser l'essai. /// /// /// Scope plutôt que CurrentUser : la /// protection est liée au POSTE, pas au compte Windows qui exécute l'app — cohérent avec le /// modèle de licence "à vie, liée au poste". Contrepartie assumée : sur un poste multi-comptes, /// n'importe quel compte Windows local peut déchiffrer ce fichier (mais pas le copier utilement /// sur un autre poste, ce qui est le seul contournement qu'on cherche à bloquer ici). /// public class DpapiTrialStateStore(string filePath) : ITrialStateStore { public TrialState? Load() { if (!File.Exists(filePath)) { return null; } try { var protectedBytes = File.ReadAllBytes(filePath); var jsonBytes = ProtectedData.Unprotect(protectedBytes, optionalEntropy: null, DataProtectionScope.LocalMachine); return JsonSerializer.Deserialize(jsonBytes); } catch (Exception ex) when (ex is CryptographicException or JsonException) { // Fichier corrompu ou déplacé depuis un autre poste (DPAPI ne peut pas le déchiffrer) : // traité comme "pas d'essai enregistré" -> LicenseService en recréera un. Sans risque // d'abus significatif : au pire ça redonne un essai, ça ne contourne pas une licence // payante déjà activée (qui, elle, est vérifiée par signature Ed25519 indépendamment). return null; } } public void Save(TrialState state) { var directory = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directory)) { Directory.CreateDirectory(directory); } var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(state); var protectedBytes = ProtectedData.Protect(jsonBytes, optionalEntropy: null, DataProtectionScope.LocalMachine); File.WriteAllBytes(filePath, protectedBytes); } }