Skip to content

Commit

Permalink
Add AES encryption support to ZipFile
Browse files Browse the repository at this point in the history
  • Loading branch information
Numpsy committed Apr 11, 2020
1 parent fed3bd2 commit 896e730
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 13 deletions.
142 changes: 142 additions & 0 deletions src/ICSharpCode.SharpZipLib/Encryption/ZipAESEncryptionStream.cs
@@ -0,0 +1,142 @@
using System;
using System.IO;
using System.Security.Cryptography;

namespace ICSharpCode.SharpZipLib.Encryption
{
/// <summary>
/// Encrypts AES ZIP entries.
/// </summary>
/// <remarks>
/// Based on information from http://www.winzip.com/aes_info.htm
/// and http://www.gladman.me.uk/cryptography_technology/fileencrypt/
/// </remarks>
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();

/// <summary>
/// Constructor
/// </summary>
/// <param name="stream">The stream on which to perform the cryptographic transformation.</param>
/// <param name="rawPassword">The password used to encrypt the entry.</param>
/// <param name="saltLength">The length of the salt to use.</param>
/// <param name="blockSize">The block size to use for transforming.</param>
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(); }

/// <summary>
/// When the stream is disposed, write the final blocks and AES Authentication code
/// </summary>
protected override void Dispose(bool disposing)
{
if (this.transform != null)
{
this.WriteAuthCode();
this.transform.Dispose();
this.transform = null;
}
}

// <inheritdoc/>
public override void Flush()
{
this.outputStream.Flush();
}

// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
{
// ZipAESEncryptionStream is only used for encryption.
throw new NotImplementedException();
}

// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
// We don't support seeking.
throw new NotImplementedException();
}

// <inheritdoc/>
public override void SetLength(long value)
{
// We don't support setting the length.
throw new NotImplementedException();
}

// <inheritdoc/>
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();
}
}
}
68 changes: 55 additions & 13 deletions src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs
Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand All @@ -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)
Expand All @@ -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)
Expand Down

0 comments on commit 896e730

Please sign in to comment.