Skip to content

Commit

Permalink
Merge pull request #18 from PandaTechAM/development
Browse files Browse the repository at this point in the history
Zip and Aes stream overload
  • Loading branch information
HaikAsatryan authored May 9, 2024
2 parents d471a4b + a22b887 commit 68d0754
Show file tree
Hide file tree
Showing 18 changed files with 265 additions and 30 deletions.
6 changes: 2 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ on:
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
dotnet-version: [ '8.x.x' ]

steps:
- name: Checkout
uses: actions/checkout@v3

- 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 }}
Expand Down
18 changes: 17 additions & 1 deletion Pandatech.Crypto.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
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
6 changes: 6 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "8.0.300",
"rollForward": "latestMajor"
}
}
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
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));
}
}
File renamed without changes.
Loading

0 comments on commit 68d0754

Please sign in to comment.