Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/SharpCompress/Common/Zip/PkwareTraditionalEncryptionData.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Security.Cryptography;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.Compressors.Deflate;

Expand Down Expand Up @@ -47,6 +48,44 @@ byte[] encryptionHeader
return encryptor;
}

/// <summary>
/// Creates a new PkwareTraditionalEncryptionData instance for writing encrypted data.
/// </summary>
/// <param name="password">The password to use for encryption.</param>
/// <param name="archiveEncoding">The archive encoding.</param>
/// <returns>A new encryption data instance.</returns>
public static PkwareTraditionalEncryptionData ForWrite(
string password,
IArchiveEncoding archiveEncoding
)
{
return new PkwareTraditionalEncryptionData(password, archiveEncoding);
}

/// <summary>
/// Generates the 12-byte encryption header required for PKWARE traditional encryption.
/// </summary>
/// <param name="crc">The CRC32 of the uncompressed file data, or the last modified time high byte if using data descriptors.</param>
/// <param name="lastModifiedTime">The last modified time (used as verification byte when CRC is unknown).</param>
/// <returns>The encrypted 12-byte header.</returns>
public byte[] GenerateEncryptionHeader(uint crc, ushort lastModifiedTime)
{
var header = new byte[12];

// Fill first 11 bytes with random data
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(header, 0, 11);
}

// The last byte is the verification byte - high byte of CRC, or high byte of lastModifiedTime
// When streaming (UsePostDataDescriptor), we use the time as verification
header[11] = (byte)((crc >> 24) & 0xff);

// Encrypt the header
return Encrypt(header, header.Length);
}

public byte[] Decrypt(byte[] cipherText, int length)
{
if (length > cipherText.Length)
Expand Down
205 changes: 205 additions & 0 deletions src/SharpCompress/Common/Zip/WinzipAesEncryptionStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using SharpCompress.IO;

namespace SharpCompress.Common.Zip;

/// <summary>
/// Stream that encrypts data using WinZip AES encryption and writes to an underlying stream.
/// </summary>
internal class WinzipAesEncryptionStream : Stream
{
private const int BLOCK_SIZE_IN_BYTES = 16;
private const int RFC2898_ITERATIONS = 1000;
private const int AUTH_CODE_LENGTH = 10;

private readonly Stream _stream;
private readonly SymmetricAlgorithm _cipher;
private readonly ICryptoTransform _transform;
private readonly HMACSHA1 _hmac;
private readonly byte[] _counter = new byte[BLOCK_SIZE_IN_BYTES];
private readonly byte[] _counterOut = new byte[BLOCK_SIZE_IN_BYTES];
private int _nonce = 1;
private bool _isDisposed;

internal WinzipAesEncryptionStream(Stream stream, string password, WinzipAesKeySize keySize)
{
_stream = stream;

// Generate salt
var saltLength = GetSaltLength(keySize);
var salt = new byte[saltLength];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(salt);
}

// Derive keys using PBKDF2
var keyLength = GetKeyLength(keySize);

#if NETFRAMEWORK || NETSTANDARD2_0
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
var keyBytes = rfc2898.GetBytes(keyLength);
var ivBytes = rfc2898.GetBytes(keyLength);
var passwordVerifyValue = rfc2898.GetBytes(2);
#elif NET10_0_OR_GREATER
var derivedKeySize = (keyLength * 2) + 2;
var passwordBytes = Encoding.UTF8.GetBytes(password);
var derivedKey = Rfc2898DeriveBytes.Pbkdf2(
passwordBytes,
salt,
RFC2898_ITERATIONS,
HashAlgorithmName.SHA1,
derivedKeySize
);
var keyBytes = derivedKey.AsSpan(0, keyLength).ToArray();
var ivBytes = derivedKey.AsSpan(keyLength, keyLength).ToArray();
var passwordVerifyValue = derivedKey.AsSpan(keyLength * 2, 2).ToArray();
#else
var rfc2898 = new Rfc2898DeriveBytes(
password,
salt,
RFC2898_ITERATIONS,
HashAlgorithmName.SHA1
);
Comment on lines +62 to +67

Copilot AI Jan 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'Rfc2898DeriveBytes' is created but not disposed.

Copilot uses AI. Check for mistakes.
var keyBytes = rfc2898.GetBytes(keyLength);
var ivBytes = rfc2898.GetBytes(keyLength);
var passwordVerifyValue = rfc2898.GetBytes(2);
#endif
Comment on lines +43 to +71

Copilot AI Jan 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Rfc2898DeriveBytes instance is not being disposed in the NETFRAMEWORK/NETSTANDARD2_0 and the else branch. Rfc2898DeriveBytes implements IDisposable and should be disposed after use to properly release cryptographic resources.

Wrap the Rfc2898DeriveBytes instantiation in a using statement for both branches (lines 44 and 62-67) to ensure proper resource disposal.

Copilot uses AI. Check for mistakes.

// Initialize cipher
_cipher = CreateCipher(keyBytes);
var iv = new byte[BLOCK_SIZE_IN_BYTES];
_transform = _cipher.CreateEncryptor(keyBytes, iv);

// Initialize HMAC for authentication
_hmac = new HMACSHA1(ivBytes);

// Write salt and password verification value
_stream.Write(salt, 0, salt.Length);
_stream.Write(passwordVerifyValue, 0, passwordVerifyValue.Length);
}

private static int GetSaltLength(WinzipAesKeySize keySize) =>
keySize switch
{
WinzipAesKeySize.KeySize128 => 8,
WinzipAesKeySize.KeySize192 => 12,
WinzipAesKeySize.KeySize256 => 16,
_ => throw new InvalidOperationException(),
};

private static int GetKeyLength(WinzipAesKeySize keySize) =>
keySize switch
{
WinzipAesKeySize.KeySize128 => 16,
WinzipAesKeySize.KeySize192 => 24,
WinzipAesKeySize.KeySize256 => 32,
_ => throw new InvalidOperationException(),
};

private static SymmetricAlgorithm CreateCipher(byte[] keyBytes)
{
var cipher = Aes.Create();
cipher.BlockSize = BLOCK_SIZE_IN_BYTES * 8;
cipher.KeySize = keyBytes.Length * 8;
cipher.Mode = CipherMode.ECB;
Comment thread Dismissed
cipher.Padding = PaddingMode.None;
return cipher;
}

public override bool CanRead => false;

public override bool CanSeek => false;

public override bool CanWrite => true;

public override long Length => throw new NotSupportedException();

public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}

public override void Flush() => _stream.Flush();

public override int Read(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();

public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();

public override void SetLength(long value) => throw new NotSupportedException();

public override void Write(byte[] buffer, int offset, int count)
{
if (count == 0)
{
return;
}

var encrypted = EncryptData(buffer, offset, count);
_hmac.TransformBlock(encrypted, 0, encrypted.Length, encrypted, 0);
_stream.Write(encrypted, 0, encrypted.Length);
}

private byte[] EncryptData(byte[] buffer, int offset, int count)
{
var result = new byte[count];
var posn = 0;

while (posn < count)
{
var blockSize = Math.Min(BLOCK_SIZE_IN_BYTES, count - posn);

// Update counter
BinaryPrimitives.WriteInt32LittleEndian(_counter, _nonce++);

// Encrypt counter to get key stream
_transform.TransformBlock(_counter, 0, BLOCK_SIZE_IN_BYTES, _counterOut, 0);

// XOR with plaintext
for (var i = 0; i < blockSize; i++)
{
result[posn + i] = (byte)(_counterOut[i] ^ buffer[offset + posn + i]);
}

posn += blockSize;
}

return result;
}

protected override void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;

if (disposing)
{
// Finalize HMAC and write authentication code
_hmac.TransformFinalBlock([], 0, 0);
var authCode = _hmac.Hash!;
_stream.Write(authCode, 0, AUTH_CODE_LENGTH);

_transform.Dispose();
_cipher.Dispose();
_hmac.Dispose();
_stream.Dispose();
}

base.Dispose(disposing);
}

/// <summary>
/// Gets the overhead bytes added by encryption (salt + password verification + auth code).
/// </summary>
internal static int GetEncryptionOverhead(WinzipAesKeySize keySize) =>
GetSaltLength(keySize) + 2 + AUTH_CODE_LENGTH;
}
30 changes: 30 additions & 0 deletions src/SharpCompress/Common/Zip/ZipEncryptionType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace SharpCompress.Common.Zip;

/// <summary>
/// Specifies the encryption method to use when creating encrypted ZIP archives.
/// </summary>
public enum ZipEncryptionType
{
/// <summary>
/// No encryption.
/// </summary>
None = 0,

/// <summary>
/// PKWARE Traditional (ZipCrypto) encryption.
/// This is the older, less secure encryption method but is widely compatible.
/// </summary>
PkwareTraditional = 1,

/// <summary>
/// WinZip AES-256 encryption.
/// This is the more secure encryption method using AES-256.
/// </summary>
Aes256 = 2,

/// <summary>
/// WinZip AES-128 encryption.
/// This uses AES-128 for encryption.
/// </summary>
Aes128 = 3,
}
4 changes: 2 additions & 2 deletions src/SharpCompress/Common/Zip/ZipHeaderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ private void LoadHeader(ZipFileEntry entryHeader, Stream stream)

var salt = new byte[WinzipAesEncryptionData.KeyLengthInBytes(keySize) / 2];
var passwordVerifyValue = new byte[2];
stream.Read(salt, 0, salt.Length);
stream.Read(passwordVerifyValue, 0, 2);
stream.ReadFully(salt);
stream.ReadFully(passwordVerifyValue);
entryHeader.WinzipAesEncryptionData = new WinzipAesEncryptionData(
keySize,
salt,
Expand Down
Loading
Loading