Licensing: 100-day offline cache with anti-rollback + cached server status

- CachedValidityDays bumped 7 → 100 days. Covers the "user goes online once
  every 3 months for updates" use case without repeatedly nagging for re-auth.
- LastValidationAt now uses response.ServerTime (signed Ed25519 by the server)
  rather than DateTime.UtcNow, so the client's local clock can't be used to
  forge a fresh validation date.
- LastSeenUtc monotonic counter, persisted on every launch as max(stored, now).
  Combined with a 1h grace window: if the user rolls their PC clock backward
  to extend the offline cache, the rollback is detected (now < LastSeen - 1h)
  and the cache is invalidated → forced re-validation next time online.
- CachedStatus persists the server-returned status (valid/expired/revoked/
  invalid/machine_limit_exceeded) so a revocation done while the user is
  offline still shows correctly when they next launch with cached data.
  Auto-override to "expired" if entitlement date has passed (handles the
  valid → expired transition without needing a server round-trip).
- ILicenseService.GetDecryptedKey() exposed for the new auto-revalidation flow.

This puts the trust boundary in the right place: cached data is informational
offline, the actual download authorization always re-validates against the
server (the gate to download is /api/download-url/, which checks the live
license state on each call). User can't fake offline-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 19:15:49 +02:00
parent 9cea07d6be
commit 07d58a8854
3 changed files with 136 additions and 8 deletions

View File

@@ -13,13 +13,53 @@ public sealed class LocalConfig
/// </summary>
public string Language { get; set; } = "auto";
/// <summary>
/// Nombre de connexions HTTP parallèles utilisées pour télécharger un build.
/// 1 = comportement legacy single-connection. 4-8 contourne le throttling
/// per-connection d'Apache/OVH mutualisé. 8 est un bon défaut.
/// </summary>
public int ParallelDownloadSegments { get; set; } = 8;
/// <summary>
/// Si false, ignore la vérification SHA-256 même si le manifest l'exige.
/// Utile sur les machines lentes ou pour gagner 30 s-3 min en fin de DL d'un gros build.
/// La sécurité repose alors uniquement sur la signature Ed25519 du manifest + HTTPS,
/// ce qui reste robuste.
/// </summary>
public bool VerifyDownloadHash { get; set; } = true;
public LicenseConfig License { get; set; } = new();
}
public sealed class LicenseConfig
{
public string? EncryptedKey { get; set; }
/// <summary>
/// Date UTC de la dernière validation online réussie. Utilise <c>response.ServerTime</c>
/// (signé Ed25519) plutôt que l'horloge locale, donc non manipulable par l'utilisateur.
/// Sert de point de départ au calcul d'expiration du cache offline.
/// </summary>
public DateTime? LastValidationAt { get; set; }
public DateTime? CachedEntitlementUntil { get; set; }
public string? CachedOwnerName { get; set; }
/// <summary>
/// Statut tel que renvoyé par le serveur lors de la dernière validation : "valid",
/// "expired", "revoked", "invalid", "machine_limit_exceeded". Persisté pour qu'une
/// révocation effectuée pendant que le user est offline soit toujours visible
/// même après son expiration de cache local. Le calcul "valid → expired" basé
/// sur la date est aussi appliqué côté lecture (sanity) si le temps a passé.
/// </summary>
public string? CachedStatus { get; set; }
/// <summary>
/// Compteur monotone : date UTC la plus récente jamais observée par le launcher
/// sur cette machine. Mise à jour à chaque démarrage si <c>now</c> est postérieur.
/// Sert d'anti-rollback : si l'utilisateur recule l'horloge PC, on détecte
/// (now &lt; LastSeenUtc - grace) et on invalide le cache → forçage d'une
/// re-validation online la prochaine fois qu'il sera connecté.
/// </summary>
public DateTime? LastSeenUtc { get; set; }
}