10

I was trying to use CryptoStream with AWS .NET SDk it failed as seek is not supported on CryptoStream. I read somewhere with content length known we should be able to add these capabilities to CryptoStream. I would like to know how to do this; any sample code will be useful too.

I have a method like this which is passed with a FieStream and returns a cryptoStream. I assign the returned Stream object to InputStream of AWS SDk PutObjectRequest object.

public static Stream GetEncryptStream(Stream existingStream,
    SymmetricAlgorithm cryptoServiceProvider,
    string encryptionKey, string encryptionIV)
{
    Stream existingStream = this.dataStream;

    cryptoServiceProvider.Key = ASCIIEncoding.ASCII.GetBytes(encryptionKey);
    cryptoServiceProvider.IV = ASCIIEncoding.ASCII.GetBytes(encryptionIV);
    CryptoStream cryptoStream = new CryptoStream(existingStream,
        cryptoServiceProvider.CreateEncryptor(), CryptoStreamMode.Read);

    return cryptoStream ;
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
Krishna Kumar
  • 7,841
  • 14
  • 49
  • 61
  • Hi, can you show us what you are doing so far? I believe if you have a stream reader linked to your CryptoStream then you can move position and seek... – Davide Piras Feb 17 '11 at 08:30

3 Answers3

8

Generally with encryption there isn't a 1:1 mapping between input bytes and output bytes, so in order to seek backwards (in particular) it would have to do a lot of work - perhaps even going right back to the start and moving forwards processing the data to consume [n] bytes from the decrypted stream. Even if it knew where each byte mapped to, the state of the encryption is dependent on the data that came before it (it isn't a decoder ring ;p), so again - it would either have to read from the start (and reset back to the initialisation-vector), or it would have to track snapshots of positions and crypto-states, and go back to the nearest snapshot, then walk forwards. Lots of work and storage.

This would apply to seeking relative to either end, too.

Moving forwards from the current position wouldn't be too bad, but again you'd have to process the data - not just jump the base-stream's position.

There isn't a good way to implement this that most consumers could use - normally if you get a true from CanSeek that means "random access", but that is not efficient in this case.

As a workaround - consider copying the decrypted data into a MemoryStream or a file; then you can access the fully decrypted data in a random-access fashion.

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • 1
    Just remember copying the decrypted data to an unencrypted memory stream or file could leave the data open to easier snooping by not-so-nice people – Justin808 Feb 17 '11 at 10:20
  • @Justin808 obviously that depends on whether it is intended to be secured *in transit/storage*, or whether even your own server is considered hostile / at-risk... – Marc Gravell Feb 17 '11 at 10:40
  • @Marc: Oh I agree, I just find a lot of people dont take the two seconds to think about it as long as the code works and the stored data is encrypted. I keep reading about all the big companies computers getting hacked and whatnot so even if you think your server is secure I find it better/safer to write code like its not. – Justin808 Feb 17 '11 at 19:56
  • 2
    @Justin808 If you have a way to work with encrypted data in a useful manner without decrypting it, the crypto community would love to hear from you. – Nick Johnson Feb 17 '11 at 23:21
  • 1
    @nick: obviously no, but depending on the data, decrypting it all at once and putting it all into a continuos block of memory might not be the smartest thing and I think dumping it to file is worse. Most filesystems wont clear the space that was used for the decrypted file as much as just marking it as free. – Justin808 Feb 18 '11 at 00:22
  • 1
    @Justin808 Definitely agreed about writing decrypted data to the filesystem. Note that memory can be swapped out to disk, too, if you don't do anything to prevent it. – Nick Johnson Feb 18 '11 at 03:16
6

It is so simple, just generate a long key with the same size as data by the position of the stream (stream.Position) and use ECB or any other encryption methods you like and then apply XOR. It is seekable, very fast and 1 to 1 encryption, which the output length is exactly same as the input length. It is memory efficient and you can use it on huge files. I think this method is used in modern WinZip AES encryption too. The only thing that you MUST be careful is the salt

Use a unique salt for each stream otherwise there is no encryption.

public class SeekableAesStream : Stream
{
    private Stream baseStream;
    private AesManaged aes;
    private ICryptoTransform encryptor;
    public bool autoDisposeBaseStream { get; set; } = true;

    /// <param name="salt">//** WARNING **: MUST be unique for each stream otherwise there is NO security</param>
    public SeekableAesStream(Stream baseStream, string password, byte[] salt)
    {
        this.baseStream = baseStream;
        using (var key = new PasswordDeriveBytes(password, salt))
        {
            aes = new AesManaged();
            aes.KeySize = 128;
            aes.Mode = CipherMode.ECB;
            aes.Padding = PaddingMode.None;
            aes.Key = key.GetBytes(aes.KeySize / 8);
            aes.IV = new byte[16]; //useless for ECB
            encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
        }
    }

    private void cipher(byte[] buffer, int offset, int count, long streamPos)
    {
        //find block number
        var blockSizeInByte = aes.BlockSize / 8;
        var blockNumber = (streamPos / blockSizeInByte) + 1;
        var keyPos = streamPos % blockSizeInByte;

        //buffer
        var outBuffer = new byte[blockSizeInByte];
        var nonce = new byte[blockSizeInByte];
        var init = false;

        for (int i = offset; i < count; i++)
        {
            //encrypt the nonce to form next xor buffer (unique key)
            if (!init || (keyPos % blockSizeInByte) == 0)
            {
                BitConverter.GetBytes(blockNumber).CopyTo(nonce, 0);
                encryptor.TransformBlock(nonce, 0, nonce.Length, outBuffer, 0);
                if (init) keyPos = 0;
                init = true;
                blockNumber++;
            }
            buffer[i] ^= outBuffer[keyPos]; //simple XOR with generated unique key
            keyPos++;
        }
    }

    public override bool CanRead { get { return baseStream.CanRead; } }
    public override bool CanSeek { get { return baseStream.CanSeek; } }
    public override bool CanWrite { get { return baseStream.CanWrite; } }
    public override long Length { get { return baseStream.Length; } }
    public override long Position { get { return baseStream.Position; } set { baseStream.Position = value; } }
    public override void Flush() { baseStream.Flush(); }
    public override void SetLength(long value) { baseStream.SetLength(value); }
    public override long Seek(long offset, SeekOrigin origin) { return baseStream.Seek(offset, origin); }

    public override int Read(byte[] buffer, int offset, int count)
    {
        var streamPos = Position;
        var ret = baseStream.Read(buffer, offset, count);
        cipher(buffer, offset, count, streamPos);
        return ret;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        cipher(buffer, offset, count, Position);
        baseStream.Write(buffer, offset, count);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            encryptor?.Dispose();
            aes?.Dispose();
            if (autoDisposeBaseStream)
                baseStream?.Dispose();
        }

        base.Dispose(disposing);
    }
}

Usage:

static void test()
    {
        var buf = new byte[255];
        for (byte i = 0; i < buf.Length; i++)
            buf[i] = i;

        //encrypting
        var uniqueSalt = new byte[16]; //** WARNING **: MUST be unique for each stream otherwise there is NO security
        var baseStream = new MemoryStream();
        var cryptor = new SeekableAesStream(baseStream, "password", uniqueSalt);
        cryptor.Write(buf, 0, buf.Length);

        //decrypting at position 200
        cryptor.Position = 200;
        var decryptedBuffer = new byte[50];
        cryptor.Read(decryptedBuffer, 0, 50);

    }
Mohammad Nikravan
  • 1,543
  • 2
  • 19
  • 22
  • This looks a little like Xts. There is a C# Xts implementation here https://bitbucket.org/garethl/xtssharp, but this code is a LOT easier (good). If security of this implementation is equally good as Xts, I would certainly use this instead. – osexpert Nov 20 '16 at 22:00
  • I'm not into the details of why CBC is preferred over ECB, although changing the line with the cipher mode to `CipherMode.CBC` seems to work. Is this on-liner change enough to enable CBC mode? – rraallvv Apr 14 '18 at 16:17
  • @rraallvv no, in CBC, each block encrypted by a seed from previous block so in CBC you need to decipher all previous blocks first. The approach here is completely different and you won't achieve CBC here, even if you change ECB to CBC, because in this method each block encrypted independently while CBC is designed to cipher all of the stream. – Mohammad Nikravan Apr 14 '18 at 22:31
  • 1
    @osexpert This seems to be an implementation of seekable CTR mode, combined with password based encryption. Beware that this uses the older PBKDF1 (`PasswordDeriveBytes`) method instead of PBKDF2 (`Rfc2898DeriveBytes`) and that it doesn't define any iteration count in the constructor. Of course the security is not just dependent on the salt, but also the password. Setting an IV for ECB mode is of course an action of futility, as ECB doesn't use one. – Maarten Bodewes Sep 13 '20 at 09:39
3

As an extension to Mark Gravell's answer, the seekability of a cipher depends on the Mode Of Operation you're using for the cipher. Most modes of operation aren't seekable, because each block of ciphertext depends in some way on the previous one. ECB is seekable, but it's almost universally a bad idea to use it. CTR mode is another one that can be accessed randomly, as is CBC.

All of these modes have their own vulnerabilities, however, so you should read carefully and think long and hard (and preferably consult an expert) before choosing one.

Nick Johnson
  • 100,655
  • 16
  • 128
  • 198
  • 1
    I'm interpreted your answers as to saying that CBC is preferred over ECB, am I right? If so, all I need is a one-liner change in the answer above with `aes.Mode = CipherMode.CBC` right? – rraallvv Apr 14 '18 at 16:22