using Blake3; using System; using System.Collections.Concurrent; using System.IO; using System.Security.Cryptography; using System.Text; namespace LightlessSync.Utils; public static class Crypto { //This buffersize seems to be the best sweetpoint for Linux and Windows private const int _bufferSize = 65536; #pragma warning disable SYSLIB0021 // Type or member is obsolete private static readonly ConcurrentDictionary<(string, ushort), string> _hashListPlayersSHA256 = new(); private static readonly ConcurrentDictionary _hashListSHA256 = new(StringComparer.Ordinal); private static readonly SHA256CryptoServiceProvider _sha256CryptoProvider = new(); // BLAKE3 hash caches private static readonly Dictionary<(string, ushort), string> _hashListPlayersBlake3 = []; private static readonly Dictionary _hashListBlake3 = new(StringComparer.Ordinal); /// /// Supports Blake3 or SHA1 for file transfers, no SHA256 supported on it /// public enum HashAlgo { Blake3, Sha1 } /// /// Detects which algo is being used for the file /// /// Hashed string /// HashAlgo public static HashAlgo DetectAlgo(string hashHex) { if (hashHex.Length == 40) return HashAlgo.Sha1; return HashAlgo.Blake3; } #region File Hashing /// /// Compute file hash with given algorithm, supports BLAKE3 and Sha1 for file hashing /// /// Filepath for the hashing /// BLAKE3 or Sha1 /// Hashed file hash /// Not a valid HashAlgo or Filepath public static string ComputeFileHash(string filePath, HashAlgo algo) { return algo switch { HashAlgo.Blake3 => ComputeFileHashBlake3(filePath), HashAlgo.Sha1 => ComputeFileHashSha1(filePath), _ => throw new ArgumentOutOfRangeException(nameof(algo), algo, null) }; } /// /// Compute file hash asynchronously with given algorithm, supports BLAKE3 and SHA1 for file hashing /// /// Filepath for the hashing /// BLAKE3 or Sha1 /// Hashed file hash /// Not a valid HashAlgo or Filepath public static async Task ComputeFileHashAsync(string filePath, HashAlgo algo, CancellationToken cancellationToken = default) { return algo switch { HashAlgo.Blake3 => await ComputeFileHashBlake3Async(filePath, cancellationToken).ConfigureAwait(false), HashAlgo.Sha1 => await ComputeFileHashSha1Async(filePath, cancellationToken).ConfigureAwait(false), _ => throw new ArgumentOutOfRangeException(nameof(algo), algo, message: null) }; } /// /// Computes an file hash with SHA1 /// /// Filepath that has to be computed /// Hashed file in hex string private static string ComputeFileHashSha1(string filePath) { using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using var sha1 = SHA1.Create(); var hash = sha1.ComputeHash(stream); return Convert.ToHexString(hash); } /// /// Computes an file hash with SHA1 asynchronously /// /// Filepath that has to be computed /// Cancellation token /// Hashed file in hex string hashed in SHA1 private static async Task ComputeFileHashSha1Async(string filePath, CancellationToken cancellationToken) { var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: _bufferSize, options: FileOptions.Asynchronous); await using (stream.ConfigureAwait(false)) { using var sha1 = SHA1.Create(); var buffer = new byte[8192]; int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) { sha1.TransformBlock(buffer, 0, bytesRead, outputBuffer: null, 0); } sha1.TransformFinalBlock([], 0, 0); return Convert.ToHexString(sha1.Hash!); } } /// /// Computes an file hash with Blake3 /// /// Filepath that has to be computed /// Hashed file in hex string hashed in Blake3 private static string ComputeFileHashBlake3(string filePath) { using var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using var hasher = Hasher.New(); var buffer = new byte[_bufferSize]; int bytesRead; while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0) { hasher.Update(buffer.AsSpan(0, bytesRead)); } var hash = hasher.Finalize(); return hash.ToString(); } /// /// Computes an file hash with Blake3 asynchronously /// /// Filepath that has to be computed /// Hashed file in hex string hashed in Blake3 private static async Task ComputeFileHashBlake3Async(string filePath, CancellationToken cancellationToken) { var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, bufferSize: _bufferSize, options: FileOptions.Asynchronous); await using (stream.ConfigureAwait(false)) { using var hasher = Hasher.New(); var buffer = new byte[8192]; int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) { hasher.Update(buffer.AsSpan(0, bytesRead)); } var hash = hasher.Finalize(); return hash.ToString(); } } #endregion #region String hashing public static string GetBlake3Hash(this (string, ushort) playerToHash) { if (_hashListPlayersBlake3.TryGetValue(playerToHash, out var hash)) return hash; var toHash = playerToHash.Item1 + playerToHash.Item2.ToString(); hash = ComputeBlake3Hex(toHash); _hashListPlayersBlake3[playerToHash] = hash; return hash; } /// /// Computes or gets an Blake3 hash(ed) string. /// /// String that needs to be hashsed /// Hashed string public static string GetBlake3Hash(this string stringToHash) { return GetOrComputeBlake3(stringToHash); } private static string GetOrComputeBlake3(string stringToCompute) { if (_hashListBlake3.TryGetValue(stringToCompute, out var hash)) return hash; hash = ComputeBlake3Hex(stringToCompute); _hashListBlake3[stringToCompute] = hash; return hash; } private static string ComputeBlake3Hex(string input) { var bytes = Encoding.UTF8.GetBytes(input); var hash = Hasher.Hash(bytes); return Convert.ToHexString(hash.AsSpan()); } public static string GetHash256(this (string, ushort) playerToHash) { if (_hashListPlayersSHA256.TryGetValue(playerToHash, out var hash)) return hash; return _hashListPlayersSHA256[playerToHash] = Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(playerToHash.Item1 + playerToHash.Item2.ToString()))); } /// /// Computes or gets an SHA256 hash(ed) string. /// /// String that needs to be hashsed /// Hashed string public static string GetHash256(this string stringToHash) { return _hashListSHA256.GetOrAdd(stringToHash, ComputeHashSHA256); } private static string ComputeHashSHA256(string stringToCompute) { if (_hashListSHA256.TryGetValue(stringToCompute, out var hash)) return hash; return _hashListSHA256[stringToCompute] = Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))); } #endregion #pragma warning restore SYSLIB0021 // Type or member is obsolete }