Skip to content

Commit 98bbdda

Browse files
Numpsypiksel
authored andcommitted
Merge PR #331: Change ZipAESStream to handle reads of less data than the AES block size
* Change ZipAESStream to better handle reads of less than the crypto block size * Add unit test for reading AES Encrypted/Stored zip entries * Fixes #324
1 parent 1c3f459 commit 98bbdda

2 files changed

Lines changed: 160 additions & 25 deletions

File tree

src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs

Lines changed: 98 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ public ZipAESStream(Stream stream, ZipAESTransform transform, CryptoStreamMode m
2727
_transform = transform;
2828
_slideBuffer = new byte[1024];
2929

30-
_blockAndAuth = CRYPTO_BLOCK_SIZE + AUTH_CODE_LENGTH;
31-
3230
// mode:
3331
// CryptoStreamMode.Read means we read from "stream" and pass decrypted to our Read() method.
3432
// Write bypasses this stream and uses the Transform directly.
@@ -41,33 +39,72 @@ public ZipAESStream(Stream stream, ZipAESTransform transform, CryptoStreamMode m
4139
// The final n bytes of the AES stream contain the Auth Code.
4240
private const int AUTH_CODE_LENGTH = 10;
4341

42+
// Blocksize is always 16 here, even for AES-256 which has transform.InputBlockSize of 32.
43+
private const int CRYPTO_BLOCK_SIZE = 16;
44+
45+
// total length of block + auth code
46+
private const int BLOCK_AND_AUTH = CRYPTO_BLOCK_SIZE + AUTH_CODE_LENGTH;
47+
4448
private Stream _stream;
4549
private ZipAESTransform _transform;
4650
private byte[] _slideBuffer;
4751
private int _slideBufStartPos;
4852
private int _slideBufFreePos;
4953

50-
// Blocksize is always 16 here, even for AES-256 which has transform.InputBlockSize of 32.
51-
private const int CRYPTO_BLOCK_SIZE = 16;
54+
// Buffer block transforms to enable partial reads
55+
private byte[] _transformBuffer = null;// new byte[CRYPTO_BLOCK_SIZE];
56+
private int _transformBufferFreePos;
57+
private int _transformBufferStartPos;
5258

53-
private int _blockAndAuth;
59+
// Do we have some buffered data available?
60+
private bool HasBufferedData =>_transformBuffer != null && _transformBufferStartPos < _transformBufferFreePos;
5461

5562
/// <summary>
5663
/// Reads a sequence of bytes from the current CryptoStream into buffer,
5764
/// and advances the position within the stream by the number of bytes read.
5865
/// </summary>
5966
public override int Read(byte[] buffer, int offset, int count)
67+
{
68+
// Nothing to do
69+
if (count == 0)
70+
return 0;
71+
72+
// If we have buffered data, read that first
73+
int nBytes = 0;
74+
if (HasBufferedData)
75+
{
76+
nBytes = ReadBufferedData(buffer, offset, count);
77+
78+
// Read all requested data from the buffer
79+
if (nBytes == count)
80+
return nBytes;
81+
82+
offset += nBytes;
83+
count -= nBytes;
84+
}
85+
86+
// Read more data from the input, if available
87+
if (_slideBuffer != null)
88+
nBytes += ReadAndTransform(buffer, offset, count);
89+
90+
return nBytes;
91+
}
92+
93+
// Read data from the underlying stream and decrypt it
94+
private int ReadAndTransform(byte[] buffer, int offset, int count)
6095
{
6196
int nBytes = 0;
6297
while (nBytes < count)
6398
{
99+
int bytesLeftToRead = count - nBytes;
100+
64101
// Calculate buffer quantities vs read-ahead size, and check for sufficient free space
65102
int byteCount = _slideBufFreePos - _slideBufStartPos;
66103

67104
// Need to handle final block and Auth Code specially, but don't know total data length.
68105
// Maintain a read-ahead equal to the length of (crypto block + Auth Code).
69106
// When that runs out we can detect these final sections.
70-
int lengthToRead = _blockAndAuth - byteCount;
107+
int lengthToRead = BLOCK_AND_AUTH - byteCount;
71108
if (_slideBuffer.Length - _slideBufFreePos < lengthToRead)
72109
{
73110
// Shift the data to the beginning of the buffer
@@ -84,17 +121,11 @@ public override int Read(byte[] buffer, int offset, int count)
84121

85122
// Recalculate how much data we now have
86123
byteCount = _slideBufFreePos - _slideBufStartPos;
87-
if (byteCount >= _blockAndAuth)
124+
if (byteCount >= BLOCK_AND_AUTH)
88125
{
89-
// At least a 16 byte block and an auth code remains.
90-
_transform.TransformBlock(_slideBuffer,
91-
_slideBufStartPos,
92-
CRYPTO_BLOCK_SIZE,
93-
buffer,
94-
offset);
95-
nBytes += CRYPTO_BLOCK_SIZE;
96-
offset += CRYPTO_BLOCK_SIZE;
97-
_slideBufStartPos += CRYPTO_BLOCK_SIZE;
126+
var read = TransformAndBufferBlock(buffer, offset, bytesLeftToRead, CRYPTO_BLOCK_SIZE);
127+
nBytes += read;
128+
offset += read;
98129
}
99130
else
100131
{
@@ -103,14 +134,7 @@ public override int Read(byte[] buffer, int offset, int count)
103134
{
104135
// At least one byte of data plus auth code
105136
int finalBlock = byteCount - AUTH_CODE_LENGTH;
106-
_transform.TransformBlock(_slideBuffer,
107-
_slideBufStartPos,
108-
finalBlock,
109-
buffer,
110-
offset);
111-
112-
nBytes += finalBlock;
113-
_slideBufStartPos += finalBlock;
137+
nBytes += TransformAndBufferBlock(buffer, offset, bytesLeftToRead, finalBlock);
114138
}
115139
else if (byteCount < AUTH_CODE_LENGTH)
116140
throw new Exception("Internal error missed auth code"); // Coding bug
@@ -125,12 +149,62 @@ public override int Read(byte[] buffer, int offset, int count)
125149
}
126150
}
127151

152+
// don't need this any more, so use it as a 'complete' flag
153+
_slideBuffer = null;
154+
128155
break; // Reached the auth code
129156
}
130157
}
131158
return nBytes;
132159
}
133160

161+
// read some buffered data
162+
private int ReadBufferedData(byte[] buffer, int offset, int count)
163+
{
164+
int copyCount = Math.Min(count, _transformBufferFreePos - _transformBufferStartPos);
165+
166+
Array.Copy(_transformBuffer, _transformBufferStartPos, buffer, offset, count);
167+
_transformBufferStartPos += copyCount;
168+
169+
return copyCount;
170+
}
171+
172+
// Perform the crypto transform, and buffer the data if less than one block has been requested.
173+
private int TransformAndBufferBlock(byte[] buffer, int offset, int count, int blockSize)
174+
{
175+
// If the requested data is greater than one block, transform it directly into the output
176+
// If it's smaller, do it into a temporary buffer and copy the requested part
177+
bool bufferRequired = (blockSize > count);
178+
179+
if (bufferRequired && _transformBuffer == null)
180+
_transformBuffer = new byte[CRYPTO_BLOCK_SIZE];
181+
182+
var targetBuffer = bufferRequired ? _transformBuffer : buffer;
183+
var targetOffset = bufferRequired ? 0 : offset;
184+
185+
// Transform the data
186+
_transform.TransformBlock(_slideBuffer,
187+
_slideBufStartPos,
188+
blockSize,
189+
targetBuffer,
190+
targetOffset);
191+
192+
_slideBufStartPos += blockSize;
193+
194+
if (!bufferRequired)
195+
{
196+
return blockSize;
197+
}
198+
else
199+
{
200+
Array.Copy(_transformBuffer, 0, buffer, offset, count);
201+
_transformBufferStartPos = count;
202+
_transformBufferFreePos = blockSize;
203+
204+
return count;
205+
}
206+
}
207+
134208
/// <summary>
135209
/// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
136210
/// </summary>

test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using ICSharpCode.SharpZipLib.Zip;
1+
using ICSharpCode.SharpZipLib.Core;
2+
using ICSharpCode.SharpZipLib.Zip;
23
using NUnit.Framework;
34
using System;
45
using System.Diagnostics;
@@ -145,6 +146,66 @@ public void ZipFileStoreAes()
145146
}
146147
}
147148

149+
/// <summary>
150+
/// Test using AES encryption on a file whose contents are Stored rather than deflated
151+
/// </summary>
152+
[Test]
153+
[Category("Encryption")]
154+
[Category("Zip")]
155+
public void ZipFileStoreAesPartialRead()
156+
{
157+
string password = "password";
158+
159+
using (var memoryStream = new MemoryStream())
160+
{
161+
// Try to create a zip stream
162+
WriteEncryptedZipToStream(memoryStream, password, 256, CompressionMethod.Stored);
163+
164+
// reset
165+
memoryStream.Seek(0, SeekOrigin.Begin);
166+
167+
// try to read it
168+
var zipFile = new ZipFile(memoryStream, leaveOpen: true)
169+
{
170+
Password = password
171+
};
172+
173+
foreach (ZipEntry entry in zipFile)
174+
{
175+
if (!entry.IsFile) continue;
176+
177+
// Should be stored rather than deflated
178+
Assert.That(entry.CompressionMethod, Is.EqualTo(CompressionMethod.Stored), "Entry should be stored");
179+
180+
using (var ms = new MemoryStream())
181+
{
182+
using (var zis = zipFile.GetInputStream(entry))
183+
{
184+
byte[] buffer = new byte[1];
185+
186+
while (true)
187+
{
188+
int b = zis.ReadByte();
189+
190+
if (b == -1)
191+
break;
192+
193+
ms.WriteByte((byte)b);
194+
}
195+
}
196+
197+
ms.Seek(0, SeekOrigin.Begin);
198+
199+
using (var sr = new StreamReader(ms, Encoding.UTF8))
200+
{
201+
var content = sr.ReadToEnd();
202+
Assert.That(content, Is.EqualTo(DummyDataString), "Decompressed content does not match input data");
203+
}
204+
}
205+
}
206+
}
207+
}
208+
148209
private static readonly string[] possible7zPaths = new[] {
149210
// Check in PATH
150211
"7z", "7za",

0 commit comments

Comments
 (0)