Skip to content

Commit 00033e9

Browse files
committed
Add AES encryption support to ZipFile
1 parent a11665d commit 00033e9

File tree

3 files changed

+195
-11
lines changed

3 files changed

+195
-11
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using System;
2+
using System.IO;
3+
using System.Security.Cryptography;
4+
5+
namespace ICSharpCode.SharpZipLib.Encryption
6+
{
7+
/// <summary>
8+
/// Encrypts AES ZIP entries.
9+
/// </summary>
10+
/// <remarks>
11+
/// Based on information from http://www.winzip.com/aes_info.htm
12+
/// and http://www.gladman.me.uk/cryptography_technology/fileencrypt/
13+
/// </remarks>
14+
internal class ZipAESEncryptionStream : Stream
15+
{
16+
// The transform to use for encryption.
17+
private ZipAESTransform transform;
18+
19+
// The output stream to write the encrypted data to.
20+
private readonly Stream outputStream;
21+
22+
// Static to help ensure that multiple files within a zip will get different random salt
23+
private static readonly RandomNumberGenerator _aesRnd = RandomNumberGenerator.Create();
24+
25+
/// <summary>
26+
/// Constructor
27+
/// </summary>
28+
/// <param name="stream">The stream on which to perform the cryptographic transformation.</param>
29+
/// <param name="rawPassword">The password used to encrypt the entry.</param>
30+
/// <param name="saltLength">The length of the salt to use.</param>
31+
/// <param name="blockSize">The block size to use for transforming.</param>
32+
public ZipAESEncryptionStream(Stream stream, string rawPassword, int saltLength, int blockSize)
33+
{
34+
// Set up stream.
35+
this.outputStream = stream;
36+
37+
// Initialise the encryption transform.
38+
var salt = new byte[saltLength];
39+
40+
// Salt needs to be cryptographically random, and unique per file
41+
_aesRnd.GetBytes(salt);
42+
43+
this.transform = new ZipAESTransform(rawPassword, salt, blockSize, true);
44+
45+
// File format for AES:
46+
// Size (bytes) Content
47+
// ------------ -------
48+
// Variable Salt value
49+
// 2 Password verification value
50+
// Variable Encrypted file data
51+
// 10 Authentication code
52+
//
53+
// Value in the "compressed size" fields of the local file header and the central directory entry
54+
// is the total size of all the items listed above. In other words, it is the total size of the
55+
// salt value, password verification value, encrypted data, and authentication code.
56+
var pwdVerifier = this.transform.PwdVerifier;
57+
this.outputStream.Write(salt, 0, salt.Length);
58+
this.outputStream.Write(pwdVerifier, 0, pwdVerifier.Length);
59+
}
60+
61+
// This stream is write only.
62+
public override bool CanRead => false;
63+
64+
// We only support writing - no seeking about.
65+
public override bool CanSeek => false;
66+
67+
// Supports writing for encrypting.
68+
public override bool CanWrite => true;
69+
70+
// We don't track this.
71+
public override long Length => throw new NotImplementedException();
72+
73+
// We don't track this, or support seeking.
74+
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
75+
76+
/// <summary>
77+
/// When the stream is disposed, write the final blocks and AES Authentication code
78+
/// </summary>
79+
protected override void Dispose(bool disposing)
80+
{
81+
if (this.transform != null)
82+
{
83+
this.WriteAuthCode();
84+
this.transform.Dispose();
85+
this.transform = null;
86+
}
87+
}
88+
89+
// <inheritdoc/>
90+
public override void Flush()
91+
{
92+
this.outputStream.Flush();
93+
}
94+
95+
// <inheritdoc/>
96+
public override int Read(byte[] buffer, int offset, int count)
97+
{
98+
// ZipAESEncryptionStream is only used for encryption.
99+
throw new NotImplementedException();
100+
}
101+
102+
// <inheritdoc/>
103+
public override long Seek(long offset, SeekOrigin origin)
104+
{
105+
// We don't support seeking.
106+
throw new NotImplementedException();
107+
}
108+
109+
// <inheritdoc/>
110+
public override void SetLength(long value)
111+
{
112+
// We don't support setting the length.
113+
throw new NotImplementedException();
114+
}
115+
116+
// <inheritdoc/>
117+
public override void Write(byte[] buffer, int offset, int count)
118+
{
119+
if (count == 0)
120+
{
121+
return;
122+
}
123+
124+
var outputBuffer = new byte[count];
125+
var outputCount = this.transform.TransformBlock(buffer, offset, count, outputBuffer, 0);
126+
this.outputStream.Write(outputBuffer, 0, outputCount);
127+
}
128+
129+
// Write the auth code for the encrypted data to the output stream
130+
private void WriteAuthCode()
131+
{
132+
// Transform the final block?
133+
134+
// Write the AES Authentication Code (a hash of the compressed and encrypted data)
135+
var authCode = this.transform.GetAuthCode();
136+
this.outputStream.Write(authCode, 0, 10);
137+
this.outputStream.Flush();
138+
}
139+
}
140+
}

src/ICSharpCode.SharpZipLib/Zip/ZipFile.cs

+53-11
Original file line numberDiff line numberDiff line change
@@ -1865,10 +1865,10 @@ public void Add(IStaticDataSource dataSource, ZipEntry entry)
18651865

18661866
// We don't currently support adding entries with AES encryption, so throw
18671867
// up front instead of failing or falling back to ZipCrypto later on
1868-
if (entry.AESKeySize > 0)
1869-
{
1870-
throw new NotSupportedException("Creation of AES encrypted entries is not supported");
1871-
}
1868+
//if (entry.AESKeySize > 0)
1869+
//{
1870+
// throw new NotSupportedException("Creation of AES encrypted entries is not supported");
1871+
//}
18721872

18731873
CheckSupportedCompressionMethod(entry.CompressionMethod);
18741874
CheckUpdating();
@@ -2159,6 +2159,12 @@ private void WriteLocalEntryHeader(ZipUpdate update)
21592159
ed.Delete(1);
21602160
}
21612161

2162+
// Write AES Data if needed
2163+
if (entry.AESKeySize > 0)
2164+
{
2165+
AddExtraDataAES(entry, ed);
2166+
}
2167+
21622168
entry.ExtraData = ed.GetEntryData();
21632169

21642170
WriteLEShort(name.Length);
@@ -2282,6 +2288,11 @@ private int WriteCentralDirectoryHeader(ZipEntry entry)
22822288
ed.Delete(1);
22832289
}
22842290

2291+
if (entry.AESKeySize > 0)
2292+
{
2293+
AddExtraDataAES(entry, ed);
2294+
}
2295+
22852296
byte[] centralExtraData = ed.GetEntryData();
22862297

22872298
WriteLEShort(centralExtraData.Length);
@@ -2336,6 +2347,22 @@ private int WriteCentralDirectoryHeader(ZipEntry entry)
23362347
return ZipConstants.CentralHeaderBaseSize + name.Length + centralExtraData.Length + rawComment.Length;
23372348
}
23382349

2350+
private static void AddExtraDataAES(ZipEntry entry, ZipExtraData extraData)
2351+
{
2352+
// Vendor Version: AE-1 IS 1. AE-2 is 2. With AE-2 no CRC is required and 0 is stored.
2353+
const int VENDOR_VERSION = 2;
2354+
// Vendor ID is the two ASCII characters "AE".
2355+
const int VENDOR_ID = 0x4541; //not 6965;
2356+
extraData.StartNewEntry();
2357+
// Pack AES extra data field see http://www.winzip.com/aes_info.htm
2358+
//extraData.AddLeShort(7); // Data size (currently 7)
2359+
extraData.AddLeShort(VENDOR_VERSION); // 2 = AE-2
2360+
extraData.AddLeShort(VENDOR_ID); // "AE"
2361+
extraData.AddData(entry.AESEncryptionStrength); // 1 = 128, 2 = 192, 3 = 256
2362+
extraData.AddLeShort((int)entry.CompressionMethod); // The actual compression method used to compress the file
2363+
extraData.AddNewEntry(0x9901);
2364+
}
2365+
23392366
#endregion Writing Values/Headers
23402367

23412368
private void PostUpdateCleanup()
@@ -2622,13 +2649,20 @@ private Stream GetOutputStream(ZipEntry entry)
26222649
switch (entry.CompressionMethod)
26232650
{
26242651
case CompressionMethod.Stored:
2625-
result = new UncompressedStream(result);
2652+
if (!entry.IsCrypted)
2653+
{
2654+
// If there is an encryption stream in use, that can be written to directly
2655+
// otherwise, wrap it in an UncompressedStream instead of returning the base stream directly
2656+
result = new UncompressedStream(result);
2657+
}
26262658
break;
26272659

26282660
case CompressionMethod.Deflated:
26292661
var dos = new DeflaterOutputStream(result, new Deflater(9, true))
26302662
{
2631-
IsStreamOwner = false
2663+
// If there is an encryption stream in use, then we want that to be disposed when the deflator stream is disposed
2664+
// If not, then we don't want it to dispose the base stream
2665+
IsStreamOwner = entry.IsCrypted
26322666
};
26332667
result = dos;
26342668
break;
@@ -3668,9 +3702,16 @@ private Stream CreateAndInitDecryptionStream(Stream baseStream, ZipEntry entry)
36683702

36693703
private Stream CreateAndInitEncryptionStream(Stream baseStream, ZipEntry entry)
36703704
{
3671-
CryptoStream result = null;
3672-
if ((entry.Version < ZipConstants.VersionStrongEncryption)
3673-
|| (entry.Flags & (int)GeneralBitFlags.StrongEncryption) == 0)
3705+
if (entry.CompressionMethodForHeader == CompressionMethod.WinZipAES)
3706+
{
3707+
int blockSize = entry.AESKeySize / 8; // bits to bytes
3708+
3709+
var aesStream =
3710+
new ZipAESEncryptionStream(baseStream, rawPassword_, entry.AESSaltLen, blockSize);
3711+
3712+
return aesStream;
3713+
}
3714+
else
36743715
{
36753716
var classicManaged = new PkzipClassicManaged();
36763717

@@ -3682,7 +3723,7 @@ private Stream CreateAndInitEncryptionStream(Stream baseStream, ZipEntry entry)
36823723

36833724
// Closing a CryptoStream will close the base stream as well so wrap it in an UncompressedStream
36843725
// which doesnt do this.
3685-
result = new CryptoStream(new UncompressedStream(baseStream),
3726+
CryptoStream result = new CryptoStream(new UncompressedStream(baseStream),
36863727
classicManaged.CreateEncryptor(key, null), CryptoStreamMode.Write);
36873728

36883729
if ((entry.Crc < 0) || (entry.Flags & 8) != 0)
@@ -3693,8 +3734,9 @@ private Stream CreateAndInitEncryptionStream(Stream baseStream, ZipEntry entry)
36933734
{
36943735
WriteEncryptionHeader(result, entry.Crc);
36953736
}
3737+
3738+
return result;
36963739
}
3697-
return result;
36983740
}
36993741

37003742
private static void CheckClassicPassword(CryptoStream classicCryptoStream, ZipEntry entry)

test/ICSharpCode.SharpZipLib.Tests/Zip/ZipFileHandling.cs

+2
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,7 @@ public void HostSystemPersistedFromZipFile()
15521552
/// Refs https://github.com/icsharpcode/SharpZipLib/issues/385
15531553
/// Trying to add an AES Encrypted entry to ZipFile should throw as it isn't supported
15541554
/// </summary>
1555+
#if false
15551556
[Test]
15561557
[Category("Zip")]
15571558
public void AddingAnAESEncryptedEntryShouldThrow()
@@ -1569,6 +1570,7 @@ public void AddingAnAESEncryptedEntryShouldThrow()
15691570
Assert.That(exception.Message, Is.EqualTo("Creation of AES encrypted entries is not supported"));
15701571
}
15711572
}
1573+
#endif
15721574

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

0 commit comments

Comments
 (0)