// SkullLabsAuth.cs // Single-file C# SDK for Skull Labs Auth. // .NET Framework 4.7.2+ / .NET 6+ / .NET Standard 2.0 compatible. // // Usage: // var client = new SkullLabs.AuthClient("slk_your_project_key"); // var session = await client.LoginAsync("alice", "hunter2"); // if (session.Ok) Console.WriteLine($"hi, {session.User.Username}"); // // Drop this file into any C# project. No NuGet packages required beyond // System.Net.Http (BCL) and System.Text.Json (built-in on .NET 6+; on // .NET Framework add the NuGet "System.Text.Json"). using System; using System.Collections.Generic; using System.Net.Http; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; #if !NETSTANDARD2_0 // System.Management and Microsoft.Win32.Registry are added via NuGet on net6.0+ #endif namespace SkullLabs { public static class SkullLabsSettings { public const string ProjectKey = "slk_REPLACE_WITH_YOUR_PROJECT_KEY"; public const string BaseUrl = "https://api.skulllabs.in"; } public class AuthClient { private static readonly HttpClient Http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) }; public string BaseUrl { get; } public string ProjectKey { get; } public AuthClient() : this(SkullLabsSettings.ProjectKey, SkullLabsSettings.BaseUrl) { } public AuthClient(string projectKey, string baseUrl = null) { if (string.IsNullOrWhiteSpace(projectKey)) throw new ArgumentException("projectKey required"); ProjectKey = projectKey; BaseUrl = (baseUrl ?? SkullLabsSettings.BaseUrl).TrimEnd('/'); } public Task LoginAsync(string username, string password) => PostLoginAsync(new { apiKey = ProjectKey, username, password, hwid = HardwareId.Get() }); public Task LoginWithLicenseAsync(string licenseKey) => PostLicenseAsync(new { apiKey = ProjectKey, key = licenseKey, hwid = HardwareId.Get() }); public async Task RegisterAsync(string username, string password, string email = null) { var body = new { apiKey = ProjectKey, username, password, email }; var resp = await Http.PostAsync($"{BaseUrl}/v1/sdk/register", new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json")) .ConfigureAwait(false); var json = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); var result = JsonSerializer.Deserialize(json, JsonOpts); if (result == null) return new RegisterResult { Ok = false, Error = "empty response" }; return result; } private async Task PostLoginAsync(object body) { try { var resp = await Http.PostAsync($"{BaseUrl}/v1/sdk/login", new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json")) .ConfigureAwait(false); var json = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); var result = JsonSerializer.Deserialize(json, JsonOpts); if (result == null) return new LoginResult { Ok = false, Error = "empty response" }; return result; } catch (Exception ex) { return new LoginResult { Ok = false, Error = ex.Message }; } } private async Task PostLicenseAsync(object body) { try { var resp = await Http.PostAsync($"{BaseUrl}/v1/license/verify", new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json")) .ConfigureAwait(false); var json = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); var result = JsonSerializer.Deserialize(json, JsonOpts); if (result == null) return new LoginResult { Ok = false, Error = "empty response" }; if (!result.Ok && string.IsNullOrWhiteSpace(result.Error)) { result.Error = "license verification failed"; } return result; } catch (Exception ex) { return new LoginResult { Ok = false, Error = ex.Message }; } } private static readonly JsonSerializerOptions JsonOpts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }; } public class LoginResult { [JsonPropertyName("ok")] public bool Ok { get; set; } [JsonPropertyName("error")] public string Error { get; set; } [JsonPropertyName("user")] public SessionUser User { get; set; } [JsonPropertyName("license")] public LicenseInfo License { get; set; } [JsonPropertyName("project")] public ProjectInfo Project { get; set; } [JsonPropertyName("sessionId")] public string SessionId { get; set; } [JsonPropertyName("issuedAt")] public long IssuedAt { get; set; } } public class SessionUser { [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("username")] public string Username { get; set; } [JsonPropertyName("email")] public string Email { get; set; } [JsonPropertyName("expiresAt")] public long? ExpiresAt { get; set; } public DateTime? ExpiresAtUtc => ExpiresAt.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(ExpiresAt.Value).UtcDateTime : (DateTime?)null; } public class LicenseInfo { [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("expiresAt")] public long? ExpiresAt { get; set; } [JsonPropertyName("uses")] public int Uses { get; set; } [JsonPropertyName("maxUses")] public int MaxUses { get; set; } } public class ProjectInfo { [JsonPropertyName("id")] public string Id { get; set; } [JsonPropertyName("name")] public string Name { get; set; } } public class RegisterResult { [JsonPropertyName("ok")] public bool Ok { get; set; } [JsonPropertyName("error")] public string Error { get; set; } [JsonPropertyName("user")] public SessionUser User { get; set; } } /// /// Hardware fingerprint built from multiple stable hardware identifiers. /// /// Sources, in priority order: /// Windows: /// 1. HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid (very stable) /// 2. Win32_ComputerSystemProduct.UUID (SMBIOS UUID) /// 3. Win32_BIOS.SerialNumber /// 4. Win32_BaseBoard.SerialNumber (motherboard) /// 5. Win32_Processor.ProcessorId (CPU ID) /// 6. Win32_DiskDrive.SerialNumber (first physical disk) /// Linux: /// 1. /etc/machine-id /// 2. /sys/class/dmi/id/product_uuid (BIOS UUID, may be root-only) /// 3. /sys/class/dmi/id/board_serial (may be root-only) /// macOS: /// 1. IOPlatformUUID via `ioreg -rd1 -c IOPlatformExpertDevice` /// /// Plus host name + first non-loopback MAC as breadth. /// Everything is concatenated and SHA-256 hashed. Missing / placeholder /// values (e.g. OEM "To Be Filled By O.E.M.") are skipped. /// Cached for the process lifetime. /// public static class HardwareId { private static string _cached; private static readonly object _lock = new object(); public static string Get() { if (_cached != null) return _cached; lock (_lock) { if (_cached != null) return _cached; var parts = new List(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Try(parts, "mg", () => ReadMachineGuid()); Try(parts, "uuid", () => Wmi("SELECT UUID FROM Win32_ComputerSystemProduct", "UUID")); Try(parts, "bios", () => Wmi("SELECT SerialNumber FROM Win32_BIOS", "SerialNumber")); Try(parts, "board", () => Wmi("SELECT SerialNumber FROM Win32_BaseBoard", "SerialNumber")); Try(parts, "cpu", () => Wmi("SELECT ProcessorId FROM Win32_Processor", "ProcessorId")); Try(parts, "disk", () => Wmi("SELECT SerialNumber FROM Win32_DiskDrive", "SerialNumber")); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { Try(parts, "mid", () => ReadFile("/etc/machine-id")); Try(parts, "uuid", () => ReadFile("/sys/class/dmi/id/product_uuid")); Try(parts, "board", () => ReadFile("/sys/class/dmi/id/board_serial")); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { Try(parts, "uuid", () => RunShell("ioreg", "-rd1 -c IOPlatformExpertDevice", @"IOPlatformUUID""\s*=\s*""([0-9A-Fa-f-]+)""")); } // Breadth: hostname + first non-loopback MAC. Try(parts, "host", () => Environment.MachineName); Try(parts, "mac", () => FirstMac()); if (parts.Count == 0) { parts.Add("fallback:" + Environment.MachineName); } var seed = string.Join("|", parts); using (var sha = SHA256.Create()) { var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(seed)); _cached = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); } return _cached; } } /// For debugging — exposes the raw seed (without hashing). public static string DebugSeed() { if (_cached == null) Get(); return _cached; } // ---- helpers ---- private static void Try(List parts, string key, Func fn) { try { var v = fn(); if (string.IsNullOrWhiteSpace(v)) return; v = v.Trim(); if (IsJunk(v)) return; parts.Add(key + "=" + v); } catch { /* swallow — non-fatal */ } } private static bool IsJunk(string s) { // OEMs sometimes ship placeholder values. Filter the known offenders. var l = s.ToLowerInvariant(); return l == "to be filled by o.e.m." || l == "default string" || l == "system serial number" || l == "system manufacturer" || l == "0" || l == "00000000-0000-0000-0000-000000000000" || l == "none" || l == "not specified" || l == "not applicable" || l == "n/a" || l == "null"; } private static string FirstMac() { foreach (var nic in NetworkInterface.GetAllNetworkInterfaces()) { if (nic.OperationalStatus != OperationalStatus.Up) continue; if (nic.NetworkInterfaceType == NetworkInterfaceType.Loopback) continue; var addr = nic.GetPhysicalAddress()?.ToString(); if (!string.IsNullOrEmpty(addr) && addr != "000000000000") return addr; } return null; } private static string ReadFile(string path) { try { return System.IO.File.ReadAllText(path).Trim(); } catch { return null; } } private static string ReadMachineGuid() { #if WINDOWS || NET6_0_OR_GREATER try { using (var k = Microsoft.Win32.RegistryKey.OpenBaseKey( Microsoft.Win32.RegistryHive.LocalMachine, Microsoft.Win32.RegistryView.Registry64) .OpenSubKey(@"SOFTWARE\Microsoft\Cryptography")) { return k?.GetValue("MachineGuid")?.ToString(); } } catch { return null; } #else return null; #endif } private static string Wmi(string query, string field) { #if WINDOWS || NET6_0_OR_GREATER try { using (var s = new System.Management.ManagementObjectSearcher(query)) { foreach (System.Management.ManagementObject o in s.Get()) { var v = o[field]?.ToString(); if (!string.IsNullOrWhiteSpace(v)) return v; } } } catch { /* swallow */ } #endif return null; } private static string RunShell(string exe, string args, string regex) { try { using (var p = new System.Diagnostics.Process()) { p.StartInfo.FileName = exe; p.StartInfo.Arguments = args; p.StartInfo.RedirectStandardOutput = true; p.StartInfo.UseShellExecute = false; p.StartInfo.CreateNoWindow = true; p.Start(); var output = p.StandardOutput.ReadToEnd(); p.WaitForExit(2000); var m = System.Text.RegularExpressions.Regex.Match(output, regex); return m.Success ? m.Groups[1].Value : null; } } catch { return null; } } } }