diff --git a/src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs b/src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs index 346b5484b..fbf76a77b 100644 --- a/src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs +++ b/src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs @@ -40,7 +40,7 @@ public ZipAESStream(Stream stream, ZipAESTransform transform, CryptoStreamMode m } // The final n bytes of the AES stream contain the Auth Code. - public const int AUTH_CODE_LENGTH = 10; + public const int AUTH_CODE_LENGTH = Zip.ZipConstants.AESAuthCodeLength; // Blocksize is always 16 here, even for AES-256 which has transform.InputBlockSize of 32. private const int CRYPTO_BLOCK_SIZE = 16; diff --git a/src/ICSharpCode.SharpZipLib/Encryption/ZipAESTransform.cs b/src/ICSharpCode.SharpZipLib/Encryption/ZipAESTransform.cs index 32c7b8156..9e7790dec 100644 --- a/src/ICSharpCode.SharpZipLib/Encryption/ZipAESTransform.cs +++ b/src/ICSharpCode.SharpZipLib/Encryption/ZipAESTransform.cs @@ -8,8 +8,6 @@ namespace ICSharpCode.SharpZipLib.Encryption /// internal class ZipAESTransform : ICryptoTransform { - private const int PWD_VER_LENGTH = 2; - // WinZip use iteration count of 1000 for PBKDF2 key generation private const int KEY_ROUNDS = 1000; @@ -28,6 +26,7 @@ internal class ZipAESTransform : ICryptoTransform private byte[] _authCode = null; private bool _writeMode; + private Action _appendHmac = remaining => { }; /// /// Constructor. @@ -63,12 +62,29 @@ public ZipAESTransform(string key, byte[] saltBytes, int blockSize, bool writeMo // Use empty IV for AES _encryptor = rm.CreateEncryptor(key1bytes, new byte[16]); - _pwdVerifier = pdb.GetBytes(PWD_VER_LENGTH); + _pwdVerifier = pdb.GetBytes(Zip.ZipConstants.AESPasswordVerifyLength); // _hmacsha1 = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA1, key2bytes); _writeMode = writeMode; } + /// + /// Append all of the last transformed input data to the HMAC. + /// + public void AppendAllPending() + { + _appendHmac(0); + } + + /// + /// Append all except the number of bytes specified by remaining of the last transformed input data to the HMAC. + /// + /// The number of bytes not to be added to the HMAC. The excluded bytes are form the end. + public void AppendFinal(int remaining) + { + _appendHmac(remaining); + } + /// /// Implement the ICryptoTransform method. /// @@ -78,8 +94,16 @@ public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, b // This does not change the inputBuffer. Do this before decryption for read mode. if (!_writeMode) { - _hmacsha1.AppendData(inputBuffer, inputOffset, inputCount); + if (!ManualHmac) + { + _hmacsha1.AppendData(inputBuffer, inputOffset, inputCount); + } + else + { + _appendHmac = remaining => _hmacsha1.AppendData(inputBuffer, inputOffset, inputCount - remaining); + } } + // Encrypt with AES in CTR mode. Regards to Dr Brian Gladman for this. int ix = 0; while (ix < inputCount) @@ -168,6 +192,13 @@ public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int input /// public bool CanReuseTransform => true; + /// + /// Gets of sets a value indicating if the HMAC is updates on every read of if updating the HMAC has to be controlled manually + /// Manual control of HMAC is needed in case not all the Transformed data should be automatically added to the HMAC. + /// E.g. because its not know how much data belongs to the current entry before the data is decrypted and analyzed. + /// + public bool ManualHmac { get; set; } + /// /// Cleanup internal state. /// diff --git a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs index 7790474d2..5ce4ed26b 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/Compression/Streams/InflaterInputStream.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Security.Cryptography; +using ICSharpCode.SharpZipLib.Encryption; namespace ICSharpCode.SharpZipLib.Zip.Compression.Streams { @@ -92,9 +93,25 @@ public byte[] ClearText public int Available { get { return available; } - set { available = value; } + set + { + if (cryptoTransform is ZipAESTransform ct) + { + ct.AppendFinal(value); + } + + available = value; + } } + /// + /// A limitation how much data is decrypted. If null all the data in the input buffer will be decrypted. + /// Setting limit is important in case the HMAC has to be calculated for each zip entry. In that case + /// it is not possible to decrypt all available data in the input buffer, and only the data + /// belonging to the current zip entry must be decrypted so that the HMAC is correctly calculated. + /// + internal int? DecryptionLimit { get; set; } + /// /// Call passing the current clear text buffer contents. /// @@ -113,6 +130,11 @@ public void SetInflaterInput(Inflater inflater) /// public void Fill() { + if (cryptoTransform is ZipAESTransform ct) + { + ct.AppendAllPending(); + } + rawLength = 0; int toRead = rawData.Length; @@ -127,13 +149,11 @@ public void Fill() toRead -= count; } + clearTextLength = rawLength; if (cryptoTransform != null) { - clearTextLength = cryptoTransform.TransformBlock(rawData, 0, rawLength, clearText, 0); - } - else - { - clearTextLength = rawLength; + var size = CalculateDecryptionSize(rawLength); + cryptoTransform.TransformBlock(rawData, 0, size, clearText, 0); } available = clearTextLength; @@ -290,7 +310,9 @@ public ICryptoTransform CryptoTransform clearTextLength = rawLength; if (available > 0) { - cryptoTransform.TransformBlock(rawData, rawLength - available, available, clearText, rawLength - available); + var size = CalculateDecryptionSize(available); + + cryptoTransform.TransformBlock(rawData, rawLength - available, size, clearText, rawLength - available); } } else @@ -301,6 +323,19 @@ public ICryptoTransform CryptoTransform } } + private int CalculateDecryptionSize(int availableBufferSize) + { + int size = DecryptionLimit ?? availableBufferSize; + size = Math.Min(size, availableBufferSize); + + if (DecryptionLimit.HasValue) + { + DecryptionLimit -= size; + } + + return size; + } + #region Instance Fields private int rawLength; @@ -459,9 +494,10 @@ public long Skip(long count) /// /// Clear any cryptographic state. /// - protected void StopDecrypting() + protected virtual void StopDecrypting() { inputBuffer.CryptoTransform = null; + inputBuffer.DecryptionLimit = null; } /// diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipConstants.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipConstants.cs index b16fdefdf..24643bf2a 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipConstants.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipConstants.cs @@ -366,6 +366,16 @@ public static class ZipConstants [Obsolete("Use CryptoHeaderSize instead")] public const int CRYPTO_HEADER_SIZE = 12; + /// + /// The number of bytes in the WinZipAes Auth Code. + /// + internal const int AESAuthCodeLength = 10; + + /// + /// The number of bytes in the password verifier for WinZipAes. + /// + internal const int AESPasswordVerifyLength = 2; + /// /// The size of the Zip64 central directory locator. /// diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipEntry.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipEntry.cs index b0bf15821..08ff6d09f 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipEntry.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipEntry.cs @@ -824,6 +824,19 @@ public int AESKeySize } } + /// + /// Gets the AES Version + /// 1: AE-1 + /// 2: AE-2 + /// + public int AESVersion + { + get + { + return _aesVer; + } + } + /// /// AES Encryption strength for storage in extra data in entry header. /// 1 is 128 bit, 2 is 192 bit, 3 is 256 bit. @@ -1149,7 +1162,7 @@ public static string CleanName(string name) private bool forceZip64_; private byte cryptoCheckValue_; - private int _aesVer; // Version number (2 = AE-2 ?). Assigned but not used. + private int _aesVer; // Version number (1 = AE-1, 2 = AE-2) private int _aesEncryptionStrength; // Encryption strength 1 = 128 2 = 192 3 = 256 #endregion Instance Fields diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs index e49ebddfb..bde176788 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs @@ -74,10 +74,10 @@ public class ZipInputStream : InflaterInputStream private ZipEntry entry; private long size; - private CompressionMethod method; private int flags; private string password; private readonly StringCodec _stringCodec = ZipStrings.GetStringCodec(); + private ZipAESTransform cryptoTransform; #endregion Instance Fields @@ -141,17 +141,22 @@ public bool CanDecompressEntry /// /// Is the compression method for the specified entry supported? /// - /// - /// Uses entry.CompressionMethodForHeader so that entries of type WinZipAES will be rejected. - /// /// the entry to check. /// true if the compression method is supported, false if not. private static bool IsEntryCompressionMethodSupported(ZipEntry entry) { - var entryCompressionMethod = entry.CompressionMethodForHeader; - - return entryCompressionMethod == CompressionMethod.Deflated || - entryCompressionMethod == CompressionMethod.Stored; + var entryCompressionMethodForHeader = entry.CompressionMethodForHeader; + var entryCompressionMethod = entry.CompressionMethod; + + var compressionMethodSupported = + entryCompressionMethod == CompressionMethod.Deflated || + entryCompressionMethod == CompressionMethod.Stored; + var entryCompressionMethodForHeaderSupported = + entryCompressionMethodForHeader == CompressionMethod.Deflated || + entryCompressionMethodForHeader == CompressionMethod.Stored || + entryCompressionMethodForHeader == CompressionMethod.WinZipAES; + + return compressionMethodSupported && entryCompressionMethodForHeaderSupported; } /// @@ -191,7 +196,7 @@ public ZipEntry GetNextEntry() var versionRequiredToExtract = (short)inputBuffer.ReadLeShort(); flags = inputBuffer.ReadLeShort(); - method = (CompressionMethod)inputBuffer.ReadLeShort(); + var method = (CompressionMethod)inputBuffer.ReadLeShort(); var dostime = (uint)inputBuffer.ReadLeInt(); int crc2 = inputBuffer.ReadLeInt(); csize = inputBuffer.ReadLeInt(); @@ -267,9 +272,20 @@ public ZipEntry GetNextEntry() size = entry.Size; } - if (method == CompressionMethod.Stored && (!isCrypted && csize != size || (isCrypted && csize - ZipConstants.CryptoHeaderSize != size))) + if (method == CompressionMethod.Stored) + { + if (!isCrypted && csize != size || (isCrypted && csize - ZipConstants.CryptoHeaderSize != size)) + { + throw new ZipException("Stored, but compressed != uncompressed"); + } + } + else if (method == CompressionMethod.WinZipAES && entry.CompressionMethod == CompressionMethod.Stored) { - throw new ZipException("Stored, but compressed != uncompressed"); + var sizeWithoutAesOverhead = csize - (entry.AESSaltLen + ZipConstants.AESPasswordVerifyLength + ZipConstants.AESAuthCodeLength); + if (sizeWithoutAesOverhead != size) + { + throw new ZipException("Stored, but compressed != uncompressed"); + } } // Determine how to handle reading of data if this is attempted. @@ -359,13 +375,50 @@ private void ReadDataDescriptor() entry.Size = size; } + /// + /// Complete any decryption processing and clear any cryptographic state. + /// + private void StopDecrypting(bool testAESAuthCode) + { + StopDecrypting(); + + if (testAESAuthCode && entry.AESKeySize != 0) + { + byte[] authBytes = new byte[ZipConstants.AESAuthCodeLength]; + int authBytesRead = inputBuffer.ReadRawBuffer(authBytes, 0, authBytes.Length); + + if (authBytesRead < ZipConstants.AESAuthCodeLength) + { + throw new ZipException("Internal error missed auth code"); // Coding bug + } + + // Final block done. Check Auth code. + byte[] calcAuthCode = this.cryptoTransform.GetAuthCode(); + for (int i = 0; i < ZipConstants.AESAuthCodeLength; i++) + { + if (calcAuthCode[i] != authBytes[i]) + { + throw new ZipException("AES Authentication Code does not match. This is a super-CRC check on the data in the file after compression and encryption. The file may be damaged or tampered."); + } + } + + // Dispose the transform? + } + } + /// /// Complete cleanup as the final part of closing. /// /// True if the crc value should be tested private void CompleteCloseEntry(bool testCrc) { - StopDecrypting(); + StopDecrypting(testCrc); + + // AE-2 does not have a CRC by specification. Do not check CRC in this case. + if (entry.AESKeySize != 0 && entry.AESVersion == 2) + { + testCrc = false; + } if ((flags & 8) != 0) { @@ -382,7 +435,7 @@ private void CompleteCloseEntry(bool testCrc) crc.Reset(); - if (method == CompressionMethod.Deflated) + if (entry.CompressionMethod == CompressionMethod.Deflated) { inf.Reset(); } @@ -409,8 +462,8 @@ public void CloseEntry() { return; } - - if (method == CompressionMethod.Deflated) + + if (entry.CompressionMethod == CompressionMethod.Deflated) { if ((flags & 8) != 0) { @@ -425,6 +478,7 @@ public void CloseEntry() } csize -= inf.TotalIn; + inputBuffer.Available += inf.RemainingInput; } @@ -556,27 +610,69 @@ private int InitialRead(byte[] destination, int offset, int count) throw new ZipException("No password set."); } - // Generate and set crypto transform... - var managed = new PkzipClassicManaged(); - byte[] key = PkzipClassic.GenerateKeys(_stringCodec.ZipCryptoEncoding.GetBytes(password)); + if (entry.AESKeySize == 0) + { + // Generate and set crypto transform... + var managed = new PkzipClassicManaged(); + byte[] key = PkzipClassic.GenerateKeys(_stringCodec.ZipCryptoEncoding.GetBytes(password)); - inputBuffer.CryptoTransform = managed.CreateDecryptor(key, null); + inputBuffer.CryptoTransform = managed.CreateDecryptor(key, null); + inputBuffer.DecryptionLimit = null; - byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize]; - inputBuffer.ReadClearTextBuffer(cryptbuffer, 0, ZipConstants.CryptoHeaderSize); + byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize]; + inputBuffer.ReadClearTextBuffer(cryptbuffer, 0, ZipConstants.CryptoHeaderSize); - if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue) - { - throw new ZipException("Invalid password"); - } + if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue) + { + throw new ZipException("Invalid password"); + } - if (csize >= ZipConstants.CryptoHeaderSize) - { - csize -= ZipConstants.CryptoHeaderSize; + if (csize >= ZipConstants.CryptoHeaderSize) + { + csize -= ZipConstants.CryptoHeaderSize; + } + else if (!usesDescriptor) + { + throw new ZipException($"Entry compressed size {csize} too small for encryption"); + } } - else if (!usesDescriptor) + else { - throw new ZipException($"Entry compressed size {csize} too small for encryption"); + int saltLen = entry.AESSaltLen; + byte[] saltBytes = new byte[saltLen]; + int saltIn = inputBuffer.ReadRawBuffer(saltBytes, 0, saltLen); + + if (saltIn != saltLen) + { + throw new ZipException("AES Salt expected " + saltLen + " got " + saltIn); + } + + // + byte[] pwdVerifyRead = new byte[ZipConstants.AESPasswordVerifyLength]; + int pwdBytesRead = inputBuffer.ReadRawBuffer(pwdVerifyRead, 0, pwdVerifyRead.Length); + + if (pwdBytesRead != pwdVerifyRead.Length) + { + throw new EndOfStreamException(); + } + + int blockSize = entry.AESKeySize / 8; // bits to bytes + + var decryptor = new ZipAESTransform(password, saltBytes, blockSize, false); + decryptor.ManualHmac = csize <= 0; + byte[] pwdVerifyCalc = decryptor.PwdVerifier; + if (pwdVerifyCalc[0] != pwdVerifyRead[0] || pwdVerifyCalc[1] != pwdVerifyRead[1]) + { + throw new ZipException("Invalid password for AES"); + } + + // The AES data has saltLen+AESPasswordVerifyLength bytes as a header, and AESAuthCodeLength bytes + // as a footer. + csize -= (saltLen + ZipConstants.AESPasswordVerifyLength + ZipConstants.AESAuthCodeLength); + inputBuffer.DecryptionLimit = csize >= 0 ? (int?)csize : null; + + inputBuffer.CryptoTransform = decryptor; + this.cryptoTransform = decryptor; } } else @@ -586,13 +682,13 @@ private int InitialRead(byte[] destination, int offset, int count) if (csize > 0 || usesDescriptor) { - if (method == CompressionMethod.Deflated && inputBuffer.Available > 0) + if (entry.CompressionMethod == CompressionMethod.Deflated && inputBuffer.Available > 0) { inputBuffer.SetInflaterInput(inf); } // It's not possible to know how many bytes to read when using "Stored" compression (unless using encryption) - if (!entry.IsCrypted && method == CompressionMethod.Stored && usesDescriptor) + if (!entry.IsCrypted && entry.CompressionMethod == CompressionMethod.Stored && usesDescriptor) { internalReader = StoredDescriptorEntry; return StoredDescriptorEntry(destination, offset, count); @@ -680,7 +776,7 @@ private int BodyRead(byte[] buffer, int offset, int count) bool finished = false; - switch (method) + switch (entry.CompressionMethod) { case CompressionMethod.Deflated: count = base.Read(buffer, offset, count); @@ -731,6 +827,8 @@ private int BodyRead(byte[] buffer, int offset, int count) } } break; + default: + throw new InvalidOperationException("Internal Error: Unsupported compression method encountered."); } if (count > 0) diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs index 21042f75a..eaff7684d 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipOutputStream.cs @@ -443,6 +443,15 @@ internal void PutNextEntry(Stream stream, ZipEntry entry, long streamOffset = 0, if (Password != null) { entry.IsCrypted = true; + + // In case of AES the CRC is always 0 according to specification. + // This also prevents that a data descriptor is needed for the CRC as it is the case for other + // password protected zip files. AES uses a 10 Byte auth code instead of the CRC + if (entry.AESKeySize != 0) + { + entry.Crc = 0; + } + if (entry.Crc < 0) { // Need to append a data descriptor as the crc isnt available for use diff --git a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs index 0cf7395cb..8d98df29e 100644 --- a/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs +++ b/test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs @@ -12,6 +12,18 @@ namespace ICSharpCode.SharpZipLib.Tests.Zip [TestFixture] public class ZipEncryptionHandling { + static ZipEncryptionHandling() + { + var sb = new StringBuilder(); + for (int i = 0; i < 200; i++) + { + sb.AppendLine(Guid.NewGuid().ToString()); + } + + DummyDataString = sb.ToString(); + DummyDataStringShort = Guid.NewGuid().ToString(); + } + [Test] [Category("Encryption")] [Category("Zip")] @@ -110,6 +122,143 @@ public void ZipFileAesDecryption() } } + /// + /// Tests for reading encrypted entries using ZipInputStream. + /// + [Test] + [Category("Encryption")] + [Category("Zip")] + public void ZipInputStreamDecryption( + [Values(0, 128, 256)] int aesKeySize, + [Values(CompressionMethod.Stored, CompressionMethod.Deflated)] + CompressionMethod compressionMethod, + [Values] bool forceDataDescriptor) + { + var password = "password"; + + using (var ms = forceDataDescriptor ? new MemoryStreamWithoutSeek() : new MemoryStream()) + { + WriteEncryptedZipToStream(ms, 3, 3, password, aesKeySize, compressionMethod); + ms.Seek(0, SeekOrigin.Begin); + + using (var zis = new ZipInputStream(ms)) + { + zis.IsStreamOwner = false; + zis.Password = password; + + for (int i = 0; i < 6; i++) + { + var entry = zis.GetNextEntry(); + int fileNumber = int.Parse(entry.Name[5].ToString()); + + using (var sr = new StreamReader(zis, Encoding.UTF8, leaveOpen: true, detectEncodingFromByteOrderMarks: true, bufferSize: 1024)) + { + var content = sr.ReadToEnd(); + + Assert.AreEqual(fileNumber < 3 ? DummyDataString : DummyDataStringShort, content, + "Decompressed content does not match input data"); + } + } + } + } + } + + /// + /// Tests for reading encrypted entries using ZipInputStream. + /// Verify that it is possible to skip reading of entries. + /// + [Test] + [Category("Encryption")] + [Category("Zip")] + public void ZipInputStreamDecryptionSupportsSkippingEntries( + [Values(0, 128, 256)] int aesKeySize, + [Values(CompressionMethod.Stored, CompressionMethod.Deflated)] + CompressionMethod compressionMethod, + [Values] bool forceDataDescriptor) + { + var password = "password"; + + using (var ms = forceDataDescriptor ? new MemoryStreamWithoutSeek() : new MemoryStream()) + { + WriteEncryptedZipToStream(ms, 3, 3, password, aesKeySize, compressionMethod); + ms.Seek(0, SeekOrigin.Begin); + + using (var zis = new ZipInputStream(ms)) + { + zis.IsStreamOwner = false; + zis.Password = password; + + for (int i = 0; i < 6; i++) + { + var entry = zis.GetNextEntry(); + int fileNumber = int.Parse(entry.Name[5].ToString()); + + if (fileNumber % 2 == 1) + { + continue; + } + + using (var sr = new StreamReader(zis, Encoding.UTF8, leaveOpen: true, + detectEncodingFromByteOrderMarks: true, bufferSize: 1024)) + { + var content = sr.ReadToEnd(); + + Assert.AreEqual(fileNumber < 3 ? DummyDataString : DummyDataStringShort, content, + "Decompressed content does not match input data"); + } + } + } + } + } + + /// + /// Tests for reading encrypted entries using ZipInputStream. + /// Verify that it is possible to read entries only partially. + /// + [Test] + [Category("Encryption")] + [Category("Zip")] + public void ZipInputStreamDecryptionSupportsPartiallyReadingOfEntries( + [Values(0, 128, 256)] int aesKeySize, + [Values(CompressionMethod.Stored, CompressionMethod.Deflated)] + CompressionMethod compressionMethod, + [Values] bool forceDataDescriptor) + { + var password = "password"; + + using (var ms = forceDataDescriptor ? new MemoryStreamWithoutSeek() : new MemoryStream()) + { + WriteEncryptedZipToStream(ms, 3, 3, password, aesKeySize, compressionMethod); + ms.Seek(0, SeekOrigin.Begin); + + using (var zis = new ZipInputStream(ms)) + { + zis.IsStreamOwner = false; + zis.Password = password; + + for (int i = 0; i < 6; i++) + { + var entry = zis.GetNextEntry(); + int fileNumber = int.Parse(entry.Name[5].ToString()); + + if (fileNumber % 2 == 1) + { + zis.ReadByte(); + continue; + } + + using (var sr = new StreamReader(zis, Encoding.UTF8, leaveOpen: true, detectEncodingFromByteOrderMarks: true, bufferSize: 1024)) + { + var content = sr.ReadToEnd(); + + Assert.AreEqual(fileNumber < 3 ? DummyDataString : DummyDataStringShort, content, + "Decompressed content does not match input data"); + } + } + } + } + } + [Test] [Category("Encryption")] [Category("Zip")] @@ -379,7 +528,7 @@ public void ZipFileAesDelete() using (var memoryStream = new MemoryStream()) { // Try to create a zip stream - WriteEncryptedZipToStream(memoryStream, 3, password, keySize, CompressionMethod.Deflated); + WriteEncryptedZipToStream(memoryStream, 3, 0, password, keySize, CompressionMethod.Deflated); // reset memoryStream.Seek(0, SeekOrigin.Begin); @@ -482,40 +631,77 @@ public void ZipFileAESReadWithEmptyPassword() } } } + + // This is a zip file with three AES encrypted entry, whose password is password. + const string TestFileWithThreeEntries = @"UEsDBDMAAQBjAI9jbFIAAAAAIQAAAAUAAAAJAAsARmlsZTEudHh0AZkHAAIAQUUDAAAoyz4gbB4/SnNvoPSBMVS9Zhp5sKKD + GnLy8zwsuV0Jh/RQSwMEMwABAGMAnWNsUgAAAAAhAAAABQAAAAkACwBGaWxlMi50eHQBmQcAAgBBRQMAANoCDbQUG7iCJgGC2/5OrmUQUk/+fACL804W0bboF8YMM1BLAwQzAA + EAYwCjY2xSAAAAACEAAAAFAAAACQALAEZpbGUzLnR4dAGZBwACAEFFAwAAqmBqgBkl3tP6ND0uCD50mhwfOtbmwV1IKyUVK5wGVQUiUEsBAj8AMwABAGMAj2NsUgAAAAAhAAAA + BQAAAAkALwAAAAAAAAAgAAAAAAAAAEZpbGUxLnR4dAoAIAAAAAAAAQAYAApFb9MyF9cB7fEh1jIX1wHNrjnNMhfXAQGZBwACAEFFAwAAUEsBAj8AMwABAGMAnWNsUgAAAAAhAA + AABQAAAAkALwAAAAAAAAAgAAAAUwAAAEZpbGUyLnR4dAoAIAAAAAAAAQAYAK5pWOQyF9cBdTCL5TIX1wGab3HVMhfXAQGZBwACAEFFAwAAUEsBAj8AMwABAGMAo2NsUgAAAAAh + AAAABQAAAAkALwAAAAAAAAAgAAAApgAAAEZpbGUzLnR4dAoAIAAAAAAAAQAYANB1M+kyF9cB0gxl6jIX1wGqVSHWMhfXAQGZBwACAEFFAwAAUEsFBgAAAAADAAMAMgEAAPkAAAAAAA=="; /// - /// ZipInputStream can't decrypt AES encrypted entries, but it should report that to the caller - /// rather than just failing. + /// Test reading an AES encrypted file and skipping some entries by not reading form the stream /// [Test] [Category("Zip")] - public void ZipinputStreamShouldGracefullyFailWithAESStreams() + public void ZipInputStreamAESReadSkippingEntriesIsPossible() { - string password = "password"; + var fileBytes = Convert.FromBase64String(TestFileWithThreeEntries); - using (var memoryStream = new MemoryStream()) + using (var ms = new MemoryStream(fileBytes)) + using (var zis = new ZipInputStream(ms) { IsStreamOwner = false}) { - // Try to create a zip stream - WriteEncryptedZipToStream(memoryStream, password, 256); + zis.Password = "password"; - // reset - memoryStream.Seek(0, SeekOrigin.Begin); - - // Try to read - using (var inputStream = new ZipInputStream(memoryStream)) + for (int i = 0; i < 3; i++) { - inputStream.Password = password; - var entry = inputStream.GetNextEntry(); - Assert.That(entry.AESKeySize, Is.EqualTo(256), "Test entry should be AES256 encrypted."); - - // CanDecompressEntry should be false. - Assert.That(inputStream.CanDecompressEntry, Is.False, "CanDecompressEntry should be false for AES encrypted entries"); + var entry = zis.GetNextEntry(); + if (i == 1) + { + continue; + } - // Should throw on read. - Assert.Throws(() => inputStream.ReadByte()); + using (var sr = new StreamReader(zis, Encoding.UTF8, leaveOpen: true, detectEncodingFromByteOrderMarks: true, bufferSize: 1024)) + { + var content = sr.ReadToEnd(); + Assert.AreEqual(Path.GetFileNameWithoutExtension(entry.Name), content, "Decompressed content does not match input data"); + } } } } + + /// + /// Test reading an AES encrypted file and reading some entries only partially be not reading to the end of stream. + /// + [Test] + [Category("Zip")] + public void ZipInputStreamAESReadEntriesCanBeReadPartially() + { + var fileBytes = Convert.FromBase64String(TestFileWithThreeEntries); + + using (var ms = new MemoryStream(fileBytes)) + using (var zis = new ZipInputStream(ms) { IsStreamOwner = false}) + { + zis.Password = "password"; + + for (int i = 0; i < 3; i++) + { + var entry = zis.GetNextEntry(); + if (i == 1) + { + zis.ReadByte(); + continue; + } + + using (var sr = new StreamReader(zis, Encoding.UTF8, leaveOpen: true, detectEncodingFromByteOrderMarks: true, bufferSize: 1024)) + { + var content = sr.ReadToEnd(); + Assert.AreEqual(Path.GetFileNameWithoutExtension(entry.Name), content, "Decompressed content does not match input data"); + } + } + } + } public static void WriteEncryptedZipToStream(Stream stream, string password, int keySize, CompressionMethod compressionMethod = CompressionMethod.Deflated) { @@ -525,11 +711,11 @@ public static void WriteEncryptedZipToStream(Stream stream, string password, int zs.SetLevel(9); // 0-9, 9 being the highest level of compression zs.Password = password; // optional. Null is the same as not setting. Required if using AES. - AddEncrypedEntryToStream(zs, $"test", keySize, compressionMethod); + AddEncrypedEntryToStream(zs, $"test", keySize, compressionMethod, DummyDataString); } } - public void WriteEncryptedZipToStream(Stream stream, int entryCount, string password, int keySize, CompressionMethod compressionMethod) + public static void WriteEncryptedZipToStream(Stream stream, int entryCount, int shortEntryCount, string password, int keySize, CompressionMethod compressionMethod = CompressionMethod.Deflated) { using (var zs = new ZipOutputStream(stream)) { @@ -539,12 +725,17 @@ public void WriteEncryptedZipToStream(Stream stream, int entryCount, string pass for (int i = 0; i < entryCount; i++) { - AddEncrypedEntryToStream(zs, $"test-{i}", keySize, compressionMethod); + AddEncrypedEntryToStream(zs, $"test-{i}", keySize, compressionMethod, DummyDataString); } + + for (int i = 0; i < shortEntryCount; i++) + { + AddEncrypedEntryToStream(zs, $"test-{i + entryCount}", keySize, compressionMethod, DummyDataStringShort); + } } } - private static void AddEncrypedEntryToStream(ZipOutputStream zipOutputStream, string entryName, int keySize, CompressionMethod compressionMethod) + private static void AddEncrypedEntryToStream(ZipOutputStream zipOutputStream, string entryName, int keySize, CompressionMethod compressionMethod, string content) { ZipEntry zipEntry = new ZipEntry(entryName) { @@ -555,7 +746,7 @@ private static void AddEncrypedEntryToStream(ZipOutputStream zipOutputStream, st zipOutputStream.PutNextEntry(zipEntry); - byte[] dummyData = Encoding.UTF8.GetBytes(DummyDataString); + byte[] dummyData = Encoding.UTF8.GetBytes(content); using (var dummyStream = new MemoryStream(dummyData)) { @@ -574,9 +765,7 @@ public void CreateZipWithEncryptedEntries(string password, int keySize, Compress } } - private const string DummyDataString = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. -Fusce bibendum diam ac nunc rutrum ornare. Maecenas blandit elit ligula, eget suscipit lectus rutrum eu. -Maecenas aliquam, purus mattis pulvinar pharetra, nunc orci maximus justo, sed facilisis massa dui sed lorem. -Vestibulum id iaculis leo. Duis porta ante lorem. Duis condimentum enim nec lorem tristique interdum. Fusce in faucibus libero."; + private static readonly string DummyDataString; + private static readonly string DummyDataStringShort; } }