diff --git a/src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs b/src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs index cf0792c47..ffafee5df 100644 --- a/src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs +++ b/src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs @@ -27,8 +27,6 @@ public ZipAESStream(Stream stream, ZipAESTransform transform, CryptoStreamMode m _transform = transform; _slideBuffer = new byte[1024]; - _blockAndAuth = CRYPTO_BLOCK_SIZE + AUTH_CODE_LENGTH; - // mode: // CryptoStreamMode.Read means we read from "stream" and pass decrypted to our Read() method. // Write bypasses this stream and uses the Transform directly. @@ -41,33 +39,72 @@ public ZipAESStream(Stream stream, ZipAESTransform transform, CryptoStreamMode m // The final n bytes of the AES stream contain the Auth Code. private const int AUTH_CODE_LENGTH = 10; + // Blocksize is always 16 here, even for AES-256 which has transform.InputBlockSize of 32. + private const int CRYPTO_BLOCK_SIZE = 16; + + // total length of block + auth code + private const int BLOCK_AND_AUTH = CRYPTO_BLOCK_SIZE + AUTH_CODE_LENGTH; + private Stream _stream; private ZipAESTransform _transform; private byte[] _slideBuffer; private int _slideBufStartPos; private int _slideBufFreePos; - // Blocksize is always 16 here, even for AES-256 which has transform.InputBlockSize of 32. - private const int CRYPTO_BLOCK_SIZE = 16; + // Buffer block transforms to enable partial reads + private byte[] _transformBuffer = null;// new byte[CRYPTO_BLOCK_SIZE]; + private int _transformBufferFreePos; + private int _transformBufferStartPos; - private int _blockAndAuth; + // Do we have some buffered data available? + private bool HasBufferedData =>_transformBuffer != null && _transformBufferStartPos < _transformBufferFreePos; /// /// Reads a sequence of bytes from the current CryptoStream into buffer, /// and advances the position within the stream by the number of bytes read. /// public override int Read(byte[] buffer, int offset, int count) + { + // Nothing to do + if (count == 0) + return 0; + + // If we have buffered data, read that first + int nBytes = 0; + if (HasBufferedData) + { + nBytes = ReadBufferedData(buffer, offset, count); + + // Read all requested data from the buffer + if (nBytes == count) + return nBytes; + + offset += nBytes; + count -= nBytes; + } + + // Read more data from the input, if available + if (_slideBuffer != null) + nBytes += ReadAndTransform(buffer, offset, count); + + return nBytes; + } + + // Read data from the underlying stream and decrypt it + private int ReadAndTransform(byte[] buffer, int offset, int count) { int nBytes = 0; while (nBytes < count) { + int bytesLeftToRead = count - nBytes; + // Calculate buffer quantities vs read-ahead size, and check for sufficient free space int byteCount = _slideBufFreePos - _slideBufStartPos; // Need to handle final block and Auth Code specially, but don't know total data length. // Maintain a read-ahead equal to the length of (crypto block + Auth Code). // When that runs out we can detect these final sections. - int lengthToRead = _blockAndAuth - byteCount; + int lengthToRead = BLOCK_AND_AUTH - byteCount; if (_slideBuffer.Length - _slideBufFreePos < lengthToRead) { // Shift the data to the beginning of the buffer @@ -84,17 +121,11 @@ public override int Read(byte[] buffer, int offset, int count) // Recalculate how much data we now have byteCount = _slideBufFreePos - _slideBufStartPos; - if (byteCount >= _blockAndAuth) + if (byteCount >= BLOCK_AND_AUTH) { - // At least a 16 byte block and an auth code remains. - _transform.TransformBlock(_slideBuffer, - _slideBufStartPos, - CRYPTO_BLOCK_SIZE, - buffer, - offset); - nBytes += CRYPTO_BLOCK_SIZE; - offset += CRYPTO_BLOCK_SIZE; - _slideBufStartPos += CRYPTO_BLOCK_SIZE; + var read = TransformAndBufferBlock(buffer, offset, bytesLeftToRead, CRYPTO_BLOCK_SIZE); + nBytes += read; + offset += read; } else { @@ -103,14 +134,7 @@ public override int Read(byte[] buffer, int offset, int count) { // At least one byte of data plus auth code int finalBlock = byteCount - AUTH_CODE_LENGTH; - _transform.TransformBlock(_slideBuffer, - _slideBufStartPos, - finalBlock, - buffer, - offset); - - nBytes += finalBlock; - _slideBufStartPos += finalBlock; + nBytes += TransformAndBufferBlock(buffer, offset, bytesLeftToRead, finalBlock); } else if (byteCount < AUTH_CODE_LENGTH) throw new Exception("Internal error missed auth code"); // Coding bug @@ -125,12 +149,62 @@ public override int Read(byte[] buffer, int offset, int count) } } + // don't need this any more, so use it as a 'complete' flag + _slideBuffer = null; + break; // Reached the auth code } } return nBytes; } + // read some buffered data + private int ReadBufferedData(byte[] buffer, int offset, int count) + { + int copyCount = Math.Min(count, _transformBufferFreePos - _transformBufferStartPos); + + Array.Copy(_transformBuffer, _transformBufferStartPos, buffer, offset, count); + _transformBufferStartPos += copyCount; + + return copyCount; + } + + // Perform the crypto transform, and buffer the data if less than one block has been requested. + private int TransformAndBufferBlock(byte[] buffer, int offset, int count, int blockSize) + { + // If the requested data is greater than one block, transform it directly into the output + // If it's smaller, do it into a temporary buffer and copy the requested part + bool bufferRequired = (blockSize > count); + + if (bufferRequired && _transformBuffer == null) + _transformBuffer = new byte[CRYPTO_BLOCK_SIZE]; + + var targetBuffer = bufferRequired ? _transformBuffer : buffer; + var targetOffset = bufferRequired ? 0 : offset; + + // Transform the data + _transform.TransformBlock(_slideBuffer, + _slideBufStartPos, + blockSize, + targetBuffer, + targetOffset); + + _slideBufStartPos += blockSize; + + if (!bufferRequired) + { + return blockSize; + } + else + { + Array.Copy(_transformBuffer, 0, buffer, offset, count); + _transformBufferStartPos = count; + _transformBufferFreePos = blockSize; + + return count; + } + } + /// /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. /// diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs index 865965e0d..3f8f64427 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs @@ -1,4 +1,5 @@ -using ICSharpCode.SharpZipLib.Zip; +using ICSharpCode.SharpZipLib.Core; +using ICSharpCode.SharpZipLib.Zip; using NUnit.Framework; using System; using System.Diagnostics; @@ -145,6 +146,66 @@ public void ZipFileStoreAes() } } + /// + /// Test using AES encryption on a file whose contents are Stored rather than deflated + /// + [Test] + [Category("Encryption")] + [Category("Zip")] + public void ZipFileStoreAesPartialRead() + { + string password = "password"; + + using (var memoryStream = new MemoryStream()) + { + // Try to create a zip stream + WriteEncryptedZipToStream(memoryStream, password, 256, CompressionMethod.Stored); + + // reset + memoryStream.Seek(0, SeekOrigin.Begin); + + // try to read it + var zipFile = new ZipFile(memoryStream, leaveOpen: true) + { + Password = password + }; + + foreach (ZipEntry entry in zipFile) + { + if (!entry.IsFile) continue; + + // Should be stored rather than deflated + Assert.That(entry.CompressionMethod, Is.EqualTo(CompressionMethod.Stored), "Entry should be stored"); + + using (var ms = new MemoryStream()) + { + using (var zis = zipFile.GetInputStream(entry)) + { + byte[] buffer = new byte[1]; + + while (true) + { + int b = zis.ReadByte(); + + if (b == -1) + break; + + ms.WriteByte((byte)b); + } + } + + ms.Seek(0, SeekOrigin.Begin); + + using (var sr = new StreamReader(ms, Encoding.UTF8)) + { + var content = sr.ReadToEnd(); + Assert.That(content, Is.EqualTo(DummyDataString), "Decompressed content does not match input data"); + } + } + } + } + } + private static readonly string[] possible7zPaths = new[] { // Check in PATH "7z", "7za",