diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1eb76aa..09a6766 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,9 +13,7 @@ on: jobs: deploy: runs-on: ubuntu-latest - strategy: - matrix: - dotnet-version: [ '8.x.x' ] + steps: - name: Checkout uses: actions/checkout@v3 @@ -23,7 +21,7 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v3 with: - dotnet-version: ${{ matrix.dotnet-version }} + global-json-file: global.json - name: Build run: dotnet build ${{ env.PROJECT_PATH }} diff --git a/Pandatech.Crypto.sln b/Pandatech.Crypto.sln index 9caca42..00df5ab 100644 --- a/Pandatech.Crypto.sln +++ b/Pandatech.Crypto.sln @@ -3,10 +3,22 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.34024.191 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pandatech.Crypto.Tests", "src/Pandatech.Crypto.Tests\Pandatech.Crypto.Tests.csproj", "{89D6535F-549C-4091-BF21-96565F098C3F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pandatech.Crypto.Tests", "test/Pandatech.Crypto.Tests\Pandatech.Crypto.Tests.csproj", "{89D6535F-549C-4091-BF21-96565F098C3F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pandatech.Crypto", "src/Pandatech.Crypto\Pandatech.Crypto.csproj", "{97B88123-20B1-4F0A-82E7-DFDD08A03B7C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D64571FB-E682-4D77-A631-DAD8E614F284}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{66D5292A-E8D3-42E6-923B-20F6908105D4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{88D1AD3C-583A-4F7C-84B9-504613641C5E}" + ProjectSection(SolutionItems) = preProject + .github\workflows\main.yml = .github\workflows\main.yml + .gitignore = .gitignore + Readme.md = Readme.md + global.json = global.json + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,4 +40,8 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D31D47F1-8D94-40DB-AEEA-9AF87866DECC} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {97B88123-20B1-4F0A-82E7-DFDD08A03B7C} = {D64571FB-E682-4D77-A631-DAD8E614F284} + {89D6535F-549C-4091-BF21-96565F098C3F} = {66D5292A-E8D3-42E6-923B-20F6908105D4} + EndGlobalSection EndGlobal diff --git a/Readme.md b/Readme.md index 3bd6b2b..039bc14 100644 --- a/Readme.md +++ b/Readme.md @@ -11,6 +11,7 @@ - [1.4.2.2. Encryption/Decryption methods with hashing](#1422-encryptiondecryption-methods-with-hashing) - [1.4.2.3. Encryption/Decryption methods without hashing](#1423-encryptiondecryption-methods-without-hashing) - [1.4.2.4. Encryption/Decryption methods with custom key (overriding options for one time)](#1424-encryptiondecryption-methods-with-custom-key-overriding-options-for-one-time) + - [1.4.2.5. Stream-based Encryption/Decryption methods](#1425-stream-based-encryptiondecryption-methods) - [1.4.3. Argon2id Class](#143-argon2id-class) - [1.4.3.1. Default Configurations](#1431-default-configurations) - [1.4.3.2 Hash password and verify hash](#1432-hash-password-and-verify-hash) @@ -111,6 +112,21 @@ string customKey = "your-custom-base64-encoded-key"; byte[] cipherText = aes256.Encrypt("your-plaintext", customKey); string plainText = aes256.Decrypt(cipherText, customKey); ``` +#### 1.4.2.5. Stream-based Encryption/Decryption methods + +The AES256 class also supports stream-based operations, allowing for encryption and decryption directly on streams, which is ideal for handling large files or data streams efficiently. + +```csharp +using var inputStream = new MemoryStream(Encoding.UTF8.GetBytes("your-plaintext")); +using var outputStream = new MemoryStream(); +aes256.EncryptStream(inputStream, outputStream, "your-custom-base64-encoded-key"); +byte[] encryptedBytes = outputStream.ToArray(); + +using var inputStream = new MemoryStream(encryptedBytes); +using var outputStream = new MemoryStream(); +aes256.DecryptStream(inputStream, outputStream, "your-custom-base64-encoded-key"); +string decryptedText = Encoding.UTF8.GetString(outputStream.ToArray()); +``` ### 1.4.3. Argon2id Class @@ -182,6 +198,20 @@ byte[] compressedData = GZip.Compress(data); string decompressedData = Encoding.UTF8.GetString(GZip.Decompress(compressedData)); ``` +Example usage for compressing and decompressing with streams: + +```csharp +using var inputStream = new MemoryStream(Encoding.UTF8.GetBytes("Sample Data")); +using var compressedStream = new MemoryStream(); +GZip.Compress(inputStream, compressedStream); +byte[] compressedData = compressedStream.ToArray(); + +using var inputStream = new MemoryStream(compressedData); +using var decompressedStream = new MemoryStream(); +GZip.Decompress(inputStream, decompressedStream); +string decompressedData = Encoding.UTF8.GetString(decompressedStream.ToArray()); +``` + ### 1.4.8. Mask Class The `Mask` class in the PandaTech.Crypto library provides methods to mask sensitive information like email addresses and diff --git a/global.json b/global.json new file mode 100644 index 0000000..f5c13f2 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.300", + "rollForward": "latestMajor" + } +} diff --git a/src/Pandatech.Crypto/Aes256.cs b/src/Pandatech.Crypto/Aes256.cs index d4c8bfb..cc4b019 100644 --- a/src/Pandatech.Crypto/Aes256.cs +++ b/src/Pandatech.Crypto/Aes256.cs @@ -2,28 +2,23 @@ namespace Pandatech.Crypto; -public class Aes256 +public class Aes256(Aes256Options options) { - private readonly Aes256Options _options; + private readonly Aes256Options _options = options ?? throw new ArgumentNullException(nameof(options)); private const int KeySize = 256; private const int IvSize = 16; private const int HashSize = 64; - public Aes256(Aes256Options options) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - public byte[] Encrypt(string? plainText, bool addHashToBytes = true) { - if (string.IsNullOrEmpty(plainText)) return Array.Empty(); + if (string.IsNullOrEmpty(plainText)) return []; return addHashToBytes ? EncryptWithHash(plainText) : Encrypt(plainText); } public byte[] Encrypt(string? plainText, string key, bool addHashToBytes = true) { ValidateKey(key); - if (string.IsNullOrEmpty(plainText)) return Array.Empty(); + if (string.IsNullOrEmpty(plainText)) return []; return addHashToBytes ? EncryptWithHash(plainText, key) : Encrypt(plainText, key); } @@ -64,6 +59,23 @@ private byte[] Encrypt(string plainText, string? key) var result = aesAlg.IV.Concat(encryptedPasswordByte).ToArray(); return result; } + + public void EncryptStream(Stream inputStream, Stream outputStream, string? key = null) + { + key ??= _options.Key; + ValidateKey(key); + using var aesAlg = Aes.Create(); + aesAlg.KeySize = KeySize; + aesAlg.Padding = PaddingMode.PKCS7; + aesAlg.Key = Convert.FromBase64String(key); + aesAlg.GenerateIV(); + + outputStream.Write(aesAlg.IV, 0, aesAlg.IV.Length); + + using var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); + using var cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write, leaveOpen: true); + inputStream.CopyTo(cryptoStream); + } private string Decrypt(byte[] cipherText, string? key) @@ -101,6 +113,26 @@ private string DecryptIgnoringHash(IEnumerable cipherTextWithHash, string? var cipherText = cipherTextWithHash.Skip(HashSize).ToArray(); return Decrypt(cipherText, key); } + + public void DecryptStream(Stream inputStream, Stream outputStream, string? key = null) + { + key ??= _options.Key; + ValidateKey(key); + + var iv = new byte[IvSize]; + if (inputStream.Read(iv, 0, IvSize) != IvSize) + throw new ArgumentException("Input stream does not contain a complete IV."); + + using var aesAlg = Aes.Create(); + aesAlg.KeySize = KeySize; + aesAlg.Padding = PaddingMode.PKCS7; + aesAlg.Key = Convert.FromBase64String(key); + aesAlg.IV = iv; + + using var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); + using var cryptoStream = new CryptoStream(inputStream, decryptor, CryptoStreamMode.Read, leaveOpen: true); + cryptoStream.CopyTo(outputStream); + } private static void ValidateKey(string key) { diff --git a/src/Pandatech.Crypto/GZip.cs b/src/Pandatech.Crypto/GZip.cs index 598fdbd..e1d8289 100644 --- a/src/Pandatech.Crypto/GZip.cs +++ b/src/Pandatech.Crypto/GZip.cs @@ -2,8 +2,6 @@ using System.Text; using System.Text.Json; - - namespace Pandatech.Crypto; public static class GZip @@ -12,6 +10,20 @@ public static class GZip { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; + + public static void Compress(Stream inputStream, Stream outputStream) + { + using var gzipStream = new GZipStream(outputStream, CompressionMode.Compress, leaveOpen: true); + inputStream.CopyTo(gzipStream); + } + + // New method to decompress data directly from one stream to another + public static void Decompress(Stream inputStream, Stream outputStream) + { + using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress, leaveOpen: true); + gzipStream.CopyTo(outputStream); + } + public static byte[] Compress(T obj) { var jsonString = JsonSerializer.Serialize(obj, JsonSerializerOptions); @@ -51,7 +63,7 @@ public static byte[] Compress(byte[] data) var jsonString = Encoding.UTF8.GetString(decompressed); return JsonSerializer.Deserialize(jsonString, JsonSerializerOptions); } - + public static T? Decompress(string compressedData) { var decompressed = Decompress(compressedData); diff --git a/src/Pandatech.Crypto/Pandatech.Crypto.csproj b/src/Pandatech.Crypto/Pandatech.Crypto.csproj index f61e7ba..b9556d1 100644 --- a/src/Pandatech.Crypto/Pandatech.Crypto.csproj +++ b/src/Pandatech.Crypto/Pandatech.Crypto.csproj @@ -8,12 +8,12 @@ MIT pandatech.png Readme.md - 2.3.1 + 2.3.2 Pandatech.Crypto Pandatech, library, encryption, hash, algorythms, security PandaTech.Crypto is a .NET library simplifying common cryptograhic functions. https://github.com/PandaTechAM/be-lib-pandatech-crypto - Gzip serializer fix + Zip and Aes stream overload @@ -25,7 +25,7 @@ - + diff --git a/src/Pandatech.Crypto/Sha3.cs b/src/Pandatech.Crypto/Sha3.cs index 884f3d5..22b6a14 100644 --- a/src/Pandatech.Crypto/Sha3.cs +++ b/src/Pandatech.Crypto/Sha3.cs @@ -1,5 +1,4 @@ -using System.Security.Cryptography; -using System.Text; +using System.Text; using Org.BouncyCastle.Crypto.Digests; namespace Pandatech.Crypto; diff --git a/src/Pandatech.Crypto.Tests/Aes256Tests.cs b/test/Pandatech.Crypto.Tests/Aes256Tests.cs similarity index 64% rename from src/Pandatech.Crypto.Tests/Aes256Tests.cs rename to test/Pandatech.Crypto.Tests/Aes256Tests.cs index e9680c3..7289640 100644 --- a/src/Pandatech.Crypto.Tests/Aes256Tests.cs +++ b/test/Pandatech.Crypto.Tests/Aes256Tests.cs @@ -1,3 +1,5 @@ +using System.Text; + namespace Pandatech.Crypto.Tests; public class Aes256Tests @@ -108,7 +110,7 @@ public void EncryptDecryptWithShortKey_ShouldThrowException() const string original = "MySensitiveData"; Assert.Throws(() => aes256.Encrypt(original, shortKey)); - Assert.Throws(() => aes256.Decrypt(Array.Empty(), shortKey)); + Assert.Throws(() => aes256.Decrypt([], shortKey)); } [Fact] @@ -128,9 +130,9 @@ public void EncryptDecryptWithNullCipher_ShouldReturnEmptyString() var aes256 = new Aes256(new Aes256Options()); var key = Random.GenerateAes256KeyString(); - Assert.Equal("", aes256.Decrypt(Array.Empty(), key)); + Assert.Equal("", aes256.Decrypt([], key)); } - + [Fact] public void GenerateAes256KeyIsValidInLoop() { @@ -145,4 +147,75 @@ public void GenerateAes256KeyIsValidInLoop() Assert.Equal("MySensitiveData", decrypt); } } + + [Fact] + public void EncryptDecryptStream_ShouldReturnOriginalData() + { + // Arrange + var aes256Options = new Aes256Options { Key = Random.GenerateAes256KeyString() }; + var aes256 = new Aes256(aes256Options); + const string originalData = "MySensitiveData"; + var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(originalData)); + var outputStream = new MemoryStream(); + + // Act + aes256.EncryptStream(inputStream, outputStream, aes256Options.Key); + outputStream.Seek(0, SeekOrigin.Begin); + + var resultStream = new MemoryStream(); + aes256.DecryptStream(outputStream, resultStream, aes256Options.Key); + resultStream.Seek(0, SeekOrigin.Begin); + var decryptedData = new StreamReader(resultStream).ReadToEnd(); + + // Assert + Assert.Equal(originalData, decryptedData); + } + + [Fact] + public void EncryptDecryptStreamWithEmptyContent_ShouldHandleGracefully() + { + // Arrange + var aes256Options = new Aes256Options { Key = Random.GenerateAes256KeyString() }; + var aes256 = new Aes256(aes256Options); + var inputStream = new MemoryStream(); + var outputStream = new MemoryStream(); + + // Act + aes256.EncryptStream(inputStream, outputStream, aes256Options.Key); + outputStream.Seek(0, SeekOrigin.Begin); // Reset the position for reading. + + var resultStream = new MemoryStream(); + aes256.DecryptStream(outputStream, resultStream, aes256Options.Key); + resultStream.Seek(0, SeekOrigin.Begin); + var decryptedData = new StreamReader(resultStream).ReadToEnd(); + + // Assert + Assert.Empty(decryptedData); + } + + [Fact] + public void EncryptStreamWithInvalidKey_ShouldThrowException() + { + // Arrange + var aes256 = new Aes256(new Aes256Options()); + const string invalidKey = "InvalidKey"; + var inputStream = new MemoryStream(Encoding.UTF8.GetBytes("MySensitiveData")); + var outputStream = new MemoryStream(); + + // Act & Assert + Assert.Throws(() => aes256.EncryptStream(inputStream, outputStream, invalidKey)); + } + + [Fact] + public void DecryptStreamWithInvalidKey_ShouldThrowException() + { + // Arrange + var aes256 = new Aes256(new Aes256Options()); + const string invalidKey = "InvalidKey"; + var inputStream = new MemoryStream(); + var outputStream = new MemoryStream(); + + // Act & Assert + Assert.Throws(() => aes256.DecryptStream(inputStream, outputStream, invalidKey)); + } } \ No newline at end of file diff --git a/src/Pandatech.Crypto.Tests/Argon2IdTests.cs b/test/Pandatech.Crypto.Tests/Argon2IdTests.cs similarity index 100% rename from src/Pandatech.Crypto.Tests/Argon2IdTests.cs rename to test/Pandatech.Crypto.Tests/Argon2IdTests.cs diff --git a/src/Pandatech.Crypto.Tests/GZipTests.cs b/test/Pandatech.Crypto.Tests/GZipTests.cs similarity index 61% rename from src/Pandatech.Crypto.Tests/GZipTests.cs rename to test/Pandatech.Crypto.Tests/GZipTests.cs index 8ccafdb..23ba0d1 100644 --- a/src/Pandatech.Crypto.Tests/GZipTests.cs +++ b/test/Pandatech.Crypto.Tests/GZipTests.cs @@ -1,5 +1,4 @@ using System.Text; -using System.Text.Json.Serialization; namespace Pandatech.Crypto.Tests; @@ -10,6 +9,76 @@ private class TestClass public int SomeLongId { get; init; } public string? FullName { get; init; } } + + [Fact] + public void CompressDecompressStream_ShouldReturnOriginalData() + { + // Arrange + var originalData = "MySensitiveData"; + var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(originalData)); + var compressedStream = new MemoryStream(); + var decompressedStream = new MemoryStream(); + + // Act - Compress + GZip.Compress(inputStream, compressedStream); + compressedStream.Seek(0, SeekOrigin.Begin); // Reset stream position for reading + + // Act - Decompress + GZip.Decompress(compressedStream, decompressedStream); + decompressedStream.Seek(0, SeekOrigin.Begin); // Reset stream position for reading + var resultData = new StreamReader(decompressedStream).ReadToEnd(); + + // Assert + Assert.Equal(originalData, resultData); + } + + [Fact] + public void CompressStream_ShouldReduceSizeForCompressibleData() + { + // Arrange + var originalData = new string('a', 1024); // Highly compressible data + var inputStream = new MemoryStream(Encoding.UTF8.GetBytes(originalData)); + var compressedStream = new MemoryStream(); + + // Act + GZip.Compress(inputStream, compressedStream); + + // Assert + Assert.True(compressedStream.Length < inputStream.Length); + } + + [Fact] + public void DecompressStream_WithCorruptedData_ShouldThrow() + { + // Arrange + var corruptedData = new byte[] { 0x0, 0x1, 0x2, 0x3 }; // Not valid compressed data + var inputStream = new MemoryStream(corruptedData); + var decompressedStream = new MemoryStream(); + + // Act & Assert + Assert.Throws(() => GZip.Decompress(inputStream, decompressedStream)); + } + + [Fact] + public void CompressDecompressEmptyStream_ShouldHandleGracefully() + { + // Arrange + var emptyStream = new MemoryStream(); + var compressedStream = new MemoryStream(); + var decompressedStream = new MemoryStream(); + + // Act + GZip.Compress(emptyStream, compressedStream); + compressedStream.Seek(0, SeekOrigin.Begin); // Reset the compressed stream position for reading + + // Act + GZip.Decompress(compressedStream, decompressedStream); + decompressedStream.Seek(0, SeekOrigin.Begin); // Reset the decompressed stream position for reading + var resultData = new StreamReader(decompressedStream).ReadToEnd(); + + // Assert + Assert.Empty(resultData); + } [Fact] public void CompressAndDecompress_ShouldReturnOriginalObject() diff --git a/src/Pandatech.Crypto.Tests/GlobalUsings.cs b/test/Pandatech.Crypto.Tests/GlobalUsings.cs similarity index 100% rename from src/Pandatech.Crypto.Tests/GlobalUsings.cs rename to test/Pandatech.Crypto.Tests/GlobalUsings.cs diff --git a/src/Pandatech.Crypto.Tests/HostBuilderTests.cs b/test/Pandatech.Crypto.Tests/HostBuilderTests.cs similarity index 100% rename from src/Pandatech.Crypto.Tests/HostBuilderTests.cs rename to test/Pandatech.Crypto.Tests/HostBuilderTests.cs diff --git a/src/Pandatech.Crypto.Tests/MaskTests.cs b/test/Pandatech.Crypto.Tests/MaskTests.cs similarity index 100% rename from src/Pandatech.Crypto.Tests/MaskTests.cs rename to test/Pandatech.Crypto.Tests/MaskTests.cs diff --git a/src/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj b/test/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj similarity index 85% rename from src/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj rename to test/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj index 1e0d0e2..58c8056 100644 --- a/src/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj +++ b/test/Pandatech.Crypto.Tests/Pandatech.Crypto.Tests.csproj @@ -12,19 +12,19 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/Pandatech.Crypto.Tests/PasswordTests.cs b/test/Pandatech.Crypto.Tests/PasswordTests.cs similarity index 100% rename from src/Pandatech.Crypto.Tests/PasswordTests.cs rename to test/Pandatech.Crypto.Tests/PasswordTests.cs diff --git a/src/Pandatech.Crypto.Tests/RandomTests.cs b/test/Pandatech.Crypto.Tests/RandomTests.cs similarity index 100% rename from src/Pandatech.Crypto.Tests/RandomTests.cs rename to test/Pandatech.Crypto.Tests/RandomTests.cs diff --git a/src/Pandatech.Crypto.Tests/Sha3Tests.cs b/test/Pandatech.Crypto.Tests/Sha3Tests.cs similarity index 100% rename from src/Pandatech.Crypto.Tests/Sha3Tests.cs rename to test/Pandatech.Crypto.Tests/Sha3Tests.cs