man/internal/ENCRYPTION.md

Password / encryption support in the zip package

This is the internal reference for how the package encrypts and (eventually) decrypts ZIP entries. It documents the on-disk format, the C and R layers, and the design decisions behind them.

Scheme: WinZip AES (method 99)

The package implements WinZip AES encryption, the de-facto standard read by 7-Zip, WinZip, modern Info-ZIP and macOS Archive Utility. It is real crypto:

The legacy traditional-PKWARE cipher ("ZipCrypto") is not implemented; encryption = "zipcrypto" raises an error in R. It is cryptographically broken and only kept on the roadmap for completeness.

On-disk layout of an encrypted entry

In the ZIP format, encryption is applied to the already-compressed bytes, so the pipeline is just an extra stage around miniz's deflate/inflate:

write:  plaintext --deflate(miniz)--> compressed --encrypt--> entry payload
read:   entry payload --decrypt--> compressed --inflate(miniz)--> plaintext

A WinZip AES entry uses compression method 99 (0x63) and sets bit 0 of the general-purpose flag word (the "encrypted" bit; we also set the UTF-8 bit 0x800). The real compression method (0 = store, 8 = deflate) and the AES parameters live in an extra field with header id 0x9901, present in both the local header and the central directory record:

0x9901 | datasize(2)=7 | vendor version(2) | vendor id "AE"(2) |
        AES strength(1: 1=128, 2=192, 3=256) | real compression method(2)

The entry payload (what follows the local header + filename + extra field) is:

salt (8/12/16 bytes for AES-128/192/256) || 2-byte password verifier ||
ciphertext || 10-byte HMAC-SHA1 authentication code

We emit AE-2 (vendor version 2), which stores the CRC-32 as 0 in the headers and relies on the HMAC for integrity. (AE-1 would instead keep the real CRC.)

Code map

Crypto primitives — crypto.c / crypto.h

Built on the vendored Mbed TLS subset (see mbedtls/VENDORING.md); no hand-written crypto. R-free, so it can also link into cmdzip/cmdunzip.

Hidden .Call test shims (R_crypto_* in rzip.c) exercise these against published vectors in tests/testthat/test-crypto.R.

miniz primitive — miniz.c (mz_zip_writer_add_mem_raw)

miniz's own writer hardcodes the compression method and never sets the encrypted bit, so it cannot emit a method-99 entry. The added function stores a caller-framed payload verbatim with an explicit method, general-purpose bit flags, CRC-32 and uncompressed size, plus the local + central 0x9901 extra field. Unlike mz_zip_writer_add_mem_ex_v2 it writes the real sizes into the local header and emits no data descriptor. This is a local patch to the vendored miniz, tracked in ../tools/extra/miniz.patch.

Writer — zip.c (zip_writer_add_aes, zip_winzip_extra_field)

For each file entry (when a password is set): raw-deflate the plaintext (falling back to stored if it does not shrink, mirroring miniz), generate a random salt, derive the keys + verifier, AES-CTR encrypt, compute the HMAC auth code, assemble the payload, and write it via mz_zip_writer_add_mem_raw with method 99 and the encrypted flag. Key material is scrubbed from the stack afterwards. Directory entries carry no data and stay unencrypted. Both the create path and the in-place/rebuild append path are covered.

zip_zip() takes cpassword / cpassword_len / cencryption (see zip_encryption_t in zip.h: 0 = none, 1/2/3 = AES-128/192/256).

R layer — ../R/utils.R, ../R/zip.R

Testing

Status / TODO



Try the zip package in your browser

Any scripts or data that you put into this service are public.

zip documentation built on July 13, 2026, 5:09 p.m.