Open
Description
I am trying to implement encrypted storing of log files. However this is currently not possible with the FileLifecycleHooks class as the underlyingStream
parameter was opened with FileAccess.Write. I'm trying to get it to work with AES in CBC mode with padding, which requires re-reading of the last block because of the padding if an existing log file is opened.
Would it be possible to open the log files with read/write access in this library?
Below is a minimal example which describes the situation:
using Serilog;
using Serilog.Sinks.File;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace TestCryptoHooks
{
public class Program
{
// Adapted from: https://stackoverflow.com/a/30864169
public static string ReadStringFromFile(string fileName, byte[] key, byte[] iv)
{
string plainText;
using (Rijndael algo = Rijndael.Create())
{
algo.Key = key;
algo.IV = iv;
algo.Mode = CipherMode.CBC;
algo.Padding = PaddingMode.PKCS7;
// Create the streams used for decryption.
using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read))
// Create a decrytor to perform the stream transform.
using (ICryptoTransform decryptor = algo.CreateDecryptor())
using (CryptoStream cs = new CryptoStream(file, decryptor, CryptoStreamMode.Read))
using (StreamReader sr = new StreamReader(cs))
{
// Read all data from the stream.
plainText = sr.ReadToEnd();
}
}
return plainText;
}
public class CryptoHooks : FileLifecycleHooks
{
private byte[] _key;
private byte[] _iv;
public CryptoHooks(byte[] key, byte[] iv)
{
_key = key;
_iv = iv;
}
public override Stream OnFileOpened(Stream underlyingStream, Encoding _)
{
var algo = Rijndael.Create();
algo.Key = _key;
algo.IV = _iv;
algo.Mode = CipherMode.CBC;
algo.Padding = PaddingMode.PKCS7;
byte[] previous = null;
int previousLength = 0;
long length = underlyingStream.Length;
if (length != 0)
{
// The IV length is equal to the block length
byte[] block = new byte[_iv.Length];
if (length >= _iv.Length * 2)
{
// At least 2 blocks, take the penultimate block
// as the IV
underlyingStream.Position = length - _iv.Length * 2;
underlyingStream.Read(block, 0, block.Length);
algo.IV = block;
}
else
{
// A single block present, use the IV given
underlyingStream.Position = length - _iv.Length;
algo.IV = _iv;
}
// Read the last block
underlyingStream.Read(block, 0, block.Length);
// And reposition at the beginning of the last block
underlyingStream.Position = length - _iv.Length;
// We use a MemoryStream because the CryptoStream
// will close the Stream at the end
using (var ms = new MemoryStream(block))
// Create a decrytor to perform the stream transform.
using (ICryptoTransform decryptor = algo.CreateDecryptor())
using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
{
// Read all data from the stream. The decrypted last
// block can be long up to block length characters
// (so up to iv.Length) (this with AES + CBC)
previous = new byte[_iv.Length];
previousLength = cs.Read(previous, 0, previous.Length);
}
}
else
{
// Use the IV given
algo.IV = _iv;
}
var outStream = new CryptoStream(underlyingStream, algo.CreateEncryptor(), CryptoStreamMode.Write);
// Rewrite the last block, if present. We even skip
// the case of block present but empty
if (previousLength != 0)
{
outStream.Write(previous, 0, previousLength);
}
return outStream;
}
}
static void Main(string[] args)
{
var key = Encoding.UTF8.GetBytes("Simple key");
var iv = Encoding.UTF8.GetBytes("Simple IV");
Array.Resize(ref key, 128 / 8);
Array.Resize(ref iv, 128 / 8);
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.File("file.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 31, hooks: new CryptoHooks(key, iv)))
.CreateLogger();
Log.Information("This will be written to disk on the worker thread");
Log.CloseAndFlush();
string plainText = ReadStringFromFile("file20200602.log", key, iv);
Console.WriteLine(plainText);
Console.ReadLine();
}
}
}