Search code examples
c#asp.net-corefilestreammemorystreamcryptostream

Returning Stream via File() causes future operations to throw "the process cannot access the file path because it is being used by another process"


To decrease memory usage when returning a file to a client, whilst decrypting, we have gone with streams. This was working fine until one quirk which is that when you upload the same file back to the server (e.g. when a client modifies it). It causes .net core to throw "the process cannot access the file path because it is being used by another process".

As this system is still in development, I'm unsure whether it's a quirk of running the application in debug mode rather than release. Although I built the code into release and still received the same error.

From what I'm aware of how return a stream works, it should dispose the streams automatically.

The first method that creates a stream contains the following:

return (await Decrypt(File.OpenRead(path), AesKey, AesIv), contentType);

The decrypt method then performs the following:

public static async Task<MemoryStream> Decrypt(FileStream data, string key, string iv)
{
    Aes aes = Aes.Create();

    aes.KeySize = 256;
    aes.BlockSize = 128;
    aes.Padding = PaddingMode.Zeros;

    aes.Key = Encoding.ASCII.GetBytes(key);
    aes.IV = Encoding.ASCII.GetBytes(iv);

    ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

    return await PerformCryptography(data, decryptor);
}

This then calls the crypto method:

private static async Task<MemoryStream> PerformCryptography(FileStream data, ICryptoTransform cryptoTransform)
{
    MemoryStream memoryStream = new MemoryStream();
    CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTransform, CryptoStreamMode.Write);

    await data.CopyToAsync(cryptoStream);
    cryptoStream.FlushFinalBlock();
    memoryStream.Seek(0, SeekOrigin.Begin);
    return memoryStream;
}

This returns back up the chain to the controller that returns the following:

return File(file, contentType, fileName);

When I was developing this, it seemed wrapping any of these in using would cause an object disposed exception, however I may have done something wrong.

Does anyone know how to fix something like this?


Solution

  • You need to dispose the FileStream after you've finished copying its contents to the CryptoStream. An easy way to do this is with a using block inside Decrypt. Note that the await is inside the using block, so the FileStream won't be disposed until the await completes:

    public static async Task<MemoryStream> Decrypt(FileStream data, string key, string iv)
    {
        using (data)
        {
            Aes aes = Aes.Create();
    
            aes.KeySize = 256;
            aes.BlockSize = 128;
            aes.Padding = PaddingMode.Zeros;
    
            aes.Key = Encoding.ASCII.GetBytes(key);
            aes.IV = Encoding.ASCII.GetBytes(iv);
    
            ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
    
            return await PerformCryptography(data, decryptor);
        }
    }
    

    Another approach is to open the CryptoStream in Read mode over the FileStream. Instead of copying data into the CryptoStream (and the CryptoStream then writes that data to the MemoryStream), you read data out of the CryptoStream (and the CryptoStream pulls data from the FileStream as it needs):

    private static async Task<MemoryStream> PerformCryptography(FileStream data, ICryptoTransform cryptoTransform)
    {
        MemoryStream memoryStream = new MemoryStream();
        using (CryptoStream cryptoStream = new CryptoStream(fileStream, cryptoTransform, CryptoStreamMode.Read))
        {
            await cryptoStream.CopyToAsync(memoryStream);
        }
        memoryStream.Seek(0, SeekOrigin.Begin);
        return memoryStream;
    }
    

    This lets you dispose the CryptoStream (which you should be disposing, because it's IDisposable) instead of calling FlushFinalBlock(), which is a bit neater. Disposing it also disposes the underlying FileStream.


    If you're in a situation where you don't need to seek on the Stream which Decrypt returns (sadly this isn't the case if you're returning it from ASP.NET Core with File(...)), you can just return the CryptoStream. This means that the decryption happens as that stream is read, rather than upfront, which may not be what you want:

    public static Stream Decrypt(FileStream data, string key, string iv)
    {
        Aes aes = Aes.Create();
    
        aes.KeySize = 256;
        aes.BlockSize = 128;
        aes.Padding = PaddingMode.Zeros;
    
        aes.Key = Encoding.ASCII.GetBytes(key);
        aes.IV = Encoding.ASCII.GetBytes(iv);
    
        return new CryptoStream(data, aes.CreateDecryptor(aes.Key, aes.IV), CryptoStreamMode.Read);
    }
    

    I don't like how you're deriving your key and IV. ASCII only supports values 0 to 127, so your keyspace is only half the size it should be: you're effectively using AES-128 rather than AES-256. Likewise your IV is half the size it should be. Use a key derivation function like PBKDF2 to turn a (long) plain-text key into a proper 256-bit binary key.

    Your IV is also suspect. Remember that everything you encrypt using a particular key needs to have a unique IV. That's very important. Do not re-use IVs!! It's common practice to randomly generate an IV when encrypting something, and to write that as the first few bytes of the ciphertext (it's fine if the IV is public, it just has to be unique). You can then extract that when decrypting.

    Your padding is also slightly suspect: it means that any trailing 0's in the plaintext are removed by the encryption/decryption process. It's much better to use something like PKCS7.