diff --git a/Azaion.Services/ResourcesService.cs b/Azaion.Services/ResourcesService.cs index 4d3d4f1..ea63ba4 100644 --- a/Azaion.Services/ResourcesService.cs +++ b/Azaion.Services/ResourcesService.cs @@ -18,7 +18,7 @@ public class ResourcesService(IOptions resourcesConfig) : IReso { var fileStream = new FileStream(GetResourcePath(request.ResourceEnum), FileMode.Open, FileAccess.Read); var key = Security.MakeEncryptionKey(request.Username, request.Password); - await fileStream.Encrypt(outputStream, key, cancellationToken); + await fileStream.EncryptTo(outputStream, key, cancellationToken); } public async Task SaveResource(UploadResourceRequest request, CancellationToken cancellationToken = default) diff --git a/Azaion.Services/Security.cs b/Azaion.Services/Security.cs index 0040a28..b10b935 100644 --- a/Azaion.Services/Security.cs +++ b/Azaion.Services/Security.cs @@ -5,7 +5,7 @@ namespace Azaion.Services; public static class Security { - private const int BUFFER_SIZE = 81920; // 80 KB buffer size + private const int BUFFER_SIZE = 524288; // 512 KB buffer size public static string ToHash(this string str) => Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str))); @@ -13,9 +13,9 @@ public static class Security public static string MakeEncryptionKey(string username, string password) => $"{username}-{password}---#%@AzaionKey@%#---"; - public static async Task Encrypt(this Stream stream, Stream outputStream, string key, CancellationToken cancellationToken = default) + public static async Task EncryptTo(this Stream stream, Stream toStream, string key, CancellationToken cancellationToken = default) { - if (stream is { CanSeek: false }) throw new ArgumentNullException(nameof(stream)); + if (stream is { CanRead: false }) throw new ArgumentNullException(nameof(stream)); if (key is not { Length: > 0 }) throw new ArgumentNullException(nameof(key)); using var aes = Aes.Create(); @@ -23,10 +23,10 @@ public static class Security aes.GenerateIV(); using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV); - await using var cs = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write); + await using var cs = new CryptoStream(toStream, encryptor, CryptoStreamMode.Write, leaveOpen: true); // Prepend IV to the encrypted data - await outputStream.WriteAsync(aes.IV.AsMemory(0, aes.IV.Length), cancellationToken); + await toStream.WriteAsync(aes.IV.AsMemory(0, aes.IV.Length), cancellationToken); var buffer = new byte[BUFFER_SIZE]; int bytesRead; @@ -34,7 +34,7 @@ public static class Security await cs.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); } - public static async Task Decrypt(this Stream encryptedStream, Stream outputStream, string key, CancellationToken cancellationToken = default) + public static async Task DecryptTo(this Stream encryptedStream, Stream toStream, string key, CancellationToken cancellationToken = default) { using var aes = Aes.Create(); aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key)); @@ -45,12 +45,12 @@ public static class Security aes.IV = iv; using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); - await using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read); + await using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read, leaveOpen: true); // Read and write in chunks var buffer = new byte[BUFFER_SIZE]; int bytesRead; while ((bytesRead = await cryptoStream.ReadAsync(buffer, cancellationToken)) > 0) - await outputStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); + await toStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); } } diff --git a/Azaion.Test/SecurityTest.cs b/Azaion.Test/SecurityTest.cs index 2fb943b..83e02ad 100644 --- a/Azaion.Test/SecurityTest.cs +++ b/Azaion.Test/SecurityTest.cs @@ -1,6 +1,8 @@ -using System.Text; +using System.Security.Cryptography; +using System.Text; using Azaion.Services; using FluentAssertions; +using Newtonsoft.Json; using Xunit; namespace Azaion.Test; @@ -15,16 +17,76 @@ public class SecurityTest var password = "testpw"; var key = Security.MakeEncryptionKey(username, password); - await using var encryptedStream = new MemoryStream(); - await StringToStream(testString).Encrypt(encryptedStream, key); + var encryptedStream = new MemoryStream(); + await StringToStream(testString).EncryptTo(encryptedStream, key); + encryptedStream.Seek(0, SeekOrigin.Begin); await using var decryptedStream = new MemoryStream(); - await encryptedStream.Decrypt(decryptedStream, key); + await encryptedStream.DecryptTo(decryptedStream, key); + encryptedStream.Close(); var str = StreamToString(decryptedStream); str.Should().Be(testString); } + [Fact] + public async Task EncryptDecryptLargeFileTest() + { + var username = "user@azaion.com"; + var password = "testpw"; + var key = Security.MakeEncryptionKey(username, password); + + var largeFilePath = "large.txt"; + var largeFileDecryptedPath = "large_decrypted.txt"; + + var stream = await CreateLargeFile(largeFilePath); + stream.Seek(0, SeekOrigin.Begin); + + var encryptedStream = new MemoryStream(); + + await stream.EncryptTo(encryptedStream, key); + encryptedStream.Seek(0, SeekOrigin.Begin); + + File.Delete(largeFileDecryptedPath); + await using var decryptedStream = new FileStream(largeFileDecryptedPath, FileMode.OpenOrCreate, FileAccess.Write); + await encryptedStream.DecryptTo(decryptedStream, key); + + encryptedStream.Close(); + stream.Close(); + decryptedStream.Close(); + + await CompareFiles(largeFilePath, largeFileDecryptedPath); + File.Delete(largeFilePath); + File.Delete(largeFileDecryptedPath); + } + + private async Task CompareFiles(string largeFilePath, string largeFileDecryptedPath) + { + await using var stream1 = new FileStream(largeFilePath, FileMode.Open, FileAccess.Read); + await using var stream2 = new FileStream(largeFileDecryptedPath, FileMode.Open, FileAccess.Read); + + var sha256Bytes1 = Encoding.UTF8.GetString(await SHA256.HashDataAsync(stream1)); + var sha256Bytes2 = Encoding.UTF8.GetString(await SHA256.HashDataAsync(stream2)); + sha256Bytes1.Should().Be(sha256Bytes2); + } + + private async Task CreateLargeFile(string largeTxtPath) + { + var max = 4000000; + File.Delete(largeTxtPath); + var stream = new FileStream(largeTxtPath, FileMode.OpenOrCreate, FileAccess.ReadWrite); + var numbersList = Enumerable.Range(1, max).Chunk(100000); + foreach (var numbers in numbersList) + { + var dict = numbers.ToDictionary(x => x, _ => DateTime.UtcNow); + var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(dict, Formatting.Indented)); + await stream.WriteAsync(bytes); + Console.WriteLine($"Writing numbers from {(numbers.FirstOrDefault()*100 / (double)max):F1} %"); + } + await stream.FlushAsync(); + return stream; + } + private static string StreamToString(Stream stream) { stream.Position = 0;