-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
Background and Motivation
This feature introduces password support for ZIP archives, enabling secure encryption and decryption of archive entries. The proposal adds support for industry-standard encryption methods (ZipCrypto and WinZip AES) while maintaining backward compatibility through optional overloads.
- All password-related parameters are optional overloads, ensuring existing APIs remain unchanged
- A single archive can contain entries encrypted with different passwords and/or encryption methods
- Both synchronous and asynchronous operations are supported
- Supports industry-standard encryption methods: ZipCrypto (traditional) and WinZip AES (128-bit, 192-bit, and 256-bit variants)
Platform Notes:
- WinZip AES encryption (
Aes128,Aes192,Aes256) is not supported on browser platforms due to the unavailability of required cryptography algorithm on the platform (AES).
Archive Structure:
- A single archive can contain a mix of plain (unencrypted) entries, entries encrypted with different passwords, and entries encrypted with different encryption methods.
ExtractToDirectorycan only work with archives where all encrypted entries use the same password. Mixed password scenarios require entry-by-entry handling viaZipArchiveEntry.Open(password).
Open Questions
-
Lax Password Handling: When an archive contains mixed plain and encrypted entries, and
ExtractToDirectory(password)is called with a single password:- Should it succeed, extracting plain entries normally and decrypting encrypted entries with the provided password?
- Or should it throw an exception because not all entries are encrypted?
-
Open(password) on Unencrypted Entry: When calling
ZipArchiveEntry.Open(password)on an entry that is not encrypted:- Currently throws
InvalidDataException. Should this behavior be more lenient (e.g., just open the entry unencrypted)? - Or keep the strict behavior to catch user mistakes?
- Currently throws
API Proposal
namespace System.IO.Compression;
+ public enum EncryptionMethod
+ {
+ None = 0,
+ ZipCrypto = 1,
+ Aes128 = 2,
+ Aes192 = 3,
+ Aes256 = 4
+ }
public partial class ZipArchiveEntry
{
// Existing
public Stream Open() { }
public Stream Open(FileAccess access) { }
+ public Stream Open(string password) { }
+ public Stream Open(FileAccess access, string password) { }
+ public Stream Open(string password, EncryptionMethod encryptionMethod) { }
+ public Stream Open(FileAccess access, string password, EncryptionMethod encryptionMethod) { }
// Async variants
public Task<Stream> OpenAsync(CancellationToken cancellationToken = default) { }
public Task<Stream> OpenAsync(FileAccess access, CancellationToken cancellationToken = default) { }
+ public Task<Stream> OpenAsync(string password, CancellationToken cancellationToken = default) { }
+ public Task<Stream> OpenAsync(FileAccess access, string password, CancellationToken cancellationToken = default) { }
+ public Task<Stream> OpenAsync(string password, EncryptionMethod encryptionMethod, CancellationToken cancellationToken = default) { }
+ public Task<Stream> OpenAsync(FileAccess access, string password, EncryptionMethod encryptionMethod, CancellationToken cancellationToken = default) { }
}
public static class ZipFileExtensions
{
// ExtractToDirectory (file-based)
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName) { }
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, bool overwriteFiles) { }
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding) { }
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles) { }
+ public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, string password) { }
+ public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, bool overwriteFiles, string password) { }
+ public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, string password) { }
+ public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, string password) { }
public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, CancellationToken cancellationToken = default) { }
public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default) { }
public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, string password, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, bool overwriteFiles, string password, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, string password, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, string password, CancellationToken cancellationToken = default) { }
// ExtractToDirectory (stream-based)
public static void ExtractToDirectory(Stream source, string destinationDirectoryName) { }
public static void ExtractToDirectory(Stream source, string destinationDirectoryName, bool overwriteFiles) { }
public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding) { }
public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles) { }
+ public static void ExtractToDirectory(Stream source, string destinationDirectoryName, string password) { }
+ public static void ExtractToDirectory(Stream source, string destinationDirectoryName, bool overwriteFiles, string password) { }
+ public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, string password) { }
+ public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, string password) { }
public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, CancellationToken cancellationToken = default) { }
public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default) { }
public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, string password, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, bool overwriteFiles, string password, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, string password, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, string password, CancellationToken cancellationToken = default) { }
// CreateEntryFromFile
public static ZipArchiveEntry CreateEntryFromFile(ZipArchive destination, string sourceFileName, string entryName) { }
public static ZipArchiveEntry CreateEntryFromFile(ZipArchive destination, string sourceFileName, string entryName, CompressionLevel compressionLevel) { }
+ public static ZipArchiveEntry CreateEntryFromFile(ZipArchive destination, string sourceFileName, string entryName, string password, EncryptionMethod encryption) { }
+ public static ZipArchiveEntry CreateEntryFromFile(ZipArchive destination, string sourceFileName, string entryName, CompressionLevel compressionLevel, string password, EncryptionMethod encryption) { }
public static Task<ZipArchiveEntry> CreateEntryFromFileAsync(ZipArchive destination, string sourceFileName, string entryName, CancellationToken cancellationToken = default);
public static Task<ZipArchiveEntry> CreateEntryFromFileAsync(ZipArchive destination, string sourceFileName, string entryName, CompressionLevel compressionLevel, CancellationToken cancellationToken = default);
+ public static Task<ZipArchiveEntry> CreateEntryFromFileAsync(ZipArchive destination, string sourceFileName, string entryName, string password, EncryptionMethod encryption, CancellationToken cancellationToken = default) { }
+ public static Task<ZipArchiveEntry> CreateEntryFromFileAsync(ZipArchive destination, string sourceFileName, string entryName, CompressionLevel compressionLevel, string password, EncryptionMethod encryption, CancellationToken cancellationToken = default) { }
// ExtractToFile
public static void ExtractToFile(this ZipArchiveEntry source, string destinationFileName) { }
public static void ExtractToFile(this ZipArchiveEntry source, string destinationFileName, bool overwrite) { }
+ public static void ExtractToFile(this ZipArchiveEntry source, string destinationFileName, string password) { }
+ public static void ExtractToFile(this ZipArchiveEntry source, string destinationFileName, bool overwrite, string password) { }
public static Task ExtractToFileAsync(ZipArchiveEntry source, string destinationFileName, CancellationToken cancellationToken = default);
public static Task ExtractToFileAsync(ZipArchiveEntry source, string destinationFileName, bool overwrite, CancellationToken cancellationToken = default);
+ public static Task ExtractToFileAsync(this ZipArchiveEntry source, string destinationFileName, string password, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToFileAsync(this ZipArchiveEntry source, string destinationFileName, bool overwrite, string password, CancellationToken cancellationToken = default) { }
// ExtractToDirectory (ZipArchive extension)
public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName) { }
public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles) { }
+ public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, string password) { }
+ public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, string password) { }
public static Task ExtractToDirectoryAsync(this ZipArchive source, string destinationDirectoryName, CancellationToken cancellationToken = default) { }
public static Task ExtractToDirectoryAsync(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(this ZipArchive source, string destinationDirectoryName, string password, CancellationToken cancellationToken = default) { }
+ public static Task ExtractToDirectoryAsync(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, string password, CancellationToken cancellationToken = default) { }
}Exception Behavior
ExtractToDirectory / ExtractToFile (with password):
- If password is null/empty: falls back to passwordless extraction.
- If password is non-empty: throws
InvalidDataExceptionif any entry is not encrypted or if the password is incorrect.
Open(Async)(password) / Open(Async)(access, password) — Decryption:
- Throws
InvalidDataExceptionif entry is not encrypted. - Throws
InvalidDataExceptionif password is incorrect. - Throws
InvalidOperationExceptionin Create mode (no entries to read).
Open(Async)(password, encryptionMethod) / Open(Async)(access, password, encryptionMethod) — Encryption:
- Create mode only; throws
InvalidOperationExceptionif opening in Read mode andInvalidDataExceptionin Update mode. - Throws
InvalidOperationExceptionif password is null or empty. - Throws
PlatformNotSupportedExceptionfor AES encryption methods on browser platforms.
API Usage
Creating a password-protected archive:
using var archive = ZipFile.Open("protected.zip", ZipArchiveMode.Create);
ZipFileExtensions.CreateEntryFromFile(
archive,
"document.txt",
"document.txt",
CompressionLevel.Optimal,
"myPassword123",
EncryptionMethod.Aes256);Extracting a password-protected archive:
ZipFile.ExtractToDirectory(
"protected.zip",
"output",
entryNameEncoding: null,
overwriteFiles: true,
password: "myPassword123");Opening individual encrypted entries:
using var archive = ZipFile.OpenRead("protected.zip");
var entry = archive.GetEntry("document.txt");
using var stream = entry.Open(FileAccess.Read, "myPassword123");Async extraction with password:
using var archive = ZipFile.OpenRead("protected.zip");
await archive.ExtractToDirectoryAsync(
"output",
overwriteFiles: true,
password: "myPassword123");