using System; using System.Collections.Generic; namespace VanadiumGuard.Protocol { /// /// A machine identity, as reported by the client at session open. /// /// Every value in here is already a salted hash when it leaves the player's machine. /// The service never sees a disk serial or a motherboard serial, only opaque strings it /// can compare for equality. That is deliberate: a ban list is a long-lived store of /// data about people who are not around to consent to it, so it holds the least it can /// while still doing its job. /// /// /// The composite is the primary identity. The individual components exist so a ban can /// survive one part being swapped: someone who replaces a disk keeps the same /// motherboard and the same Windows install id, and matching on any single strong /// component still recognises the machine. That is also why a component must be strong /// to count - matching on a CPU model would ban everyone who bought the same processor. /// /// public sealed class MachineFingerprint { /// Hex SHA-256 over every component. Empty when collection failed. public string Composite { get; set; } = string.Empty; /// Per-source hashes, keyed by the names in . public Dictionary Components { get; set; } = new Dictionary(StringComparer.Ordinal); public DateTimeOffset CollectedUtc { get; set; } public bool IsUsable => !string.IsNullOrEmpty(Composite); /// /// True when both fingerprints name the same machine: the same composite, or any /// shared strong component. Weak components are carried for triage and are never /// enough on their own. /// public bool Matches(MachineFingerprint? other) { if (other == null || !IsUsable || !other.IsUsable) return false; if (string.Equals(Composite, other.Composite, StringComparison.Ordinal)) return true; foreach (string name in MachineComponents.Strong) { if (Components.TryGetValue(name, out string? mine) && other.Components.TryGetValue(name, out string? theirs) && !string.IsNullOrEmpty(mine) && string.Equals(mine, theirs, StringComparison.Ordinal)) { return true; } } return false; } /// Which strong components two fingerprints agree on. For audit logs. public IReadOnlyList SharedStrongComponents(MachineFingerprint? other) { var shared = new List(); if (other == null) return shared; foreach (string name in MachineComponents.Strong) { if (Components.TryGetValue(name, out string? mine) && other.Components.TryGetValue(name, out string? theirs) && !string.IsNullOrEmpty(mine) && string.Equals(mine, theirs, StringComparison.Ordinal)) { shared.Add(name); } } return shared; } } /// /// Component names, and which of them identify one machine rather than one model of /// machine. Names are persisted in ban records, so they are never renamed. /// public static class MachineComponents { /// Windows installation id. Survives hardware changes, dies on reinstall. public const string MachineGuid = "machineGuid"; /// Volume serial of the system drive. Dies on reformat. public const string SystemVolume = "systemVolume"; /// Motherboard identity, serial included. The most durable component there is. public const string Baseboard = "baseboard"; /// BIOS vendor, version and date. Weak: it names a model, not a machine. public const string Bios = "bios"; /// Processor identifier string. A model, not a machine - weak by design. public const string Processor = "processor"; /// OS build. Triage only; changes with every update. public const string OsBuild = "osBuild"; /// /// Components specific enough to identify one machine. Only these are used to /// recognise a banned machine whose composite has changed. /// public static readonly string[] Strong = { MachineGuid, SystemVolume, Baseboard }; /// Carried for review tooling, never used for matching. public static readonly string[] Weak = { Bios, Processor, OsBuild }; } }