Skip to content

Password-Protected Workbooks

wolfxl supports OOXML password protection on both the read and write paths via the optional msoffcrypto-tool dependency. Install it with:

pip install wolfxl[encrypted]

Supplying password= without msoffcrypto-tool installed raises ImportError naming the extra.

Quick Start

import wolfxl

# Encrypt-on-save: pass password= to Workbook.save
wb = wolfxl.Workbook()
wb.active["A1"] = "secret"
wb.save("confidential.xlsx", password="my-password")

# Decrypt-on-load: pass password= to load_workbook
wb2 = wolfxl.load_workbook("confidential.xlsx", password="my-password")
print(wb2.active["A1"].value)  # -> "secret"

password accepts str or bytes (UTF-8 decoded). Passing password=None or omitting the argument produces / consumes a plaintext xlsx — same as openpyxl's behaviour.

Empty passwords ("" / b"") raise ValueError("empty password not allowed") on save. On read, an empty / wrong password and a damaged or tampered payload raise ValueError naming the failed check instead of yielding a malformed workbook.

Supported Algorithms

OOXML supports three families of password protection. wolfxl's coverage matches what msoffcrypto-tool's upstream library is able to produce or consume:

Algorithm Read Write Notes
Agile (AES-256, SHA-512) ✅ ✅ The modern Excel default. Used for every wolfxl save(..., password=...) call.
Standard / ECMA-376 (AES-128) ✅ ❌ Read-only. msoffcrypto-tool does not implement Standard encryption, only decryption.
XOR obfuscation (legacy .xls) ✅ ❌ Decrypt-only. XOR obfuscation is BIFF-era and not part of the OOXML spec.

If you need to write a Standard or XOR-encrypted file, wolfxl is the wrong tool — please file an issue describing your use case so we can discuss alternatives (e.g. shelling out to LibreOffice).

Architecture (write side)

Workbook.save(path, password=...) is layered on top of the existing plaintext save path:

  1. Validate the password (empty rejected up front so we don't leak a plaintext tempfile).
  2. Materialise the plaintext xlsx via the normal Rust writer / patcher into a tempfile.NamedTemporaryFile next to path's parent dir.
  3. Hand the bytes to wolfxl._encryption.encrypt_xlsx_to_path, which encrypts them with msoffcrypto-tool's Agile primitives (see the note on small packages below).
  4. Atomic-rename the encrypted file into place.
  5. Always clean up the plaintext tempfile, including on error paths.

The Rust crates are not modified — encryption stays Python-side, same as the read path.

Implementation note: small packages

msoffcrypto-tool's own container writer stores an EncryptedPackage stream below the OLE2 mini-stream cutoff (4096 bytes) in the wrong sector chain, so small, valid workbooks cannot be decrypted afterward. wolfxl uses the library's Agile encryption primitives with its own container allocation, which places such a stream in the mini stream, and reads the container back before writing it.

During encryption, the admitted plaintext bytes are passed directly to the writer, without padding or package reconstruction. After decryption, the admitted bytes are unchanged for both small and ordinary workbooks.

Verification on decrypt

When loading with load_workbook(path, password=...), the password is verified before plaintext is recovered from Agile or Standard encryption. For Agile, the HMAC over the encrypted package is also verified; expect a refusal if ciphertext fails this check. No equivalent payload integrity guarantee is available with Standard encryption.

Modify-mode

Workbook.save(..., password=...) works in both write mode (Workbook()) and modify mode (load_workbook(path, modify=True)). Encryption is applied to the final byte stream regardless of which Rust backend produced it.

wb = wolfxl.load_workbook("plaintext.xlsx", modify=True)
wb.active["B2"] = "edited"
wb.save("encrypted.xlsx", password="pw")

A workbook that was opened with password= and saved without one produces a plaintext output. Pass password= on save to re-encrypt it.

Round-trip verification

import wolfxl

wb = wolfxl.Workbook()
wb.active["A1"] = "round-trip"
wb.save("rt.xlsx", password="pw")

wb2 = wolfxl.load_workbook("rt.xlsx", password="pw")
assert wb2.active["A1"].value == "round-trip"

The same workflow is exercised end-to-end in tests/test_encrypted_writes.py and tests/parity/test_encrypted_write_parity.py.