// sellkeys Auth – C# SDK (.NET 6+) // // dotnet add package BouncyCastle.Cryptography // // var auth = new SellkeysAuth("APP_ID", "PUBLIC_KEY", "1.0"); // try { // await auth.InitAsync(); // var lic = await auth.LicenseAsync(Console.ReadLine()!); // Console.WriteLine($"Welcome! Level {lic.Level}, expires {lic.ExpiresAt?.ToString() ?? "never"}"); // } catch (SellkeysAuthException e) { Console.WriteLine($"Login failed: {e.Code} {e.Message}"); } // // Every answer is signed with your app's Ed25519 key; the SDK rejects wrong signatures and replayed nonces, so a fake // server cannot unlock your program. (It does not protect against patching your program.) using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Org.BouncyCastle.Math.EC.Rfc8032; public sealed class SellkeysAuthException : Exception { public string Code { get; } public SellkeysAuthException(string code, string message) : base(message) => Code = code; } public sealed record SellkeysLicense(int Level, DateTimeOffset? ExpiresAt, int Devices, int DeviceLimit); public sealed record SellkeysUser(string Username, int? Level, DateTimeOffset? ExpiresAt, int Devices, int DeviceLimit, int Licenses = 0); public sealed record SellkeysAppInfo(string Name, string Version, string? UpdateUrl); public sealed class SellkeysAuth { public const string DefaultApiUrl = "https://auth.sellkeys.cc/v1"; private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(15) }; private readonly string _appId, _version, _apiUrl; private readonly byte[] _publicKey; public string Hwid { get; } public string? Session { get; private set; } public string? UpdateUrl { get; private set; } /// Name, latest version and update URL of the app, set by InitAsync. public SellkeysAppInfo? App { get; private set; } public SellkeysLicense? License { get; private set; } public SellkeysUser? User { get; private set; } public SellkeysAuth(string appId, string publicKey, string version, string apiUrl = DefaultApiUrl, string? hwid = null) { _appId = appId; _version = version; _apiUrl = apiUrl; _publicKey = Convert.FromBase64String(publicKey); if (_publicKey.Length != 32) throw new ArgumentException("Invalid public key"); Hwid = hwid ?? DefaultHwid(); } /// Stable per-machine id, hashed (the raw id never leaves the machine). public static string DefaultHwid() { string raw = ""; try { if (OperatingSystem.IsWindows()) raw = Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography", "MachineGuid", "") as string ?? ""; else if (File.Exists("/etc/machine-id")) raw = File.ReadAllText("/etc/machine-id").Trim(); } catch { raw = ""; } if (raw.Length == 0) raw = Environment.MachineName + Environment.UserName; return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(raw))).ToLowerInvariant(); } /// SHA-256 of your program file for the app's hash check (default: the running executable). public static string FileHash(string? path = null) { using var f = File.OpenRead(path ?? Environment.ProcessPath!); return Convert.ToHexString(SHA256.HashData(f)).ToLowerInvariant(); } private async Task CallAsync(string type, Dictionary fields) { var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant(); var form = new Dictionary { ["type"] = type, ["app"] = _appId, ["nonce"] = nonce }; foreach (var (k, v) in fields) if (v != null) form[k] = v; HttpResponseMessage res; try { res = await Http.PostAsync(_apiUrl, new FormUrlEncodedContent(form)); } catch (Exception e) { throw new SellkeysAuthException("network_error", "Could not reach the license server: " + e.Message); } var bytes = await res.Content.ReadAsByteArrayAsync(); if (!res.Headers.TryGetValues("X-Sellkeys-Signature", out var sigs)) { // only errors before the app is known (rate limit, unknown app) are unsigned try { var e = JsonDocument.Parse(bytes).RootElement; throw new SellkeysAuthException(e.GetProperty("code").GetString()!, e.GetProperty("message").GetString()!); } catch (Exception ex) when (ex is not SellkeysAuthException) { throw new SellkeysAuthException($"http_{(int)res.StatusCode}", "Unexpected server response."); } } var sig = Convert.FromBase64String(sigs.First()); if (sig.Length != 64 || !Ed25519.Verify(sig, 0, _publicKey, 0, bytes, 0, bytes.Length)) throw new SellkeysAuthException("bad_signature", "The server response is not signed by sellkeys."); var root = JsonDocument.Parse(bytes).RootElement.Clone(); if (root.GetProperty("nonce").GetString() != nonce) throw new SellkeysAuthException("bad_nonce", "The server response does not belong to this request."); if (!root.GetProperty("success").GetBoolean()) { if (root.TryGetProperty("update_url", out var u) && u.ValueKind == JsonValueKind.String) UpdateUrl = u.GetString(); throw new SellkeysAuthException(root.GetProperty("code").GetString()!, root.GetProperty("message").GetString()!); } return root; } private static SellkeysLicense ParseLicense(JsonElement l) => new( l.GetProperty("level").GetInt32(), l.GetProperty("expires_at").ValueKind == JsonValueKind.String ? DateTimeOffset.Parse(l.GetProperty("expires_at").GetString()!) : null, l.GetProperty("devices").GetInt32(), l.GetProperty("device_limit").GetInt32()); /// Starts a session. Code "update_required" means your version is too old (see UpdateUrl). public async Task InitAsync(string? fileHash = null) { var r = await CallAsync("init", new() { ["version"] = _version, ["hash"] = fileHash }); Session = r.GetProperty("session").GetString(); var a = r.GetProperty("app"); UpdateUrl = a.TryGetProperty("update_url", out var uu) && uu.ValueKind == JsonValueKind.String ? uu.GetString() : null; App = new SellkeysAppInfo(a.GetProperty("name").GetString()!, a.GetProperty("version").GetString()!, UpdateUrl); } public async Task LicenseAsync(string key) { if (Session == null) await InitAsync(); var r = await CallAsync("license", new() { ["session"] = Session, ["key"] = key.Trim(), ["hwid"] = Hwid }); return License = ParseLicense(r.GetProperty("license")); } /// Call periodically: throws if the license/account was banned, expired or the session ended. public async Task CheckAsync() { var r = await CallAsync("check", new() { ["session"] = Session }); if (r.TryGetProperty("user", out var u)) User = ParseUser(u); else License = ParseLicense(r.GetProperty("license")); } private static SellkeysUser ParseUser(JsonElement u) => new( u.GetProperty("username").GetString()!, u.TryGetProperty("level", out var lv) && lv.ValueKind == JsonValueKind.Number ? lv.GetInt32() : null, u.TryGetProperty("expires_at", out var e) && e.ValueKind == JsonValueKind.String ? DateTimeOffset.Parse(e.GetString()!) : null, u.GetProperty("devices").GetInt32(), u.GetProperty("device_limit").GetInt32(), u.TryGetProperty("licenses", out var n) && n.ValueKind == JsonValueKind.Number ? n.GetInt32() : 0); // ---- accounts (username + password; the key is redeemed to the account) ---- public async Task RegisterAsync(string username, string password, string key) { if (Session == null) await InitAsync(); var r = await CallAsync("register", new() { ["session"] = Session, ["username"] = username, ["password"] = password, ["key"] = key.Trim(), ["hwid"] = Hwid }); return User = ParseUser(r.GetProperty("user")); } public async Task LoginAsync(string username, string password) { if (Session == null) await InitAsync(); var r = await CallAsync("login", new() { ["session"] = Session, ["username"] = username, ["password"] = password, ["hwid"] = Hwid }); return User = ParseUser(r.GetProperty("user")); } /// Adds another license (renewal, higher level) to an existing account. public async Task UpgradeAsync(string username, string key) { if (Session == null) await InitAsync(); var r = await CallAsync("upgrade", new() { ["session"] = Session, ["username"] = username, ["key"] = key.Trim() }); return ParseUser(r.GetProperty("user")); } // ---- content ---- /// Global app variable (set in the dashboard). public async Task VarAsync(string name) => (await CallAsync("var", new() { ["session"] = Session, ["name"] = name })).GetProperty("value").GetString()!; public async Task GetUserVarAsync(string name) => (await CallAsync("getvar", new() { ["session"] = Session, ["name"] = name })).GetProperty("value").GetString()!; public async Task SetUserVarAsync(string name, string value) => await CallAsync("setvar", new() { ["session"] = Session, ["name"] = name, ["value"] = value }); /// Downloads an app file and checks it against the SHA-256 from the signed answer. public async Task DownloadAsync(string fileId) { var info = await CallAsync("file", new() { ["session"] = Session, ["file"] = fileId }); byte[] data; try { data = await Http.GetByteArrayAsync(info.GetProperty("url").GetString()); } catch (Exception e) { throw new SellkeysAuthException("network_error", "Download failed: " + e.Message); } if (Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant() != info.GetProperty("sha256").GetString()) throw new SellkeysAuthException("bad_file_hash", "The downloaded file does not match its signed checksum."); return data; } /// Calls a webhook set in the dashboard through sellkeys (its URL and keys stay on the server). public async Task<(int Status, string Response)> WebhookAsync(string name, string parameters = "", string? body = null, string? contentType = null) { var f = new Dictionary { ["session"] = Session, ["webid"] = name }; if (parameters.Length > 0) f["params"] = parameters; if (body != null) { f["body"] = body; if (contentType != null) f["content_type"] = contentType; } var r = await CallAsync("webhook", f); return (r.GetProperty("status").GetInt32(), r.GetProperty("response").GetString()!); } /// Writes a line to the app's logs in the sellkeys dashboard. public async Task LogAsync(string message) => await CallAsync("log", new() { ["session"] = Session, ["message"] = message.Length > 500 ? message[..500] : message }); public async Task LogoutAsync() { if (Session == null) return; await CallAsync("logout", new() { ["session"] = Session }); Session = null; } }