Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add AES encryption support to ZipFile #443

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
140 changes: 140 additions & 0 deletions src/ICSharpCode.SharpZipLib/Encryption/ZipAESEncryptionStream.cs
@@ -0,0 +1,140 @@
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 readonly 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
_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();
}
}
}
53 changes: 44 additions & 9 deletions src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs
Expand Up @@ -1879,10 +1879,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 @@ -2171,6 +2171,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 @@ -2294,6 +2300,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 @@ -2348,6 +2359,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 @@ -3723,9 +3750,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 @@ -3737,7 +3771,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 @@ -3748,8 +3782,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
2 changes: 2 additions & 0 deletions test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs
Expand Up @@ -1581,6 +1581,7 @@ public void HostSystemPersistedFromZipFile()
/// Refs https://github.com/icsharpcode/SharpZipLib/issues/385
/// Trying to add an AES Encrypted entry to ZipFile should throw as it isn't supported
/// </summary>
#if false
[Test]
[Category("Zip")]
public void AddingAnAESEncryptedEntryShouldThrow()
Expand All @@ -1598,6 +1599,7 @@ public void AddingAnAESEncryptedEntryShouldThrow()
Assert.That(exception.Message, Is.EqualTo("Creation of AES encrypted entries is not supported"));
}
}
#endif

/// <summary>
/// Test that we can add a file entry and set the name to sometihng other than the name of the file.
Expand Down