Skip to content
This repository has been archived by the owner on May 15, 2024. It is now read-only.

Performance improvement for zipping with AES encryption #273

Closed
wants to merge 9 commits into from
221 changes: 221 additions & 0 deletions src/Zip Tests/WinZipAesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

using Ionic.Zip;
using Ionic.Zip.Tests.Utilities;
using System.Linq;

namespace Ionic.Zip.Tests.WinZipAes
{
Expand Down Expand Up @@ -611,6 +612,226 @@ public void WZA_CreateZip_NoCompression()
}
}

[TestMethod]
public void WZA_CreateZip_MultipleEntriesDifferentPasswords()
{
if (!WinZipIsPresent)
throw new Exception("no winzip! [WZA_CreateZip_MultipleEntriesDifferentPasswords]");

string zipFileToCreate = "WZA_CreateZip_MultipleEntriesDifferentPasswords.zip";

string[] passwords =
{
TestUtilities.GenerateRandomPassword(),
TestUtilities.GenerateRandomPassword(),
TestUtilities.GenerateRandomPassword()
};

TestContext.WriteLine("=======================================");
TestContext.WriteLine("Creating file {0}", zipFileToCreate);
TestContext.WriteLine(" Passwords: {0}", string.Join(", ", passwords));
int entries = _rnd.Next(21) + 5;

string filename = null;
var checksums = new Dictionary<string, string>();
using (ZipFile zip1 = new ZipFile())
{
zip1.Encryption = EncryptionAlgorithm.WinZipAes256;
zip1.CompressionLevel = Ionic.Zlib.CompressionLevel.None;

for (int i = 0; i < entries; i++)
{
zip1.Password = passwords[i % 3];
if (_rnd.Next(2) == 1)
{
filename = Path.Combine(TopLevelDir, String.Format("Data{0}.bin", i));
int filesize = _rnd.Next(144000) + 5000;
TestUtilities.CreateAndFillFileBinary(filename, filesize);
}
else
{
filename = Path.Combine(TopLevelDir, String.Format("Data{0}.txt", i));
int filesize = _rnd.Next(144000) + 5000;
TestUtilities.CreateAndFillFileText(filename, filesize);
}
zip1.AddFile(filename, "");

var chk = TestUtilities.ComputeChecksum(filename);
checksums.Add(Path.GetFileName(filename), TestUtilities.CheckSumToString(chk));
}

zip1.Comment = String.Format("This archive uses Encryption({0}) passwords({1}) no compression.", zip1.Encryption, string.Join(", ", passwords));
zip1.Save(zipFileToCreate);
}

// validate all the checksums
using (ZipFile zip2 = ZipFile.Read(zipFileToCreate))
{
int i = 0;
foreach (ZipEntry e in zip2)
{
if (!e.IsDirectory)
{
Assert.AreEqual<short>(0, (short)e.CompressionMethod);
e.ExtractWithPassword("unpack", passwords[i % 3]);
string PathToExtractedFile = Path.Combine("unpack", e.FileName);
Assert.IsTrue(checksums.ContainsKey(e.FileName));

// verify the checksum of the file is correct
string expectedCheckString = checksums[e.FileName];
string actualCheckString = TestUtilities.CheckSumToString(TestUtilities.ComputeChecksum(PathToExtractedFile));
Assert.AreEqual<String>(expectedCheckString, actualCheckString, "Unexpected checksum on extracted filesystem file ({0}).", PathToExtractedFile);
i++;
}
}
}
}

[TestMethod]
public void WZA_CreateZip_MultipleEntriesDifferentEncryption()
{
if (!WinZipIsPresent)
throw new Exception("no winzip! [WZA_CreateZip_MultipleEntriesDifferentEncryption]");

string zipFileToCreate = "WZA_CreateZip_MultipleEntriesDifferentEncryption.zip";

string password = TestUtilities.GenerateRandomPassword();

TestContext.WriteLine("=======================================");
TestContext.WriteLine("Creating file {0}", zipFileToCreate);
TestContext.WriteLine(" Password: {0}", password);
int entries = _rnd.Next(21) + 5;

string filename = null;
var checksums = new Dictionary<string, string>();
using (ZipFile zip1 = new ZipFile())
{
zip1.Password = password;
zip1.CompressionLevel = Ionic.Zlib.CompressionLevel.None;

for (int i = 0; i < entries; i++)
{
zip1.Encryption = i % 2 == 0 ? EncryptionAlgorithm.WinZipAes256 : EncryptionAlgorithm.WinZipAes128;
if (_rnd.Next(2) == 1)
{
filename = Path.Combine(TopLevelDir, String.Format("Data{0}.bin", i));
int filesize = _rnd.Next(144000) + 5000;
TestUtilities.CreateAndFillFileBinary(filename, filesize);
}
else
{
filename = Path.Combine(TopLevelDir, String.Format("Data{0}.txt", i));
int filesize = _rnd.Next(144000) + 5000;
TestUtilities.CreateAndFillFileText(filename, filesize);
}
zip1.AddFile(filename, "");

var chk = TestUtilities.ComputeChecksum(filename);
checksums.Add(Path.GetFileName(filename), TestUtilities.CheckSumToString(chk));
}

zip1.Comment = String.Format("This archive uses Encryption({0} or {1}) password({1}) no compression.", EncryptionAlgorithm.WinZipAes256, EncryptionAlgorithm.WinZipAes128, password);
zip1.Save(zipFileToCreate);
}

BasicVerifyZip(zipFileToCreate, password);

// validate all the checksums
using (ZipFile zip2 = ZipFile.Read(zipFileToCreate))
{
foreach (ZipEntry e in zip2)
{
if (!e.IsDirectory)
{
Assert.AreEqual<short>(0, (short)e.CompressionMethod);
e.ExtractWithPassword("unpack", password);
string PathToExtractedFile = Path.Combine("unpack", e.FileName);
Assert.IsTrue(checksums.ContainsKey(e.FileName));

// verify the checksum of the file is correct
string expectedCheckString = checksums[e.FileName];
string actualCheckString = TestUtilities.CheckSumToString(TestUtilities.ComputeChecksum(PathToExtractedFile));
Assert.AreEqual<String>(expectedCheckString, actualCheckString, "Unexpected checksum on extracted filesystem file ({0}).", PathToExtractedFile);
}
}
}
}

[TestMethod]
public void WZA_CreateZip_MultipleEntriesDifferentEncryptionAndPasswords()
{
if (!WinZipIsPresent)
throw new Exception("no winzip! [WZA_CreateZip_MultipleEntriesDifferentEncryptionAndPasswords]");

string zipFileToCreate = "WZA_CreateZip_MultipleEntriesDifferentEncryptionAndPasswords.zip";

string[] passwords =
{
TestUtilities.GenerateRandomPassword(),
TestUtilities.GenerateRandomPassword(),
TestUtilities.GenerateRandomPassword()
};

TestContext.WriteLine("=======================================");
TestContext.WriteLine("Creating file {0}", zipFileToCreate);
TestContext.WriteLine(" Passwords: {0}", string.Join(", ", passwords));
int entries = _rnd.Next(21) + 5;

string filename = null;
var checksums = new Dictionary<string, string>();
using (ZipFile zip1 = new ZipFile())
{
zip1.CompressionLevel = Ionic.Zlib.CompressionLevel.None;

for (int i = 0; i < entries; i++)
{
zip1.Encryption = i % 2 == 0 ? EncryptionAlgorithm.WinZipAes256 : EncryptionAlgorithm.WinZipAes128;
zip1.Password = passwords[i % 3];
if (_rnd.Next(2) == 1)
{
filename = Path.Combine(TopLevelDir, String.Format("Data{0}.bin", i));
int filesize = _rnd.Next(144000) + 5000;
TestUtilities.CreateAndFillFileBinary(filename, filesize);
}
else
{
filename = Path.Combine(TopLevelDir, String.Format("Data{0}.txt", i));
int filesize = _rnd.Next(144000) + 5000;
TestUtilities.CreateAndFillFileText(filename, filesize);
}
zip1.AddFile(filename, "");

var chk = TestUtilities.ComputeChecksum(filename);
checksums.Add(Path.GetFileName(filename), TestUtilities.CheckSumToString(chk));
}

zip1.Comment = String.Format("This archive uses Encryption({0} or {1}) password({1}) no compression.", EncryptionAlgorithm.WinZipAes256, EncryptionAlgorithm.WinZipAes128, string.Join(", ", passwords));
zip1.Save(zipFileToCreate);
}

// validate all the checksums
using (ZipFile zip2 = ZipFile.Read(zipFileToCreate))
{
int i = 0;
foreach (ZipEntry e in zip2)
{
if (!e.IsDirectory)
{
Assert.AreEqual<short>(0, (short)e.CompressionMethod);
e.ExtractWithPassword("unpack", passwords[i % 3]);
string PathToExtractedFile = Path.Combine("unpack", e.FileName);
Assert.IsTrue(checksums.ContainsKey(e.FileName));

// verify the checksum of the file is correct
string expectedCheckString = checksums[e.FileName];
string actualCheckString = TestUtilities.CheckSumToString(TestUtilities.ComputeChecksum(PathToExtractedFile));
Assert.AreEqual<String>(expectedCheckString, actualCheckString, "Unexpected checksum on extracted filesystem file ({0}).", PathToExtractedFile);
i++;
}
}
}
}

[TestMethod]
public void WZA_CreateZip_Spanned()
{
Expand Down
33 changes: 30 additions & 3 deletions src/Zip.Shared/ZipEntry.Write.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@


using System;
using System.Collections.Generic;
using System.IO;
using RE = System.Text.RegularExpressions;

Expand Down Expand Up @@ -2085,7 +2086,30 @@ private void OnZipErrorWhileSaving(Exception e)
_ioOperationCanceled = _container.ZipFile.OnZipErrorSaving(this, e);
}

#if AESCRYPTO
internal WinZipAesCrypto CalculateAesCryptoKey(Dictionary<Tuple<EncryptionAlgorithm, string>, WinZipAesCrypto> preCalculatedKeys) {
if (this.Password == null) { return null; }

WinZipAesCrypto aesCryptoKey;

Tuple<EncryptionAlgorithm, string> strengthAndPasswordDictKey = new Tuple<EncryptionAlgorithm, string>(this.Encryption, this.Password);
bool alreadyCalculatedKey = preCalculatedKeys.TryGetValue(strengthAndPasswordDictKey, out aesCryptoKey);

if(!alreadyCalculatedKey) {
int keystrength = GetKeyStrengthInBits(this.Encryption);
aesCryptoKey = WinZipAesCrypto.Generate(this.Password, keystrength);
preCalculatedKeys[strengthAndPasswordDictKey] = aesCryptoKey;
}

return aesCryptoKey;
}

internal void Write(Stream outstream, WinZipAesCrypto aesCrypto_forWrite)
{
_aesCrypto_forWrite = aesCrypto_forWrite;
Write(outstream);
}
#endif

internal void Write(Stream s)
{
Expand Down Expand Up @@ -2395,14 +2419,17 @@ internal void WriteSecurityMetadata(Stream outstream)
// preceded by a variable-sized Salt and a 2-byte "password
// verification" value for the entry.

int keystrength = GetKeyStrengthInBits(Encryption);
_aesCrypto_forWrite = WinZipAesCrypto.Generate(pwd, keystrength);
if (_aesCrypto_forWrite == null)
{
int keystrength = GetKeyStrengthInBits(Encryption);
_aesCrypto_forWrite = WinZipAesCrypto.Generate(pwd, keystrength);
}
outstream.Write(_aesCrypto_forWrite.Salt, 0, _aesCrypto_forWrite._Salt.Length);
outstream.Write(_aesCrypto_forWrite.GeneratedPV, 0, _aesCrypto_forWrite.GeneratedPV.Length);
_LengthOfHeader += _aesCrypto_forWrite._Salt.Length + _aesCrypto_forWrite.GeneratedPV.Length;

TraceWriteLine("WriteSecurityMetadata: AES e({0}) keybits({1}) _LOH({2})",
FileName, keystrength, _LengthOfHeader);
FileName, _aesCrypto_forWrite._KeyStrengthInBits, _LengthOfHeader);

}
#endif
Expand Down
18 changes: 18 additions & 0 deletions src/Zip.Shared/ZipFile.Save.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,30 @@ public void Save()

// write an entry in the zip for each file
int n = 0;
#if AESCRYPTO
Dictionary<Tuple<EncryptionAlgorithm,string>, WinZipAesCrypto> sharedEncryptionKeys = new Dictionary<Tuple<EncryptionAlgorithm, string>, WinZipAesCrypto>();
#endif

// workitem 9831
ICollection<ZipEntry> c = (SortEntriesBeforeSaving) ? EntriesSorted : Entries;
foreach (ZipEntry e in c) // _entries.Values
{
OnSaveEntry(n, e, true);
#if AESCRYPTO
if (e.Encryption == EncryptionAlgorithm.WinZipAes128 ||
e.Encryption == EncryptionAlgorithm.WinZipAes256) {

WinZipAesCrypto sharedEncryptionKey = e.CalculateAesCryptoKey(sharedEncryptionKeys);
e.Write(WriteStream, sharedEncryptionKey);
}
else
{
e.Write(WriteStream);
}
#else
e.Write(WriteStream);
#endif

if (_saveOperationCanceled)
break;

Expand Down