From 896e730e2a53ba9c59ca3bf7013feaf70f939b89 Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Sun, 15 Sep 2019 20:51:46 +0100 Subject: [PATCH] Add AES encryption support to ZipFile --- .../Encryption/ZipAESEncryptionStream.cs | 142 ++++++++++++++++++ src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs | 68 +++++++-- 2 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 src/ICSharpCode.SharpZipLib/Encryption/ZipAESEncryptionStream.cs diff --git a/src/ICSharpCode.SharpZipLib/Encryption/ZipAESEncryptionStream.cs b/src/ICSharpCode.SharpZipLib/Encryption/ZipAESEncryptionStream.cs new file mode 100644 index 000000000..f1c32daad --- /dev/null +++ b/src/ICSharpCode.SharpZipLib/Encryption/ZipAESEncryptionStream.cs @@ -0,0 +1,142 @@ +using System; +using System.IO; +using System.Security.Cryptography; + +namespace ICSharpCode.SharpZipLib.Encryption +{ + /// + /// Encrypts AES ZIP entries. + /// + /// + /// Based on information from http://www.winzip.com/aes_info.htm + /// and http://www.gladman.me.uk/cryptography_technology/fileencrypt/ + /// + internal class ZipAESEncryptionStream : Stream + { + // The transform to use for encryption. + private ZipAESTransform transform; + + // The output stream to write the encrypted data to. + private readonly Stream outputStream; + + // Static to help ensure that multiple files within a zip will get different random salt + private static RandomNumberGenerator _aesRnd = RandomNumberGenerator.Create(); + + /// + /// Constructor + /// + /// The stream on which to perform the cryptographic transformation. + /// The password used to encrypt the entry. + /// The length of the salt to use. + /// The block size to use for transforming. + public ZipAESEncryptionStream(Stream stream, string rawPassword, int saltLength, int blockSize) + { + // Set up stream. + this.outputStream = stream; + + // Initialise the encryption transform. + var salt = new byte[saltLength]; + + // Salt needs to be cryptographically random, and unique per file + if (_aesRnd == null) + _aesRnd = RandomNumberGenerator.Create(); + _aesRnd.GetBytes(salt); + + this.transform = new ZipAESTransform(rawPassword, salt, blockSize, true); + + // File format for AES: + // Size (bytes) Content + // ------------ ------- + // Variable Salt value + // 2 Password verification value + // Variable Encrypted file data + // 10 Authentication code + // + // Value in the "compressed size" fields of the local file header and the central directory entry + // is the total size of all the items listed above. In other words, it is the total size of the + // salt value, password verification value, encrypted data, and authentication code. + var pwdVerifier = this.transform.PwdVerifier; + this.outputStream.Write(salt, 0, salt.Length); + this.outputStream.Write(pwdVerifier, 0, pwdVerifier.Length); + } + + // This stream is write only. + public override bool CanRead => false; + + // We only support writing - no seeking about. + public override bool CanSeek => false; + + // Supports writing for encrypting. + public override bool CanWrite => true; + + // We don't track this. + public override long Length => throw new NotImplementedException(); + + // We don't track this, or support seeking. + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + /// + /// When the stream is disposed, write the final blocks and AES Authentication code + /// + protected override void Dispose(bool disposing) + { + if (this.transform != null) + { + this.WriteAuthCode(); + this.transform.Dispose(); + this.transform = null; + } + } + + // + public override void Flush() + { + this.outputStream.Flush(); + } + + // + public override int Read(byte[] buffer, int offset, int count) + { + // ZipAESEncryptionStream is only used for encryption. + throw new NotImplementedException(); + } + + // + public override long Seek(long offset, SeekOrigin origin) + { + // We don't support seeking. + throw new NotImplementedException(); + } + + // + public override void SetLength(long value) + { + // We don't support setting the length. + throw new NotImplementedException(); + } + + // + public override void Write(byte[] buffer, int offset, int count) + { + if (count == 0) + { + return; + } + + var outputBuffer = new byte[count]; + var outputCount = this.transform.TransformBlock(buffer, offset, count, outputBuffer, 0); + this.outputStream.Write(outputBuffer, 0, outputCount); + } + + // Write the auth code for the encrypted data to the output stream + private void WriteAuthCode() + { + // Transform the final block? + + // Write the AES Authentication Code (a hash of the compressed and encrypted data) + var authCode = this.transform.GetAuthCode(); + this.outputStream.Write(authCode, 0, 10); + this.outputStream.Flush(); + } + } +} diff --git a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs index 5942b2c5c..66118c975 100644 --- a/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs +++ b/src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs @@ -1864,10 +1864,10 @@ public void Add(IStaticDataSource dataSource, ZipEntry entry) // We don't currently support adding entries with AES encryption, so throw // up front instead of failing or falling back to ZipCrypto later on - if (entry.AESKeySize > 0) - { - throw new NotSupportedException("Creation of AES encrypted entries is not supported"); - } + //if (entry.AESKeySize > 0) + //{ + // throw new NotSupportedException("Creation of AES encrypted entries is not supported"); + //} CheckSupportedCompressionMethod(entry.CompressionMethod); CheckUpdating(); @@ -2104,7 +2104,7 @@ private void WriteLocalEntryHeader(ZipUpdate update) WriteLEShort(entry.Version); WriteLEShort(entry.Flags); - WriteLEShort((byte)entry.CompressionMethod); + WriteLEShort((byte)entry.CompressionMethodForHeader); WriteLEInt((int)entry.DosTime); if (!entry.HasCrc) @@ -2158,6 +2158,12 @@ private void WriteLocalEntryHeader(ZipUpdate update) ed.Delete(1); } + // Write AES Data if needed + if (entry.AESKeySize > 0) + { + AddExtraDataAES(entry, ed); + } + entry.ExtraData = ed.GetEntryData(); WriteLEShort(name.Length); @@ -2214,7 +2220,7 @@ private int WriteCentralDirectoryHeader(ZipEntry entry) unchecked { - WriteLEShort((byte)entry.CompressionMethod); + WriteLEShort((byte)entry.CompressionMethodForHeader); WriteLEInt((int)entry.DosTime); WriteLEInt((int)entry.Crc); } @@ -2281,6 +2287,11 @@ private int WriteCentralDirectoryHeader(ZipEntry entry) ed.Delete(1); } + if (entry.AESKeySize > 0) + { + AddExtraDataAES(entry, ed); + } + byte[] centralExtraData = ed.GetEntryData(); WriteLEShort(centralExtraData.Length); @@ -2335,6 +2346,22 @@ private int WriteCentralDirectoryHeader(ZipEntry entry) return ZipConstants.CentralHeaderBaseSize + name.Length + centralExtraData.Length + rawComment.Length; } + private static void AddExtraDataAES(ZipEntry entry, ZipExtraData extraData) + { + // Vendor Version: AE-1 IS 1. AE-2 is 2. With AE-2 no CRC is required and 0 is stored. + const int VENDOR_VERSION = 2; + // Vendor ID is the two ASCII characters "AE". + const int VENDOR_ID = 0x4541; //not 6965; + extraData.StartNewEntry(); + // Pack AES extra data field see http://www.winzip.com/aes_info.htm + //extraData.AddLeShort(7); // Data size (currently 7) + extraData.AddLeShort(VENDOR_VERSION); // 2 = AE-2 + extraData.AddLeShort(VENDOR_ID); // "AE" + extraData.AddData(entry.AESEncryptionStrength); // 1 = 128, 2 = 192, 3 = 256 + extraData.AddLeShort((int)entry.CompressionMethod); // The actual compression method used to compress the file + extraData.AddNewEntry(0x9901); + } + #endregion Writing Values/Headers private void PostUpdateCleanup() @@ -2621,13 +2648,20 @@ private Stream GetOutputStream(ZipEntry entry) switch (entry.CompressionMethod) { case CompressionMethod.Stored: - result = new UncompressedStream(result); + if (!entry.IsCrypted) + { + // If there is an encryption stream in use, that can be written to directly + // otherwise, wrap it in an UncompressedStream instead of returning the base stream directly + result = new UncompressedStream(result); + } break; case CompressionMethod.Deflated: var dos = new DeflaterOutputStream(result, new Deflater(9, true)) { - IsStreamOwner = false + // If there is an encryption stream in use, then we want that to be disposed when the deflator stream is disposed + // If not, then we don't want it to dispose the base stream + IsStreamOwner = entry.IsCrypted }; result = dos; break; @@ -3667,9 +3701,16 @@ private Stream CreateAndInitDecryptionStream(Stream baseStream, ZipEntry entry) private Stream CreateAndInitEncryptionStream(Stream baseStream, ZipEntry entry) { - CryptoStream result = null; - if ((entry.Version < ZipConstants.VersionStrongEncryption) - || (entry.Flags & (int)GeneralBitFlags.StrongEncryption) == 0) + if (entry.CompressionMethodForHeader == CompressionMethod.WinZipAES) + { + int blockSize = entry.AESKeySize / 8; // bits to bytes + + var aesStream = + new ZipAESEncryptionStream(baseStream, rawPassword_, entry.AESSaltLen, blockSize); + + return aesStream; + } + else { var classicManaged = new PkzipClassicManaged(); @@ -3681,7 +3722,7 @@ private Stream CreateAndInitEncryptionStream(Stream baseStream, ZipEntry entry) // Closing a CryptoStream will close the base stream as well so wrap it in an UncompressedStream // which doesnt do this. - result = new CryptoStream(new UncompressedStream(baseStream), + CryptoStream result = new CryptoStream(new UncompressedStream(baseStream), classicManaged.CreateEncryptor(key, null), CryptoStreamMode.Write); if ((entry.Crc < 0) || (entry.Flags & 8) != 0) @@ -3692,8 +3733,9 @@ private Stream CreateAndInitEncryptionStream(Stream baseStream, ZipEntry entry) { WriteEncryptionHeader(result, entry.Crc); } + + return result; } - return result; } private static void CheckClassicPassword(CryptoStream classicCryptoStream, ZipEntry entry)