Password-Protected Workbooks¶
wolfxl supports OOXML password protection on both the read and write
paths via the optional msoffcrypto-tool dependency. Install
it with:
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:
- Validate the password (empty rejected up front so we don't leak a plaintext tempfile).
- Materialise the plaintext xlsx via the normal Rust writer / patcher
into a
tempfile.NamedTemporaryFilenext topath's parent dir. - Hand the bytes to
wolfxl._encryption.encrypt_xlsx_to_path, which encrypts them withmsoffcrypto-tool's Agile primitives (see the note on small packages below). - Atomic-rename the encrypted file into place.
- 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.