diff --git a/Encrypter Tests/EncrypterTests.cs b/Encrypter Tests/EncrypterTests.cs index 6acff19..8355dd1 100644 --- a/Encrypter Tests/EncrypterTests.cs +++ b/Encrypter Tests/EncrypterTests.cs @@ -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; @@ -200,5 +203,141 @@ namespace Encrypter_Tests 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 + { + } + } + } } } diff --git a/Encrypter/CryptoProcessor.cs b/Encrypter/CryptoProcessor.cs index 65ff6f1..52a5aa6 100644 --- a/Encrypter/CryptoProcessor.cs +++ b/Encrypter/CryptoProcessor.cs @@ -88,6 +88,78 @@ namespace Encrypter return Encoding.ASCII.GetString(encryptedAndEncodedData.GetBuffer()[..(int)encryptedAndEncodedData.Length]); } + /// + /// 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. + /// + /// The desired input stream. The encryption starts at the current position. + /// The desired output stream. The encrypted data gets written to the current position. + /// The encryption password. + /// The desired number of iterations to create the key. Should not be adjusted. The default is secure for the current time. + 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(); + } + /// /// 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). @@ -160,6 +232,76 @@ namespace Encrypter return Encoding.UTF8.GetString(decryptedData.GetBuffer()[..(int)decryptedData.Length]); } + /// + /// 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. + /// + /// The desired input stream. The decryption starts at the current position. + /// The desired output stream. The decrypted data gets written to the current position. + /// The encryption password. + /// The desired number of iterations to create the key. Should not be adjusted. The default is secure for the current time. + 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(); + } + /// /// Upgrades the encryption regarding the used iterations for the key. /// diff --git a/Encrypter/Encrypter.xml b/Encrypter/Encrypter.xml index c6bdb52..14cf47d 100644 --- a/Encrypter/Encrypter.xml +++ b/Encrypter/Encrypter.xml @@ -22,6 +22,21 @@ The number of iterations to derive the key. Should not be adjusted. The default is secure for the current time. The base64 encoded and encrypted string. The string is ASCII encoding. + + + 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. + + The desired input stream. The encryption starts at the current position. + The desired output stream. The encrypted data gets written to the current position. + The encryption password. + The desired number of iterations to create the key. Should not be adjusted. The default is secure for the current time. + Decrypts an base64 encoded and encrypted string. Due to the necessary millions of SHA512 iterations, @@ -35,6 +50,21 @@ The number of iterations to derive the key. Should not be adjusted. The default is secure for the current time. The decrypted UTF8 encoded string. + + + 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. + + The desired input stream. The decryption starts at the current position. + The desired output stream. The decrypted data gets written to the current position. + The encryption password. + The desired number of iterations to create the key. Should not be adjusted. The default is secure for the current time. + Upgrades the encryption regarding the used iterations for the key. diff --git a/Encrypter/Extensions.cs b/Encrypter/Extensions.cs index 534a655..3b1ea38 100644 --- a/Encrypter/Extensions.cs +++ b/Encrypter/Extensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Text; using System.Threading.Tasks; @@ -22,6 +23,23 @@ namespace Encrypter return await CryptoProcessor.EncryptString(data, password); } + /// + /// 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. + /// + /// The desired input stream. The encryption starts at the current position. + /// The desired output stream. The encrypted data gets written to the current position. + /// The encryption password. + public static async Task Encrypt(this Stream inputStream, Stream outputStream, string password) + { + await CryptoProcessor.EncryptStream(inputStream, outputStream, password); + } + /// /// 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). @@ -36,5 +54,22 @@ namespace Encrypter { return await CryptoProcessor.DecryptString(data, password); } + + /// + /// 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. + /// + /// The desired input stream. The decryption starts at the current position. + /// The desired output stream. The decrypted data gets written to the current position. + /// The encryption password. + public static async Task Decrypt(this Stream inputStream, Stream outputStream, string password) + { + await CryptoProcessor.DecryptStream(inputStream, outputStream, password); + } } }