Skip to content

Add password to ZipArchive #1545

@neridonk

Description

@neridonk

ZIP Archive Encryption API Proposal

Background and Motivation

This feature introduces password support for ZIP archives, enabling encryption and decryption of archive entries. The proposal adds support for industry-standard WinZip AES encryption and support for legacy ZipCrypto method.

  • A single archive can contain entries encrypted with different passwords and/or encryption methods
  • Both synchronous and asynchronous operations are supported
  • Supports industry-standard WinZip AES encryption methods (128-bit, 192-bit, and 256-bit variants) and for compatibility reasons also ZipCrypto
  • Password parameters use ReadOnlySpan<char> (sync) and ReadOnlyMemory<char> (async) to enable safer handling of sensitive data.
  • No defaults provided for Encryption methods, users need to always specify the encryption method they want themselves.
  • All existing helper methods are currently considered, but to avoid API bloat, I believe it is also reasonable that the password/encryption overload is added only to the most parameter-rich overload.

Platform Notes:

  • Since an AES cryptography algorithm is required, WinZip AES encryption is supported only on platforms where AES is available. That means it is not supported on browser platforms.

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).

API Proposal

namespace System.IO.Compression;

// This enum is intended purely as a user-friendly way to specify the encryption method.
public enum EncryptionMethod
{
     None = 0,
     ZipCrypto = 1,  // or possibly LegacyZipCrypto to make it clear we don't recommend it
     Aes128 = 2,
     Aes192 = 3,
     Aes256 = 4
 }

public partial class ZipArchiveEntry
{
    // Existing
    public Stream Open();
    public Stream Open(FileAccess access);
    public Task<Stream> OpenAsync(CancellationToken cancellationToken = default);
    public Task<Stream> OpenAsync(FileAccess access, CancellationToken cancellationToken = default);

    // New (decryption)
    // Opens an encrypted entry in read or update mode.
    // Throws InvalidDataException if the entry is not encrypted or the password is wrong.
    // Throws InvalidOperationException in Create mode (no existing entry to decrypt).

    public Stream Open(ReadOnlySpan<char> password);
    public Stream Open(FileAccess access, ReadOnlySpan<char> password);
    public Task<Stream> OpenAsync(ReadOnlyMemory<char> password,
        CancellationToken cancellationToken = default);
    public Task<Stream> OpenAsync(FileAccess access, ReadOnlyMemory<char> password,
        CancellationToken cancellationToken = default);

    // New (encryption)
    // Opens a new entry for writing with encryption.
    // Only valid in Create mode; throws InvalidOperationException in Read mode and InvalidDataException in Update mode.
    // Throws ArgmentException if password is empty.
    // Throws PlatuformNotSupportedException for AES methods on browser platforms.

    public Stream Open(ReadOnlySpan<char> password, ZipEncryptionMethod encryptionMethod);
    public Stream Open(FileAccess access, ReadOnlySpan<char> password,
        ZipEncryptionMethod encryptionMethod);
    public Task<Stream> OpenAsync(ReadOnlyMemory<char> password,
        ZipEncryptionMethod encryptionMethod,
        CancellationToken cancellationToken = default);
    public Task<Stream> OpenAsync(FileAccess access, ReadOnlyMemory<char> password,
        ZipEncryptionMethod encryptionMethod,
        CancellationToken cancellationToken = default);
}

public static class ZipFileExtensions
{
    // ExtractToDirectory (file-based, sync) exsiting
    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) { }

    // ExtractToDirectory (file-based, sync) new
    // Decrypts encrypted entries with the given password.
    // Throws InvalidDataException if any encrypted entry fails decryption.
    // If password is empty, falls back to passwordless extraction.

   public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, ReadOnlySpan<char> password) { }
   public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, bool overwriteFiles, ReadOnlySpan<char> password) { }
   public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, ReadOnlySpan<char> password) { }
   public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, ReadOnlySpan<char> password) { }

    // ExtractToDirectory (file-based, async) exsiting
    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) { }

    // ExtractToDirectory (file-based, async) exsiting
   public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }
   public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, bool overwriteFiles, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }
   public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }
   public static Task ExtractToDirectoryAsync(string sourceArchiveFileName, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }

    // ExtractToDirectory (stream-based, sync) existing
    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) { }

    // ExtractToDirectory (stream-based, sync) new
   public static void ExtractToDirectory(Stream source, string destinationDirectoryName, ReadOnlySpan<char> password) { }
   public static void ExtractToDirectory(Stream source, string destinationDirectoryName, bool overwriteFiles, ReadOnlySpan<char> password) { }
   public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, ReadOnlySpan<char> password) { }
   public static void ExtractToDirectory(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, ReadOnlySpan<char> password) { }

    // ExtractToDirectory (stream-based, async) existing
    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) { }

    // ExtractToDirectory (stream-based, async) new
   public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }
   public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, bool overwriteFiles, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }
   public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }
   public static Task ExtractToDirectoryAsync(Stream source, string destinationDirectoryName, Encoding? entryNameEncoding, bool overwriteFiles, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }

    // CreateEntryFromFile, sync, existing
    public static ZipArchiveEntry CreateEntryFromFile(ZipArchive destination, string sourceFileName, string entryName) { }
    public static ZipArchiveEntry CreateEntryFromFile(ZipArchive destination, string sourceFileName, string entryName, CompressionLevel compressionLevel) { }

    // CreateEntryFromFile, sync, new
   public static ZipArchiveEntry CreateEntryFromFile(ZipArchive destination, string sourceFileName, string entryName, ReadOnlySpan<char> password, EncryptionMethod encryption) { }
   public static ZipArchiveEntry CreateEntryFromFile(ZipArchive destination, string sourceFileName, string entryName, CompressionLevel compressionLevel, ReadOnlySpan<char> password, EncryptionMethod encryption) { }

    // CreateEntryFromFile, async, existing
    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);

    // CreateEntryFromFile, async, new
   public static Task<ZipArchiveEntry> CreateEntryFromFileAsync(ZipArchive destination, string sourceFileName, string entryName, ReadOnlyMemory<char> password, EncryptionMethod encryption, CancellationToken cancellationToken = default) { }
   public static Task<ZipArchiveEntry> CreateEntryFromFileAsync(ZipArchive destination, string sourceFileName, string entryName, CompressionLevel compressionLevel, ReadOnlyMemory<char> password, EncryptionMethod encryption, CancellationToken cancellationToken = default) { }

    // ExtractToFile, sync, existing
    public static void ExtractToFile(this ZipArchiveEntry source, string destinationFileName) { }
    public static void ExtractToFile(this ZipArchiveEntry source, string destinationFileName, bool overwrite) { }

    // ExtractToFile, sync, new
   public static void ExtractToFile(this ZipArchiveEntry source, string destinationFileName, ReadOnlySpan<char> password) { }
   public static void ExtractToFile(this ZipArchiveEntry source, string destinationFileName, bool overwrite, ReadOnlySpan<char> password) { }

    // ExtractToFile, async, existing
    public static Task ExtractToFileAsync(ZipArchiveEntry source, string destinationFileName, CancellationToken cancellationToken = default);
    public static Task ExtractToFileAsync(ZipArchiveEntry source, string destinationFileName, bool overwrite, CancellationToken cancellationToken = default);

    // ExtractToFile, async, new
   public static Task ExtractToFileAsync(this ZipArchiveEntry source, string destinationFileName, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }
   public static Task ExtractToFileAsync(this ZipArchiveEntry source, string destinationFileName, bool overwrite, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }

    // ExtractToDirectory (ZipArchive extension), sync, exisitng
    public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName) { }
    public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles) { }

    // ExtractToDirectory (ZipArchive extension), sync, new
   public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, ReadOnlySpan<char> password) { }
   public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, ReadOnlySpan<char> password) { }

    // ExtractToDirectory (ZipArchive extension), async, exisitng
    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) { }

    // ExtractToDirectory (ZipArchive extension), async, new
   public static Task ExtractToDirectoryAsync(this ZipArchive source, string destinationDirectoryName, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }
   public static Task ExtractToDirectoryAsync(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, ReadOnlyMemory<char> password, CancellationToken cancellationToken = default) { }
}

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 ArgumentException. Should this behavior be more lenient (e.g., just open the entry unencrypted)?
    • Or keep the strict behavior to catch user mistakes?

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:

await ZipFile.ExtractToDirectoryAsync(
    "protected.zip",
    "output",
    overwriteFiles: true,
    password: "myPassword123".AsMemory());

Using char arrays for zero-copy password handling:

char[] password = GetPasswordFromSecureSource();
try
{
    // Sync — implicit ReadOnlySpan<char> conversion
    entry.Open(password);

    // Async — explicit ReadOnlyMemory<char> conversion
    await entry.OpenAsync(password.AsMemory());
}
finally
{
    // Clear sensitive data from memory
    Array.Clear(password);
}

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-System.IO.Compressionin-prThere is an active PR which will close this issue when it is merged
No fields configured for Feature.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions