// Authsy SDK for C# / .NET (net6.0+). // // var client = new Authsy.AuthsyClient(new Authsy.Options { // BaseUrl = "https://YOUR-HOST/api/v1", // ApiKey = "ak_live_xxx", // SigningSecret = "…" // portal → Settings; verifies responses // }); // await client.ActivateAsync(licenseKey, machineId); // bool ok = await client.ValidateAsync(licenseKey, machineId); // // Responses are HMAC-SHA256 signed over canonical JSON; ValidateAsync verifies // the signature, echoes a per-call nonce and rejects stale responses. Drop this // file into your project or wrap it in a NuGet package. using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; namespace Authsy { public class Options { public string BaseUrl = ""; public string ApiKey = ""; public string? SigningSecret = null; public long MaxSkewMs = 5 * 60 * 1000; public HttpClient? Http = null; } public class AuthsyException : Exception { public string? Code; public AuthsyException(string message, string? code = null) : base(message) { Code = code; } } public class AuthsyClient { private readonly string _base; private readonly string _apiKey; private readonly string? _secret; private readonly long _skew; private readonly HttpClient _http; public JsonObject? LastResponse { get; private set; } public AuthsyClient(Options o) { if (string.IsNullOrEmpty(o.BaseUrl)) throw new AuthsyException("BaseUrl is required"); if (string.IsNullOrEmpty(o.ApiKey)) throw new AuthsyException("ApiKey is required"); _base = o.BaseUrl.TrimEnd('/'); _apiKey = o.ApiKey; _secret = o.SigningSecret; _skew = o.MaxSkewMs; _http = o.Http ?? new HttpClient(); } // Canonical JSON: object keys sorted lexicographically at every level. public static string Canonical(JsonNode? v) { if (v is JsonObject o) return "{" + string.Join(",", o.OrderBy(p => p.Key, StringComparer.Ordinal) .Select(p => JsonSerializer.Serialize(p.Key) + ":" + Canonical(p.Value))) + "}"; if (v is JsonArray a) return "[" + string.Join(",", a.Select(Canonical)) + "]"; return v?.ToJsonString() ?? "null"; } private string Hmac(string message) { using var h = new HMACSHA256(Encoding.UTF8.GetBytes(_secret!)); return Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(message))).ToLowerInvariant(); } public bool VerifySignature(JsonObject resp) { if (_secret == null) throw new AuthsyException("SigningSecret required to verify responses"); var sig = (string?)resp["signature"] ?? ""; var clone = JsonNode.Parse(resp.ToJsonString())!.AsObject(); clone.Remove("signature"); var expected = Hmac(Canonical(clone)); return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(sig)); } private async Task PostAsync(string path, JsonObject body) { var req = new HttpRequestMessage(HttpMethod.Post, _base + path) { Content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json") }; req.Headers.Add("X-Api-Key", _apiKey); var res = await _http.SendAsync(req); var raw = await res.Content.ReadAsStringAsync(); var data = JsonNode.Parse(raw)?.AsObject(); if ((int)res.StatusCode == 403) throw new AuthsyException("invalid api key", "invalid_api_key"); if ((int)res.StatusCode == 429) throw new AuthsyException("over quota", "over_quota"); if (data == null) throw new AuthsyException("empty response (" + (int)res.StatusCode + ")"); return data; } private JsonObject Checked(JsonObject resp, string? nonce) { if (_secret != null) { if (!VerifySignature(resp)) throw new AuthsyException("response signature invalid (tampered?)", "bad_signature"); if (nonce != null && (string?)resp["nonce"] != nonce) throw new AuthsyException("nonce mismatch (replay?)", "nonce_mismatch"); var ts = (long?)resp["ts"]; if (ts != null && Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - ts.Value) > _skew) throw new AuthsyException("response is stale", "stale"); } return resp; } private static string Nonce() => Guid.NewGuid().ToString(); public async Task ActivateAsync(string licenseKey, string machineId) { var nonce = Nonce(); var body = new JsonObject { ["license_key"] = licenseKey, ["machine_id"] = machineId, ["nonce"] = nonce }; return Checked(await PostAsync("/client/activate", body), nonce); } public async Task ValidateAsync(string licenseKey, string machineId) { var nonce = Nonce(); var body = new JsonObject { ["license_key"] = licenseKey, ["machine_id"] = machineId, ["nonce"] = nonce }; var resp = Checked(await PostAsync("/client/validate", body), nonce); LastResponse = resp; return (bool?)resp["valid"] ?? false; } } }