using System; using System.Buffers.Binary; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace CarManagerV3.Util { internal class CUID { public const int DefaultLength = 6; private static int _counter = RandomNumberGenerator.GetInt32(int.MaxValue); /// /// Generate a random CUID (Collision-resistant Unique Identifier) of a specified length. /// The CUID is designed to be unique across different machines and time, making it suitable for use as an identifier for cars in the application. /// The length must be between 4 and 32 characters to ensure a good balance between uniqueness and readability. /// The generated CUID consists of a combination of alphanumeric characters to ensure uniqueness and readability. /// /// The desired length of the generated CUID /// Whether to prefix the CUID with 'c' for better readability and to avoid starting with a digit." /// public static string NewCUID(int length = DefaultLength, bool prefixWithC = true) { // CUIDv2 specs allow between 4 and 32 chars. if(length < 4 || length > 32) throw new ArgumentOutOfRangeException("length"); // We will produce enough encoded chars to satisfy 'length' after prefixing and truncation. // Base64 encodes 3 bytes -> 4 chars. So bytesNeeded ≈ ceil(charsNeeded * 3/4). int charsNeeded = prefixWithC ? (length - 1) : length; int bytesNeeded = (int)Math.Ceiling(charsNeeded * 3.0 / 4.0); Span material = stackalloc byte[32]; FillMaterial(material); byte[] outputBytes = ExpandWithSha256(material, bytesNeeded); string encoded = Base64UrlEncode(outputBytes); if (encoded.Length < charsNeeded) { // Extremely unlikely unless length is huge; ensure we have enough by expanding more. // (Kept as a guard; for typical lengths like 24-64, you're fine.) outputBytes = ExpandWithSha256(material, bytesNeeded + 32); encoded = Base64UrlEncode(outputBytes); } string body = encoded.Substring(0, charsNeeded); return prefixWithC ? ("c" + body) : body; } private static void FillMaterial(Span dst32) { // Compose a payload with: // - 16 bytes random // - 8 bytes timestamp (UTC ticks) // - 4 bytes counter // - 4 bytes process/thread noise Span payload = stackalloc byte[16 + 8 + 4 + 4]; RandomNumberGenerator.Fill(payload.Slice(0, 16)); long ticks = DateTime.UtcNow.Ticks; BinaryPrimitives.WriteInt64LittleEndian(payload.Slice(16, 8), ticks); int c = Interlocked.Increment(ref _counter); BinaryPrimitives.WriteInt32LittleEndian(payload.Slice(24, 4), c); // Some extra variability (not relied on for security) int noise = Environment.ProcessId ^ Thread.CurrentThread.ManagedThreadId ^ (int)Stopwatch.GetTimestamp(); BinaryPrimitives.WriteInt32LittleEndian(payload.Slice(28, 4), noise); // Hash to produce 32 bytes of uniformly distributed output SHA256.HashData(payload, dst32); } private static byte[] ExpandWithSha256(ReadOnlySpan seed32, int bytesNeeded) { if (bytesNeeded <= 0) return Array.Empty(); // If <= 32 bytes, we can just take from seed32 by hashing once more for separation. // We'll use SHA256(seed || blockIndex) to generate blocks. int blocks = (int)Math.Ceiling(bytesNeeded / 32.0); byte[] result = new byte[blocks * 32]; Span input = stackalloc byte[32 + 4]; seed32.CopyTo(input.Slice(0, 32)); for (int i = 0; i < blocks; i++) { BinaryPrimitives.WriteInt32LittleEndian(input.Slice(32, 4), i); Span block = result.AsSpan(i * 32, 32); SHA256.HashData(input, block); } if (result.Length == bytesNeeded) return result; byte[] trimmed = new byte[bytesNeeded]; Buffer.BlockCopy(result, 0, trimmed, 0, bytesNeeded); return trimmed; } private static string Base64UrlEncode(byte[] bytes) { // Standard base64url without padding per RFC 4648 §5 string b64 = Convert.ToBase64String(bytes); return b64.Replace('+', '-').Replace('/', '_').TrimEnd('='); } } }