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] AES decryption support in ZipInputStream #381

Draft
wants to merge 4 commits 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
2 changes: 1 addition & 1 deletion src/ICSharpCode.SharpZipLib/Encryption/ZipAESStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public ZipAESStream(Stream stream, ZipAESTransform transform, CryptoStreamMode m
}

// The final n bytes of the AES stream contain the Auth Code.
private const int AUTH_CODE_LENGTH = 10;
private const int AUTH_CODE_LENGTH = Zip.ZipConstants.AESAuthCodeLength;

// Blocksize is always 16 here, even for AES-256 which has transform.InputBlockSize of 32.
private const int CRYPTO_BLOCK_SIZE = 16;
Expand Down
4 changes: 1 addition & 3 deletions src/ICSharpCode.SharpZipLib/Encryption/ZipAESTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ static class HashAlgorithmName
}
#endif

private const int PWD_VER_LENGTH = 2;

// WinZip use iteration count of 1000 for PBKDF2 key generation
private const int KEY_ROUNDS = 1000;

Expand Down Expand Up @@ -84,7 +82,7 @@ public ZipAESTransform(string key, byte[] saltBytes, int blockSize, bool writeMo

// Use empty IV for AES
_encryptor = rm.CreateEncryptor(key1bytes, new byte[16]);
_pwdVerifier = pdb.GetBytes(PWD_VER_LENGTH);
_pwdVerifier = pdb.GetBytes(Zip.ZipConstants.AESPasswordVerifyLength);
//
_hmacsha1 = IncrementalHash.CreateHMAC(HashAlgorithmName.SHA1, key2bytes);
_writeMode = writeMode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ public long Skip(long count)
/// <summary>
/// Clear any cryptographic state.
/// </summary>
protected void StopDecrypting()
protected virtual void StopDecrypting()
{
inputBuffer.CryptoTransform = null;
}
Expand Down
10 changes: 10 additions & 0 deletions src/ICSharpCode.SharpZipLib/Zip/ZipConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,16 @@ public static class ZipConstants
[Obsolete("Use CryptoHeaderSize instead")]
public const int CRYPTO_HEADER_SIZE = 12;

/// <summary>
/// The number of bytes in the WinZipAes Auth Code.
/// </summary>
internal const int AESAuthCodeLength = 10;

/// <summary>
/// The number of bytes in the password verifier for WinZipAes.
/// </summary>
internal const int AESPasswordVerifyLength = 2;

/// <summary>
/// The size of the Zip64 central directory locator.
/// </summary>
Expand Down
121 changes: 98 additions & 23 deletions src/ICSharpCode.SharpZipLib/Zip/ZipInputStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ public class ZipInputStream : InflaterInputStream
private ZipEntry entry;

private long size;
private CompressionMethod method;
private int flags;
private string password;

Expand Down Expand Up @@ -191,7 +190,7 @@ public ZipEntry GetNextEntry()
var versionRequiredToExtract = (short)inputBuffer.ReadLeShort();

flags = inputBuffer.ReadLeShort();
method = (CompressionMethod)inputBuffer.ReadLeShort();
var method = (CompressionMethod)inputBuffer.ReadLeShort();
var dostime = (uint)inputBuffer.ReadLeInt();
int crc2 = inputBuffer.ReadLeInt();
csize = inputBuffer.ReadLeInt();
Expand Down Expand Up @@ -265,9 +264,20 @@ public ZipEntry GetNextEntry()
size = entry.Size;
}

if (method == CompressionMethod.Stored && (!isCrypted && csize != size || (isCrypted && csize - ZipConstants.CryptoHeaderSize != size)))
if (method == CompressionMethod.Stored)
{
if (!isCrypted && csize != size || (isCrypted && csize - ZipConstants.CryptoHeaderSize != size))
{
throw new ZipException("Stored, but compressed != uncompressed");
}
}
else if (method == CompressionMethod.WinZipAES && entry.CompressionMethod == CompressionMethod.Stored)
{
throw new ZipException("Stored, but compressed != uncompressed");
var sizeWithoutAesOverhead = csize - (entry.AESSaltLen + ZipConstants.AESPasswordVerifyLength + ZipConstants.AESAuthCodeLength);
if (sizeWithoutAesOverhead != size)
{
throw new ZipException("Stored, but compressed != uncompressed");
}
}

// Determine how to handle reading of data if this is attempted.
Expand Down Expand Up @@ -309,6 +319,40 @@ private void ReadDataDescriptor()
entry.Size = size;
}

/// <summary>
/// Complete any decryption processing and clear any cryptographic state.
/// </summary>
protected override void StopDecrypting()
{
base.StopDecrypting();

if (entry.AESKeySize != 0)
{
byte[] authBytes = new byte[ZipConstants.AESAuthCodeLength];
int authBytesRead = inputBuffer.ReadRawBuffer(authBytes, 0, authBytes.Length);

if (authBytesRead < ZipConstants.AESAuthCodeLength)
{
throw new Exception("Internal error missed auth code"); // Coding bug
// Final block done. Check Auth code.
}

/*
byte[] calcAuthCode = (this.cryptoTransform as ZipAESTransform).GetAuthCode();
for (int i = 0; i < ZipConstants.AESAuthCodeLength; i++)
{
if (calcAuthCode[i] != authBytes[i])
{
// throw new Exception("AES Authentication Code does not match. This is a super-CRC check on the data in the file after compression and encryption. \r\n"
// + "The file may be damaged.");
}
}
*/

// Dispose the transform?
}
}

/// <summary>
/// Complete cleanup as the final part of closing.
/// </summary>
Expand All @@ -332,7 +376,7 @@ private void CompleteCloseEntry(bool testCrc)

crc.Reset();

if (method == CompressionMethod.Deflated)
if (entry.CompressionMethod == CompressionMethod.Deflated)
{
inf.Reset();
}
Expand Down Expand Up @@ -360,7 +404,7 @@ public void CloseEntry()
return;
}

if (method == CompressionMethod.Deflated)
if (entry.CompressionMethod == CompressionMethod.Deflated)
{
if ((flags & 8) != 0)
{
Expand Down Expand Up @@ -500,27 +544,58 @@ private int InitialRead(byte[] destination, int offset, int count)
throw new ZipException("No password set.");
}

// Generate and set crypto transform...
var managed = new PkzipClassicManaged();
byte[] key = PkzipClassic.GenerateKeys(ZipStrings.ConvertToArray(password));
if (entry.AESKeySize == 0)
{
// Generate and set crypto transform...
var managed = new PkzipClassicManaged();
byte[] key = PkzipClassic.GenerateKeys(ZipStrings.ConvertToArray(password));

inputBuffer.CryptoTransform = managed.CreateDecryptor(key, null);
inputBuffer.CryptoTransform = managed.CreateDecryptor(key, null);

byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize];
inputBuffer.ReadClearTextBuffer(cryptbuffer, 0, ZipConstants.CryptoHeaderSize);
byte[] cryptbuffer = new byte[ZipConstants.CryptoHeaderSize];
inputBuffer.ReadClearTextBuffer(cryptbuffer, 0, ZipConstants.CryptoHeaderSize);

if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue)
{
throw new ZipException("Invalid password");
}
if (cryptbuffer[ZipConstants.CryptoHeaderSize - 1] != entry.CryptoCheckValue)
{
throw new ZipException("Invalid password");
}

if (csize >= ZipConstants.CryptoHeaderSize)
{
csize -= ZipConstants.CryptoHeaderSize;
if (csize >= ZipConstants.CryptoHeaderSize)
{
csize -= ZipConstants.CryptoHeaderSize;
}
else if ((entry.Flags & (int)GeneralBitFlags.Descriptor) == 0)
{
throw new ZipException(string.Format("Entry compressed size {0} too small for encryption", csize));
}
}
else if ((entry.Flags & (int)GeneralBitFlags.Descriptor) == 0)
else
{
throw new ZipException(string.Format("Entry compressed size {0} too small for encryption", csize));
int saltLen = entry.AESSaltLen;
byte[] saltBytes = new byte[saltLen];
int saltIn = inputBuffer.ReadRawBuffer(saltBytes, 0, saltLen);

if (saltIn != saltLen)
throw new ZipException("AES Salt expected " + saltLen + " got " + saltIn);

//
byte[] pwdVerifyRead = new byte[ZipConstants.AESPasswordVerifyLength];
int pwdBytesRead = inputBuffer.ReadRawBuffer(pwdVerifyRead, 0, pwdVerifyRead.Length);

if (pwdBytesRead != pwdVerifyRead.Length)
throw new EndOfStreamException();

int blockSize = entry.AESKeySize / 8; // bits to bytes

var decryptor = new ZipAESTransform(password, saltBytes, blockSize, false);
byte[] pwdVerifyCalc = decryptor.PwdVerifier;
if (pwdVerifyCalc[0] != pwdVerifyRead[0] || pwdVerifyCalc[1] != pwdVerifyRead[1])
throw new ZipException("Invalid password for AES");

// The AES data has saltLen+AESPasswordVerifyLength bytes as a header, and AESAuthCodeLength bytes
// as a footer.
csize -= (saltLen + ZipConstants.AESPasswordVerifyLength + ZipConstants.AESAuthCodeLength);
inputBuffer.CryptoTransform = decryptor;
}
}
else
Expand All @@ -530,7 +605,7 @@ private int InitialRead(byte[] destination, int offset, int count)

if ((csize > 0) || ((flags & (int)GeneralBitFlags.Descriptor) != 0))
{
if ((method == CompressionMethod.Deflated) && (inputBuffer.Available > 0))
if ((entry.CompressionMethod == CompressionMethod.Deflated) && (inputBuffer.Available > 0))
{
inputBuffer.SetInflaterInput(inf);
}
Expand Down Expand Up @@ -612,7 +687,7 @@ private int BodyRead(byte[] buffer, int offset, int count)

bool finished = false;

switch (method)
switch (entry.CompressionMethod)
{
case CompressionMethod.Deflated:
count = base.Read(buffer, offset, count);
Expand Down
38 changes: 38 additions & 0 deletions test/ICSharpCode.SharpZipLib.Tests/Zip/ZipEncryptionHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,44 @@ public void ZipFileAesDecryption()
}
}

/// <summary>
/// Tests for reading encrypted entries using ZipInputStream.
/// </summary>
/// <param name="aesKeySize"></param>
[Test]
[Category("Encryption")]
[Category("Zip")]
[TestCase(0, CompressionMethod.Deflated)]
[TestCase(0, CompressionMethod.Stored)]
[TestCase(128, CompressionMethod.Deflated)]
[TestCase(128, CompressionMethod.Stored)]
[TestCase(256, CompressionMethod.Deflated)]
[TestCase(256, CompressionMethod.Stored)]
public void ZipInputStreamDecryption(int aesKeySize, CompressionMethod compressionMethod)
{
var password = "password";

using (var ms = new MemoryStream())
{
WriteEncryptedZipToStream(ms, password, aesKeySize, compressionMethod);
ms.Seek(0, SeekOrigin.Begin);

using (var zis = new ZipInputStream(ms))
{
zis.IsStreamOwner = false;
zis.Password = password;

var hmm = zis.GetNextEntry();

using (var sr = new StreamReader(zis, Encoding.UTF8))
{
var content = sr.ReadToEnd();
Assert.AreEqual(DummyDataString, content, "Decompressed content does not match input data");
}
}
}
}

[Test]
[Category("Encryption")]
[Category("Zip")]
Expand Down