Skip to content

Commit

Permalink
Added stream support
Browse files Browse the repository at this point in the history
  • Loading branch information
SommerEngineering committed Jan 5, 2020
1 parent fa4ce31 commit b803611
Show file tree
Hide file tree
Showing 4 changed files with 346 additions and 0 deletions.
139 changes: 139 additions & 0 deletions Encrypter Tests/EncrypterTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
Expand Down Expand Up @@ -200,5 +203,141 @@ public async Task TestChangedPasswordBehaviour()
Assert.That(true);
}
}

[Test]
public async Task TestSimpleStream()
{
var message = "This is a test with umlauts äüö.";
var tempSourceFile = Path.GetTempFileName();
var tempDestFile = Path.GetTempFileName();
var tempFinalFile = Path.GetTempFileName();
var password = "test password";

try
{
await File.WriteAllTextAsync(tempSourceFile, message);
await CryptoProcessor.EncryptStream(File.OpenRead(tempSourceFile), File.OpenWrite(tempDestFile), password);
await CryptoProcessor.DecryptStream(File.OpenRead(tempDestFile), File.OpenWrite(tempFinalFile), password);

Assert.That(File.Exists(tempDestFile), Is.True);
Assert.That(File.Exists(tempFinalFile), Is.True);
Assert.That(File.ReadAllText(tempFinalFile), Is.EqualTo(message));
}
finally
{
try
{
File.Delete(tempSourceFile);
}
catch
{
}

try
{
File.Delete(tempDestFile);
}
catch
{
}

try
{
File.Delete(tempFinalFile);
}
catch
{
}
}
}

[Test]
public async Task Test32GBStream()
{
var tempSourceFile = Path.GetTempFileName();
var tempDestFile = Path.GetTempFileName();
var tempFinalFile = Path.GetTempFileName();
var password = "test password";

try
{
// Write 32 GB random data:
await using (var stream = File.OpenWrite(tempSourceFile))
{
var rnd = new Random();
var buffer = new byte[512_000];
var iterations = 32_000_000_000 / buffer.Length;
for(var n=0; n < iterations; n++)
{
rnd.NextBytes(buffer);
await stream.WriteAsync(buffer);
}
}

var fileInfoSource = new FileInfo(tempSourceFile);
Assert.That(fileInfoSource.Length, Is.EqualTo(32_000_000_000));

await CryptoProcessor.EncryptStream(File.OpenRead(tempSourceFile), File.OpenWrite(tempDestFile), password);
await CryptoProcessor.DecryptStream(File.OpenRead(tempDestFile), File.OpenWrite(tempFinalFile), password);

Assert.That(File.Exists(tempDestFile), Is.True);
Assert.That(File.Exists(tempFinalFile), Is.True);

var fileInfoEncrypted = new FileInfo(tempDestFile);
var fileInfoFinal = new FileInfo(tempFinalFile);

Assert.That(fileInfoEncrypted.Length, Is.GreaterThan(32_000_000_000));
Assert.That(fileInfoFinal.Length, Is.EqualTo(fileInfoSource.Length));

var identical = true;
await using (var sourceStream = File.OpenRead(tempSourceFile))
{
await using var finalStream = File.OpenRead(tempFinalFile);

var bufferSource = new byte[512_000];
var bufferFinal = new byte[512_000];
var iterations = 32_000_000_000 / bufferSource.Length;
for (var n = 0; n < iterations; n++)
{
await sourceStream.ReadAsync(bufferSource, 0, bufferSource.Length);
await finalStream.ReadAsync(bufferFinal, 0, bufferFinal.Length);

if (!bufferSource.SequenceEqual(bufferFinal))
{
identical = false;
break;
}
}
}

Assert.That(identical, Is.True);
}
finally
{
try
{
File.Delete(tempSourceFile);
}
catch
{
}

try
{
File.Delete(tempDestFile);
}
catch
{
}

try
{
File.Delete(tempFinalFile);
}
catch
{
}
}
}
}
}
142 changes: 142 additions & 0 deletions Encrypter/CryptoProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,78 @@ await Task.Run(() =>
return Encoding.ASCII.GetString(encryptedAndEncodedData.GetBuffer()[..(int)encryptedAndEncodedData.Length]);
}

/// <summary>
/// Encrypts a given input stream and writes the encrypted data to the provided output stream. A buffer stream
/// gets used in front of the output stream. This method expects, that both streams are read-to-use e.g. the
/// input stream is at the desired position and the output stream is writable, etc. This method disposes the
/// internal crypto streams. Thus, the input and output streams might get disposed as well. Please note, that
/// this method writes binary data without e.g. base64 encoding.
///
/// When the task finished, the entire encryption of the input stream is done.
/// </summary>
/// <param name="inputStream">The desired input stream. The encryption starts at the current position.</param>
/// <param name="outputStream">The desired output stream. The encrypted data gets written to the current position.</param>
/// <param name="password">The encryption password.</param>
/// <param name="iterations">The desired number of iterations to create the key. Should not be adjusted. The default is secure for the current time.</param>
public static async Task EncryptStream(Stream inputStream, Stream outputStream, string password, int iterations = ITERATIONS_YEAR_2020)
{
if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
throw new CryptographicException("The password was empty or shorter than 6 characters.");

if (inputStream == null)
throw new CryptographicException("The input stream cannot be null.");

if (outputStream == null)
throw new CryptographicException("The output stream cannot be null.");

// Generate new random salt:
var saltBytes = Guid.NewGuid().ToByteArray();

// Derive key and iv vector:
var key = new byte[32];
var iv = new byte[16];

// The following operations take several seconds. Thus, using a task:
await Task.Run(() =>
{
using var keyVectorObj = new Rfc2898DeriveBytes(password, saltBytes, iterations, HashAlgorithmName.SHA512);
key = keyVectorObj.GetBytes(32); // the max valid key length = 256 bit = 32 bytes
iv = keyVectorObj.GetBytes(16); // the only valid block size = 128 bit = 16 bytes
});

// Create AES encryption:
using var aes = Aes.Create();
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = iv;

using var encryption = aes.CreateEncryptor();

// A buffer stream for the output:
await using var bufferOutputStream = new BufferedStream(outputStream, 65_536);

// Write the salt into the base64 stream:
await bufferOutputStream.WriteAsync(saltBytes);

// Create the encryption stream:
await using var cryptoStream = new CryptoStream(bufferOutputStream, encryption, CryptoStreamMode.Write);

// Write the payload into the encryption stream:
await inputStream.CopyToAsync(cryptoStream);

// Flush the final block. Please note, that it is not enough to call the regular flush method!
cryptoStream.FlushFinalBlock();

// Clears all sensitive information:
aes.Clear();
Array.Clear(key, 0, key.Length);
Array.Clear(iv, 0, iv.Length);
password = string.Empty;

// Waits for the buffer stream to finish:
await bufferOutputStream.FlushAsync();
}

/// <summary>
/// Decrypts an base64 encoded and encrypted string. Due to the necessary millions of SHA512 iterations,
/// the methods runs at least several seconds in the year 2020 (approx. 5-7s).
Expand Down Expand Up @@ -160,6 +232,76 @@ await Task.Run(() =>
return Encoding.UTF8.GetString(decryptedData.GetBuffer()[..(int)decryptedData.Length]);
}

/// <summary>
/// Decrypts a given input stream and writes the decrypted data to the provided output stream. A buffer stream
/// gets used in front of the output stream. This method expects, that both streams are read-to-use e.g. the
/// input stream is at the desired position and the output stream is writable, etc. This method disposes the
/// internal crypto streams. Thus, the input and output streams might get disposed as well. Please note, that
/// this method writes binary data without e.g. base64 encoding.
///
/// When the task finished, the entire decryption of the input stream is done.
/// </summary>
/// <param name="inputStream">The desired input stream. The decryption starts at the current position.</param>
/// <param name="outputStream">The desired output stream. The decrypted data gets written to the current position.</param>
/// <param name="password">The encryption password.</param>
/// <param name="iterations">The desired number of iterations to create the key. Should not be adjusted. The default is secure for the current time.</param>
public static async Task DecryptStream(Stream inputStream, Stream outputStream, string password, int iterations = ITERATIONS_YEAR_2020)
{
if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
throw new CryptographicException("The password was empty or shorter than 6 characters.");

if (inputStream == null)
throw new CryptographicException("The input stream cannot be null.");

if (outputStream == null)
throw new CryptographicException("The output stream cannot be null.");

// A buffer for the salt's bytes:
var saltBytes = new byte[16]; // 16 bytes = Guid

// Read the salt's bytes out of the stream:
await inputStream.ReadAsync(saltBytes, 0, saltBytes.Length);

// Derive key and iv vector:
var key = new byte[32];
var iv = new byte[16];

// The following operations take several seconds. Thus, using a task:
await Task.Run(() =>
{
using var keyVectorObj = new Rfc2898DeriveBytes(password, saltBytes, iterations, HashAlgorithmName.SHA512);
key = keyVectorObj.GetBytes(32); // the max valid key length = 256 bit = 32 bytes
iv = keyVectorObj.GetBytes(16); // the only valid block size = 128 bit = 16 bytes
});

// Create AES decryption:
using var aes = Aes.Create();
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = iv;

using var decryption = aes.CreateDecryptor();

// The crypto stream:
await using var cryptoStream = new CryptoStream(inputStream, decryption, CryptoStreamMode.Read);

// Create a buffer stream in front of the output stream:
await using var bufferOutputStream = new BufferedStream(outputStream);

// Reads all remaining data trough the decrypt stream. Note, that this operation
// starts at the current position, i.e. after the salt bytes:
await cryptoStream.CopyToAsync(bufferOutputStream);

// Clears all sensitive information:
aes.Clear();
Array.Clear(key, 0, key.Length);
Array.Clear(iv, 0, iv.Length);
password = string.Empty;

// Waits for the buffer stream to finish:
await bufferOutputStream.FlushAsync();
}

/// <summary>
/// Upgrades the encryption regarding the used iterations for the key.
/// </summary>
Expand Down
30 changes: 30 additions & 0 deletions Encrypter/Encrypter.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b803611

Please sign in to comment.