Skip to content

Add password to ZipArchive #1545

@neridonk

Description

@neridonk

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.
  • ExtractToDirectory can only work with archives where all encrypted entries use the same password. Mixed password scenarios require entry-by-entry handling via ZipArchiveEntry.Open(password).

Open Questions

  1. 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?
  2. 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?

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 InvalidDataException if any entry is not encrypted or if the password is incorrect.

Open(Async)(password) / Open(Async)(access, password) — Decryption:

  • Throws InvalidDataException if entry is not encrypted.
  • Throws InvalidDataException if password is incorrect.
  • Throws InvalidOperationException in Create mode (no entries to read).

Open(Async)(password, encryptionMethod) / Open(Async)(access, password, encryptionMethod) — Encryption:

  • Create mode only; throws InvalidOperationException if opening in Read mode and InvalidDataException in Update mode.
  • Throws InvalidOperationException if password is null or empty.
  • Throws PlatformNotSupportedException for 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");

Metadata

Metadata

Labels

api-ready-for-reviewAPI is ready for review, it is NOT ready for implementationarea-System.IO.Compressionin-prThere is an active PR which will close this issue when it is merged

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions