first commit
This commit is contained in:
57
src/App.Infrastructure/Licensing/DpapiTrialStateStore.cs
Normal file
57
src/App.Infrastructure/Licensing/DpapiTrialStateStore.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using App.Core.Interfaces.Licensing;
|
||||
using App.Core.Services.Licensing;
|
||||
|
||||
namespace App.Infrastructure.Licensing;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Scope <see cref="DataProtectionScope.LocalMachine"/> plutôt que <c>CurrentUser</c> : 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).
|
||||
/// </remarks>
|
||||
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<TrialState>(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);
|
||||
}
|
||||
}
|
||||
41
src/App.Infrastructure/Licensing/LicenseFileRepository.cs
Normal file
41
src/App.Infrastructure/Licensing/LicenseFileRepository.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Text.Json;
|
||||
using App.Core.Interfaces.Licensing;
|
||||
using App.Core.Services.Licensing;
|
||||
|
||||
namespace App.Infrastructure.Licensing;
|
||||
|
||||
/// <summary>
|
||||
/// Lit/écrit le fichier de licence (.lic) activée dans le dossier sandboxé de l'application.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="LoadInstalled"/> ne retourne <c>null</c> QUE si aucun fichier n'existe. Si le
|
||||
/// fichier existe mais est illisible/corrompu (JSON invalide), une <see cref="JsonException"/>
|
||||
/// est volontairement laissée remonter plutôt que d'être avalée : un fichier corrompu doit être
|
||||
/// traité comme une licence invalide (écran d'activation), jamais comme "pas de licence" — sinon
|
||||
/// il suffirait de corrompre le fichier pour relancer un essai gratuit. Voir <c>LicenseService</c>.
|
||||
/// </remarks>
|
||||
public class LicenseFileRepository(string filePath) : ILicenseFileRepository
|
||||
{
|
||||
public LicenseFile? LoadInstalled()
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(filePath);
|
||||
return JsonSerializer.Deserialize<LicenseFile>(json, LicenseJsonOptions.Default);
|
||||
}
|
||||
|
||||
public void Install(LicenseFile license)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(license, LicenseJsonOptions.Default);
|
||||
File.WriteAllText(filePath, json);
|
||||
}
|
||||
}
|
||||
41
src/App.Infrastructure/Licensing/WmiHardwareIdProvider.cs
Normal file
41
src/App.Infrastructure/Licensing/WmiHardwareIdProvider.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Management;
|
||||
using App.Core.Interfaces.Licensing;
|
||||
using App.Core.Services.Licensing;
|
||||
|
||||
namespace App.Infrastructure.Licensing;
|
||||
|
||||
/// <summary>
|
||||
/// Lit l'UUID de la carte mère et le numéro de série du disque système via WMI, tels que
|
||||
/// requis par le format de licence (voir docs/DATABASE_SCHEMA.md §Licence).
|
||||
/// </summary>
|
||||
public class WmiHardwareIdProvider : IHardwareIdProvider
|
||||
{
|
||||
public HardwareId GetCurrent()
|
||||
{
|
||||
var motherboardUuid = QuerySingleProperty("SELECT UUID FROM Win32_ComputerSystemProduct", "UUID")
|
||||
?? throw new InvalidOperationException("Impossible de lire Win32_ComputerSystemProduct.UUID via WMI.");
|
||||
|
||||
// Win32_DiskDrive.Index=0 : le disque système (celui sur lequel Windows est installé),
|
||||
// conformément au cahier des charges ("disque système index 0").
|
||||
var diskSerial = QuerySingleProperty("SELECT SerialNumber FROM Win32_DiskDrive WHERE Index = 0", "SerialNumber")
|
||||
?? throw new InvalidOperationException("Impossible de lire Win32_DiskDrive (Index=0).SerialNumber via WMI.");
|
||||
|
||||
return new HardwareId(motherboardUuid.Trim(), diskSerial.Trim());
|
||||
}
|
||||
|
||||
private static string? QuerySingleProperty(string query, string propertyName)
|
||||
{
|
||||
using var searcher = new ManagementObjectSearcher(query);
|
||||
using var results = searcher.Get();
|
||||
|
||||
foreach (ManagementBaseObject item in results)
|
||||
{
|
||||
using (item)
|
||||
{
|
||||
return item[propertyName]?.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user