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)