Skip to content

Commit

Permalink
Zip and Aes stream overload
Browse files Browse the repository at this point in the history
  • Loading branch information
HaikAsatryan committed May 9, 2024
1 parent 8130b93 commit a22b887
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 24 deletions.
30 changes: 30 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
50 changes: 41 additions & 9 deletions src/Pandatech.Crypto/Aes256.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte>();
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<byte>();
if (string.IsNullOrEmpty(plainText)) return [];
return addHashToBytes ? EncryptWithHash(plainText, key) : Encrypt(plainText, key);
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -101,6 +113,26 @@ private string DecryptIgnoringHash(IEnumerable<byte> 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)
{
Expand Down
18 changes: 15 additions & 3 deletions src/Pandatech.Crypto/GZip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
using System.Text;
using System.Text.Json;



namespace Pandatech.Crypto;

public static class GZip
Expand All @@ -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>(T obj)
{
var jsonString = JsonSerializer.Serialize(obj, JsonSerializerOptions);
Expand Down Expand Up @@ -51,7 +63,7 @@ public static byte[] Compress(byte[] data)
var jsonString = Encoding.UTF8.GetString(decompressed);
return JsonSerializer.Deserialize<T>(jsonString, JsonSerializerOptions);
}

public static T? Decompress<T>(string compressedData)
{
var decompressed = Decompress(compressedData);
Expand Down
6 changes: 3 additions & 3 deletions src/Pandatech.Crypto/Pandatech.Crypto.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
<Copyright>MIT</Copyright>
<PackageIcon>pandatech.png</PackageIcon>
<PackageReadmeFile>Readme.md</PackageReadmeFile>
<Version>2.3.1</Version>
<Version>2.3.2</Version>
<Title>Pandatech.Crypto</Title>
<PackageTags>Pandatech, library, encryption, hash, algorythms, security</PackageTags>
<Description>PandaTech.Crypto is a .NET library simplifying common cryptograhic functions.</Description>
<RepositoryUrl>https://github.com/PandaTechAM/be-lib-pandatech-crypto</RepositoryUrl>
<PackageReleaseNotes>Gzip serializer fix</PackageReleaseNotes>
<PackageReleaseNotes>Zip and Aes stream overload</PackageReleaseNotes>
</PropertyGroup>

<ItemGroup>
Expand All @@ -25,7 +25,7 @@
<PackageReference Include="BouncyCastle.NetCore" Version="2.2.1" />
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Pandatech.RegexBox" Version="1.2.3" />
<PackageReference Include="Pandatech.RegexBox" Version="1.2.4" />
</ItemGroup>

</Project>
3 changes: 1 addition & 2 deletions src/Pandatech.Crypto/Sha3.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Security.Cryptography;
using System.Text;
using System.Text;
using Org.BouncyCastle.Crypto.Digests;

namespace Pandatech.Crypto;
Expand Down
79 changes: 76 additions & 3 deletions test/Pandatech.Crypto.Tests/Aes256Tests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text;

namespace Pandatech.Crypto.Tests;

public class Aes256Tests
Expand Down Expand Up @@ -108,7 +110,7 @@ public void EncryptDecryptWithShortKey_ShouldThrowException()
const string original = "MySensitiveData";

Assert.Throws<ArgumentException>(() => aes256.Encrypt(original, shortKey));
Assert.Throws<ArgumentException>(() => aes256.Decrypt(Array.Empty<byte>(), shortKey));
Assert.Throws<ArgumentException>(() => aes256.Decrypt([], shortKey));
}

[Fact]
Expand All @@ -128,9 +130,9 @@ public void EncryptDecryptWithNullCipher_ShouldReturnEmptyString()
var aes256 = new Aes256(new Aes256Options());
var key = Random.GenerateAes256KeyString();

Assert.Equal("", aes256.Decrypt(Array.Empty<byte>(), key));
Assert.Equal("", aes256.Decrypt([], key));
}

[Fact]
public void GenerateAes256KeyIsValidInLoop()
{
Expand All @@ -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<ArgumentException>(() => 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<ArgumentException>(() => aes256.DecryptStream(inputStream, outputStream, invalidKey));
}
}
71 changes: 70 additions & 1 deletion test/Pandatech.Crypto.Tests/GZipTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Text;
using System.Text.Json.Serialization;

namespace Pandatech.Crypto.Tests;

Expand All @@ -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<InvalidDataException>(() => 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()
Expand Down
Loading

0 comments on commit a22b887

Please sign in to comment.