Langages
Intégration C# / .NET
SDK .NET 8+ sans dépendance : signatures vérifiées, retries, heartbeat.
SDK officiel
Projet Keyline.Sdk (dossier sdks/csharp du dépôt), .NET 8+, aucune dépendance externe. Ajoutez-le en référence de projet ou générez le paquet avec dotnet pack.
csharp
using Keyline;
using var client = new LicenseClient(new LicenseClientOptions
{
ApiUrl = new Uri("https://keyline.example.com/api/v1"),
ApplicationId = "<application_id>",
PublicKeys = new Dictionary<string, string> { ["<kid>"] = PublicKeyPem },
AppVersion = "1.4.2",
});
var result = await client.ActivateAsync(licenseKey);
if (result.Success)
{
// Conserver « heartbeat » jusqu'à la fermeture du logiciel : Dispose() arrête la boucle.
var heartbeat = client.StartHeartbeat(onFailure: r => LockPremiumFeatures(r.Error!.Code));
}
else
{
Console.WriteLine($"Refus : {result.Error!.Code}");
}Sans SDK : le protocole avec HttpClient
csharp
// KeylineClient.cs — .NET 8+
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
public sealed class LicenseException(string code, string message) : Exception(message)
{
public string Code { get; } = code;
}
public sealed class KeylineClient(HttpClient http, string applicationId, IReadOnlyDictionary<string, string> publicKeysPem)
{
// http.BaseAddress = new Uri("https://keyline.example.com/api/v1/"); http.Timeout = TimeSpan.FromSeconds(10);
public Task<JsonElement> ActivateAsync(string licenseKey, string deviceId, string? appVersion = null, CancellationToken ct = default) =>
CallAsync("activate", new Dictionary<string, object?> { ["license_key"] = licenseKey, ["device_id"] = deviceId, ["app_version"] = appVersion }, ct);
public Task<JsonElement> HeartbeatAsync(string sessionToken, CancellationToken ct = default) =>
CallAsync("heartbeat", new Dictionary<string, object?> { ["session_token"] = sessionToken }, ct);
public Task<JsonElement> DeactivateAsync(string sessionToken, CancellationToken ct = default) =>
CallAsync("deactivate", new Dictionary<string, object?> { ["session_token"] = sessionToken }, ct);
private async Task<JsonElement> CallAsync(string endpoint, Dictionary<string, object?> payload, CancellationToken ct)
{
var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant();
// Les champs facultatifs absents ne sont pas envoyés (null est refusé par l'API).
foreach (var k in payload.Where(kv => kv.Value is null).Select(kv => kv.Key).ToList()) payload.Remove(k);
payload["application_id"] = applicationId;
payload["nonce"] = nonce;
for (var attempt = 0; ; attempt++)
{
HttpResponseMessage res;
try { res = await http.PostAsJsonAsync($"licenses/{endpoint}", payload, ct); }
catch (HttpRequestException) when (attempt < 2) { await Task.Delay(500 * (1 << attempt), ct); continue; }
using (res)
{
if ((int)res.StatusCode == 429 && attempt < 2)
{
await Task.Delay(res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1), ct);
continue;
}
var body = await res.Content.ReadAsByteArrayAsync(ct);
Verify(res, body);
using var doc = JsonDocument.Parse(body);
var root = doc.RootElement.Clone();
if (!root.TryGetProperty("nonce", out var n) || n.GetString() != nonce) throw new LicenseException("REPLAY", "Nonce inattendu");
if (!root.GetProperty("success").GetBoolean())
{
var e = root.GetProperty("error");
throw new LicenseException(e.GetProperty("code").GetString()!, e.GetProperty("message").GetString()!);
}
return root;
}
}
}
/// <summary>ECDSA P-256 / SHA-256 (IEEE P1363) sur "<ts>.<corps>".</summary>
private void Verify(HttpResponseMessage res, byte[] body)
{
if (!res.Headers.TryGetValues("Keyline-Signature", out var values)) throw new LicenseException("UNSIGNED", "Réponse non signée");
var parts = values.First().Split(',').Select(p => p.Split('=', 2)).ToDictionary(p => p[0], p => p[1]);
if (!publicKeysPem.TryGetValue(parts["kid"], out var pem)) throw new LicenseException("UNKNOWN_KEY", "Clé de signature inconnue");
using var ecdsa = ECDsa.Create();
ecdsa.ImportFromPem(pem);
var message = Encoding.UTF8.GetBytes(parts["ts"] + ".").Concat(body).ToArray();
// VerifyData utilise par défaut le format IEEE P1363 (r || s), celui de Keyline.
if (!ecdsa.VerifyData(message, Base64UrlDecode(parts["sig"]), HashAlgorithmName.SHA256))
throw new LicenseException("BAD_SIGNATURE", "Signature invalide");
}
private static byte[] Base64UrlDecode(string s)
{
s = s.Replace('-', '+').Replace('_', '/');
return Convert.FromBase64String(s.PadRight(s.Length + (4 - s.Length % 4) % 4, '='));
}
}Identifiant d’appareil
csharp
using System.Security.Cryptography;
using System.Text;
static string DeviceId(string salt = "mon-logiciel")
{
string? raw = null;
if (OperatingSystem.IsWindows())
raw = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Cryptography")?.GetValue("MachineGuid") as string;
else if (OperatingSystem.IsLinux() && File.Exists("/etc/machine-id"))
raw = File.ReadAllText("/etc/machine-id").Trim();
raw ??= Environment.MachineName;
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes($"{salt}:{raw}"))).ToLowerInvariant();
}Utilisation
csharp
var http = new HttpClient { BaseAddress = new Uri("https://keyline.example.com/api/v1/"), Timeout = TimeSpan.FromSeconds(10) };
var client = new KeylineClient(http, "<application_id>", new Dictionary<string, string> { ["<kid>"] = PublicKeyPem });
try
{
var r = await client.ActivateAsync(licenseKey, DeviceId(), "1.4.2");
var token = r.GetProperty("session").GetProperty("token").GetString();
}
catch (LicenseException e) when (e.Code == "DEVICE_LIMIT_REACHED")
{
// inviter l'utilisateur à libérer un appareil
}