0

I am trying to create an AES encryption method where the same file is read and write at the same time. Other codes I have read all create a new file, I don't want to have new file. Other encrypted line matched my reference output from separate output, except my method generated an incorrect last line as plain text plus additional bytes.

I have tried replacing FileStream argument of CryptoStream and remove fs.Seek() at line 58, the program runs correctly and generated new .aes encrypted file for my reference output above.

static void Main(string[] args)
{
    AESCryptFile(@"C:\Users\user\Downloads\test.txt",
        new byte[] { 0x13, 0x11, 0x7F, 0x08, 0x45, 0x2E, 0x96, 0x33 },
        new byte[] { 0x13, 0x11, 0x7F, 0x08, 0x45, 0x2E, 0x96, 0x33 }, false);
    Console.WriteLine("Done");
    Console.ReadLine();
}

public async static void AESCryptFile
    (string path, byte[] key, byte[] iv, bool encrypt)
{
    // validation
    if (path == null || !File.Exists(path))
        throw new FileNotFoundException("Path does not exist");
    if (key == null || key.Length == 0)
        throw new ArgumentNullException("Password is null");
    if (iv == null || iv.Length < 8)
        throw new ArgumentException("IV is null or under 8 bytes long");

    // in and out stream for files
    FileStream fs = new FileStream
        (path, FileMode.Open, FileAccess.ReadWrite, FileShare.None);

    // initialize aes with safe hash and mode
    RijndaelManaged algo = new RijndaelManaged();
    Rfc2898DeriveBytes hashed = new Rfc2898DeriveBytes(key, iv, 25000);
    algo.Key = hashed.GetBytes(32);
    algo.IV = hashed.GetBytes(16);
    algo.Padding = PaddingMode.PKCS7;
    algo.Mode = CipherMode.CFB;

    // mediator stream for cryptography
    CryptoStream cs = new CryptoStream(fs,
        encrypt ? algo.CreateEncryptor() : algo.CreateDecryptor(),
        encrypt ? CryptoStreamMode.Write : CryptoStreamMode.Read);

    // main file transfer and crypto
    await Task.Run(
        () => readCryptWrite(new FileInfo(path).Length, fs, cs, encrypt));
}

private static void readCryptWrite(long fileSize, FileStream fs,
    CryptoStream cs, bool encrypt)
{
    // 1 MB of buffer zone allocation
    byte[] buffer = new byte[1048576];
    int nextBlockSize;
    long processedSize = 0;
    // loop while there are more to read
    while ((nextBlockSize = encrypt ? fs.Read(buffer, 0, buffer.Length) :
        cs.Read(buffer, 0, buffer.Length)) > 0)
    {
        // set pointer back to last written space
        fs.Seek(processedSize, SeekOrigin.Begin);
        // write out to file
        if (encrypt) cs.Write(buffer, 0, nextBlockSize);
        else fs.Write(buffer, 0, nextBlockSize);
        // set progress
        processedSize = fs.Position;
        SetProgress?.Invoke((int)((double)processedSize / fileSize * 100));
        if (nextBlockSize != buffer.Length) break;
    }
    // close streams
    cs.Clear();
    cs.Close();
    fs.Close();
}

The input I used: Long English file

WylieYYYY
  • 3
  • 1
  • 4
  • 3
    Unrelated, but your IV is derived from your key. Don't do this. Your IV needs to be different every time you encrypt something. A common approach is to randomly generate it, and then wire it as the first few bytes if the encrypted file. – canton7 Feb 17 '19 at 13:48
  • You're making an assumption that if you feed the CryptoStream some bytes, it writes the same number of bytes to the file. This is not true: the CryptoStream will buffer internally, and will write data to file when it's completed encrypting a block. I *suspect* you lucked out in that your buffer size is a multiple of your block size, but the padding at the end is messing that up, but I haven't worked out exactly what's going wrong. If you're careful to reset the filestream back to where the CryptoStream left it, not to where you think it should be, this *might* work? – canton7 Feb 17 '19 at 13:54
  • @canton7 Key as IV is just for testing purpose. In regard of setting filestream where cryptostream left it, I tried to access cs.Position but it throws a NotSupportedException. – WylieYYYY Feb 17 '19 at 14:32
  • You'll need to record the position of the FileStream, then restore it, as appropriate. – canton7 Feb 17 '19 at 15:02
  • @canton7 Replace `processedSize += nextBlockSize;` with `pracessedSize = fs.Position;` does help, but the code still yield problem (not exception) like extra bytes and repeated characters in the last line when decrypted. `Come on, Daddy!Come on, Daddy!��`. But should this be out of scope for this question. – WylieYYYY Feb 17 '19 at 16:56
  • Your question doesn't include your decryption code? – canton7 Feb 17 '19 at 18:31
  • Also note that your encrypted file will be longer than your unencrypted one, because of the padding at the end. Make sure you set the length of the decrypted file correctly after decrypting to remove that padding. – canton7 Feb 17 '19 at 18:52
  • Updated, last parameter: true for encrypt, false for decrypt. The symtoms are as follow: The last line is always duplicated and as the buffer size change, the random bytes change position. – WylieYYYY Feb 17 '19 at 18:59
  • See my last comment about padding – canton7 Feb 17 '19 at 19:07
  • The final bytes are removed in the latest run, but the plain text still repeated. I think that the padding is correctly removed now because when I open up notepad++, no hidden bytes are there. Thank you for the constant help! – WylieYYYY Feb 17 '19 at 19:30
  • I'd break out the debugger I'm that case, you should be able to see the duplicate plain text appearing in the file? – canton7 Feb 17 '19 at 19:38
  • `if (nextBlockSize != buffer.Length) break;` added after `SetProgress`, the seek was messing up the loop, the break statement ensure it quits in time. The final problem is random bytes in file. – WylieYYYY Feb 17 '19 at 20:58
  • Seems like a lot to do when "write new file, delete old file, rename new file" works and doesn't seem to require a lot of rigmarole. – Damien_The_Unbeliever Feb 18 '19 at 07:54

1 Answers1

1

This was the sort of thing I was trying to describe in the comments, and it works.

I haven't bothered to do Rfc2898DeriveBytes on the key, check the existence of the file, etc. It does however randomly generate an IV, and writes it to the end of the file (turned out to be easier than writing it to the start).

private static void EncryptFile(string path, byte[] key)
{
    byte[] iv = new byte[16];
    // private static readonly RNGCryptoServiceProvider rngcsp = new RNGCryptoServiceProvider();
    rngcsp.GetBytes(iv);

    using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
    {
        byte[] buffer = new byte[1024];
        long readPos = 0;
        long writePos = 0;
        long readLength = fs.Length;

        using (var aes = new RijndaelManaged()
        {
            Key = key,
            IV = iv,
            Padding = PaddingMode.PKCS7,
            Mode = CipherMode.CFB,
        })
        using (var cs = new CryptoStream(fs, aes.CreateEncryptor(), CryptoStreamMode.Write))
        {
            while (readPos < readLength)
            {
                fs.Position = readPos;
                int bytesRead = fs.Read(buffer, 0, buffer.Length);
                readPos = fs.Position;

                fs.Position = writePos;
                cs.Write(buffer, 0, bytesRead);
                writePos = fs.Position;
            }

            // In older versions of .NET, CryptoStream doesn't have a ctor
            // with 'leaveOpen', so we have to do this instead.
            cs.FlushFinalBlock();

            // Write the IV to the end of the file
            fs.Write(iv, 0, iv.Length);
        }
    }
}

And to decrypt:

private static void DecryptFile(string path, byte[] key)
{
    using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
    {
        byte[] buffer = new byte[1024];
        byte[] iv = new byte[16];
        long readPos = 0;
        long writePos = 0;
        long readLength = fs.Length - iv.Length;

        // IV is the last 16 bytes
        if (fs.Length < iv.Length)
            throw new IOException("File is too short");
        fs.Position = readLength;
        fs.Read(iv, 0, iv.Length);
        fs.SetLength(readLength);

        using (var aes = new RijndaelManaged()
        {
            Key = key,
            IV = iv,
            Padding = PaddingMode.PKCS7,
            Mode = CipherMode.CFB,
        })
        using (var cs = new CryptoStream(fs, aes.CreateDecryptor(), CryptoStreamMode.Read))
        {
            while (readPos < readLength)
            {
                fs.Position = readPos;
                int bytesRead = cs.Read(buffer, 0, buffer.Length);
                readPos = fs.Position;

                fs.Position = writePos;
                fs.Write(buffer, 0, bytesRead);
                writePos = fs.Position;
            }

            // Trim the padding
            fs.SetLength(writePos);
        }
    }
}
canton7
  • 37,633
  • 3
  • 64
  • 77