diff --git a/LightlessSync/FileCache/FileCacheManager.cs b/LightlessSync/FileCache/FileCacheManager.cs index c0a1bf3..d970896 100644 --- a/LightlessSync/FileCache/FileCacheManager.cs +++ b/LightlessSync/FileCache/FileCacheManager.cs @@ -230,11 +230,11 @@ public sealed class FileCacheManager : IHostedService brokenEntities.Add(fileCache); return; } - + var algo = Crypto.DetectAlgo(fileCache.Hash); string computedHash; try { - computedHash = await Crypto.GetFileHashAsync(fileCache.ResolvedFilepath, token).ConfigureAwait(false); + computedHash = await Crypto.ComputeFileHashAsync(fileCache.ResolvedFilepath, algo, token).ConfigureAwait(false); } catch (Exception ex) { @@ -246,8 +246,8 @@ public sealed class FileCacheManager : IHostedService if (!string.Equals(computedHash, fileCache.Hash, StringComparison.Ordinal)) { _logger.LogInformation( - "Hash mismatch: {file} (got {computedHash}, expected {expected})", - fileCache.ResolvedFilepath, computedHash, fileCache.Hash); + "Hash mismatch: {file} (got {computedHash}, expected {expected} : hash {hash})", + fileCache.ResolvedFilepath, computedHash, fileCache.Hash, algo); brokenEntities.Add(fileCache); } @@ -422,12 +422,13 @@ public sealed class FileCacheManager : IHostedService _logger.LogTrace("Updating hash for {path}", fileCache.ResolvedFilepath); var oldHash = fileCache.Hash; var prefixedPath = fileCache.PrefixedFilePath; + var algo = Crypto.DetectAlgo(fileCache.ResolvedFilepath); if (computeProperties) { var fi = new FileInfo(fileCache.ResolvedFilepath); fileCache.Size = fi.Length; fileCache.CompressedSize = null; - fileCache.Hash = Crypto.GetFileHash(fileCache.ResolvedFilepath); + fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, algo); fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); } RemoveHashedFile(oldHash, prefixedPath); @@ -570,7 +571,8 @@ public sealed class FileCacheManager : IHostedService private FileCacheEntity? CreateFileCacheEntity(FileInfo fileInfo, string prefixedPath, string? hash = null) { - hash ??= Crypto.GetFileHash(fileInfo.FullName); + var algo = Crypto.DetectAlgo(Path.GetFileNameWithoutExtension(fileInfo.Name)); + hash ??= Crypto.ComputeFileHash(fileInfo.FullName, algo); var entity = new FileCacheEntity(hash, prefixedPath, fileInfo.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture), fileInfo.Length); entity = ReplacePathPrefixes(entity); AddHashedFile(entity); diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index abe4a58..d8a21ab 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -27,6 +27,7 @@ + diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index dfcd975..08ce324 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -68,7 +68,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber try { var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false); - return cid.ToString().GetHash256(); + return cid.ToString().GetBlake3Hash(); } catch (Exception ex) { diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 4d50074..6bd6ec0 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -143,7 +143,7 @@ internal class ContextMenuService : IHostedService return; } - var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetBlake3Hash(); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 680aa29..8b77f16 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -347,7 +347,7 @@ public sealed class DtrEntry : IDisposable, IHostedService try { var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult(); - var hashedCid = cid.ToString().GetHash256(); + var hashedCid = cid.ToString().GetBlake3Hash(); _localHashedCid = hashedCid; _localHashedCidFetchedAt = now; return hashedCid; diff --git a/LightlessSync/Utils/Crypto.cs b/LightlessSync/Utils/Crypto.cs index 25215c0..e4ee053 100644 --- a/LightlessSync/Utils/Crypto.cs +++ b/LightlessSync/Utils/Crypto.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using Blake3; +using System.Security.Cryptography; using System.Text; namespace LightlessSync.Utils; @@ -9,20 +10,93 @@ public static class Crypto private const int _bufferSize = 65536; #pragma warning disable SYSLIB0021 // Type or member is obsolete + // SHA256 hash caches private static readonly Dictionary<(string, ushort), string> _hashListPlayersSHA256 = []; private static readonly Dictionary _hashListSHA256 = new(StringComparer.Ordinal); private static readonly SHA256CryptoServiceProvider _sha256CryptoProvider = new(); - - public static string GetFileHash(this string filePath) + + // 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); } - public static async Task GetFileHashAsync(string filePath, CancellationToken cancellationToken = default) + /// + /// 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)) @@ -31,18 +105,107 @@ public static class Crypto 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)) @@ -52,6 +215,11 @@ public static class Crypto 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 GetOrComputeHashSHA256(stringToHash); @@ -64,6 +232,8 @@ public static class Crypto return _hashListSHA256[stringToCompute] = Convert.ToHexString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))); - } + } + + #endregion #pragma warning restore SYSLIB0021 // Type or member is obsolete } \ No newline at end of file diff --git a/LightlessSync/packages.lock.json b/LightlessSync/packages.lock.json index e2bb034..c8b5c98 100644 --- a/LightlessSync/packages.lock.json +++ b/LightlessSync/packages.lock.json @@ -2,6 +2,12 @@ "version": 1, "dependencies": { "net10.0-windows7.0": { + "Blake3": { + "type": "Direct", + "requested": "[2.0.0, )", + "resolved": "2.0.0", + "contentHash": "v447kojeuNYSY5dvtVGG2bv1+M3vOWJXcrYWwXho/2uUpuwK6qPeu5WSMlqLm4VRJu96kysVO11La0zN3dLAuQ==" + }, "DalamudPackager": { "type": "Direct", "requested": "[13.1.0, )",