[
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nTo report a security vulnerability, please use the\n[Tidelift security contact](https://tidelift.com/security).\nTidelift will coordinate the fix and disclosure.\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: build\n\non:\n  push:\n    # branches: [$default-branch]\n    branches: [\"master\"]\n    tags: [\"*\"]\n  pull_request:\n    # branches: [$default-branch]\n    branches: [\"master\"]\n\njobs:\n  # https://srz-zumix.blogspot.com/2019/10/github-actions-ci-skip.html\n  prepare:\n    runs-on: ubuntu-latest\n    if: \"! contains(github.event.head_commit.message, '[skip ci]')\"\n    steps:\n      - run: echo \"[skip ci] ${{ contains(github.event.head_commit.message, '[skip ci]') }}\"\n      - run: echo \"[github.ref] ${{ github.ref }}\"\n\n  build:\n    needs: [\"prepare\"]\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: true\n      matrix:\n        os: [\"ubuntu-latest\", \"macos-latest\", \"windows-latest\"]\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Python ${{ matrix.python-version }}\n        uses: actions/setup-python@v3\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install poetry and codecov\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install poetry codecov\n      - name: Install dependencies\n        run: |\n          poetry install --no-interaction\n      - name: Test with pytest\n        run: |\n          poetry run coverage run -m pytest -v\n          codecov\n\n  publish:\n    needs: [\"build\"]\n    if: \"success() && startsWith(github.ref, 'refs/tags')\"\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Set up Python\n        uses: actions/setup-python@v3\n        with:\n          python-version: \"3.x\"\n      - name: Install poetry\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install poetry\n      - name: Build and publish package\n        run: |\n          poetry config pypi-token.pypi \"${{ secrets.PYPI_API_TOKEN }}\"\n          poetry publish --no-interaction --build\n"
  },
  {
    "path": ".gitignore",
    "content": "docs/_static/\ndocs/_templates/\ndocs/_build/\n\n### https://raw.github.com/github/gitignore/4bff4a2986af526650f1d329d97047dc1fa87599/Python.gitignore\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n.static_storage/\n.media/\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n\n### https://raw.github.com/github/gitignore/4bff4a2986af526650f1d329d97047dc1fa87599/Global/macOS.gitignore\n\n# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n\n# Thumbnails\n._*\n\n# Files that might appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n\n### https://raw.github.com/github/gitignore/4bff4a2986af526650f1d329d97047dc1fa87599/Global/Windows.gitignore\n\n# Windows thumbnail cache files\nThumbs.db\nehthumbs.db\nehthumbs_vista.db\n\n# Dump file\n*.stackdump\n\n# Folder config file\n[Dd]esktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n\n"
  },
  {
    "path": ".readthedocs.yml",
    "content": "version: 2\nsphinx:\n  configuration: docs/conf.py\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\npython:\n  install:\n    - requirements: docs/requirements.txt\n    - method: pip\n      path: .\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "\nv6.0.0 / 2026-01-12\n===================\n\n  * (BREAKING) Drop support for Python 3.8 and 3.9, add Python 3.14 to CI\n  * Update dependencies\n  * Clarify error messages\n\nv5.4.2 / 2024-08-09\n===================\n\n  * Fix DeprecationWarning from cryptography library (reported by @dennn11, [#92](https://github.com/nolze/msoffcrypto-tool/issues/92))\n\nv5.4.1 / 2024-05-25\n===================\n\n  * Fix for incorrect key size with 0 length keySize var (@UserJHansen, [#89](https://github.com/nolze/msoffcrypto-tool/pull/89))\n\nv5.4.0 / 2024-05-02\n===================\n\n  * Never return None in ooxml's \\_parseinfo (@gdesmar, [#88](https://github.com/nolze/msoffcrypto-tool/pull/88))\n\nv5.3.1 / 2024-01-19\n===================\n\n  * Bug fixes\n\nv5.3.0 / 2024-01-19\n===================\n\n  * Add support for OOXML encryption, a port from the C++ library https://github.com/herumi/msoffice (@stephane-rouleau, [#86](https://github.com/nolze/msoffcrypto-tool/pull/86))\n\nv5.2.0 / 2024-01-06\n===================\n\n  * Support XOR Obfuscation decryption for .xls documents (@DissectMalware, [#80](https://github.com/nolze/msoffcrypto-tool/pull/80))\n  * Bug fixes\n\nv5.1.1 / 2023-07-20\n===================\n\n  * Drop Python 3.7 support as it reaches EOL, Add Python 3.11 to CI environments\n  * Get the version in `__main__.py` instead of `__init__.py` to avoid a relevant error in PyInstaller/cx\\_Freeze in which `pkg_resources` does not work by default\n\nv5.1.0 / 2023-07-17\n===================\n\n  * Load plain OOXML as OfficeFile with type == plain. Fixes [#74](https://github.com/nolze/msoffcrypto-tool/issues/74)\n  * Use importlib.metadata.version in Python >=3.8 ([#77](https://github.com/nolze/msoffcrypto-tool/issues/77))\n\n5.0.1 / 2023-02-28\n===================\n\n  * (dev) Switch to GitHub Actions from Travis CI\n  * Update dependencies, Drop Python 3.6 support\n\n5.0.0 / 2022-01-20\n==================\n\n  * (dev) Add tests on Python 3.7 to 3.9 ([#71](https://github.com/nolze/msoffcrypto-tool/pull/71))\n  * (dev) Track poetry.lock ([#71](https://github.com/nolze/msoffcrypto-tool/pull/71))\n  * (BREAKING) Drop Python 2 support ([#71](https://github.com/nolze/msoffcrypto-tool/pull/71))\n  * Raise exception if no encryption type is specified ([#70](https://github.com/nolze/msoffcrypto-tool/issues/70))\n  * Support SHA256, SHA384 hash algorithm (@jackydo, [#67](https://github.com/nolze/msoffcrypto-tool/pull/67))\n  * Fix errors for unencrypted documents\n  * Use absolute imports ([#63](https://github.com/nolze/msoffcrypto-tool/pull/63))\n\n4.12.0 / 2021-06-04\n===================\n\n  * Use custom exceptions ([#59](https://github.com/nolze/msoffcrypto-tool/pull/59))\n  * (dev) Remove nose (thank you) ([#57](https://github.com/nolze/msoffcrypto-tool/pull/57))\n  * (dev) Use poetry ([#55](https://github.com/nolze/msoffcrypto-tool/pull/55))\n\n4.11.0 / 2020-09-03\n===================\n\n  * Improve hash calculation (suggested by @StanislavNikolov)\n  * Add \"verify\\_passwd\" and \"verify\\_integrity\" option (@jeffli678)\n  * Make _packUserEditAtom spec-compliant\n\n4.10.2 / 2020-04-08\n===================\n\n  * Update \\_makekey in rc4\\_cryptoapi (@doracpphp)\n  * Fix handling of optional field value in ppt97\n  * Add tests for is_encrypted() (--test)\n  * Make Doc97File.is_encrypted() return boolean\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "MIT License\n\nCopyright (c) 2015 nolze\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "NOTICE.txt",
    "content": "This software contains derivative works from https://github.com/herumi/msoffice\nwhich is licensed under the BSD 3-Clause License.\n\nhttps://github.com/herumi/msoffice/blob/c3cdb1ea0a5285a2a1718fee2dc893fd884bdad0/COPYRIGHT\n\nCopyright (c) 2007-2015 Cybozu Labs, Inc.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\nRedistributions of source code must retain the above copyright notice, this\nlist of conditions and the following disclaimer.\nRedistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\nNeither the name of the Cybozu Labs, Inc. nor the names of its contributors may\nbe used to endorse or promote products derived from this software without\nspecific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# msoffcrypto-tool\n\n[![PyPI](https://img.shields.io/pypi/v/msoffcrypto-tool.svg)](https://pypi.org/project/msoffcrypto-tool/)\n[![PyPI downloads](https://img.shields.io/pypi/dm/msoffcrypto-tool.svg)](https://pypistats.org/packages/msoffcrypto-tool)\n[![build](https://github.com/nolze/msoffcrypto-tool/actions/workflows/ci.yaml/badge.svg)](https://github.com/nolze/msoffcrypto-tool/actions/workflows/ci.yaml)\n[![Coverage Status](https://codecov.io/gh/nolze/msoffcrypto-tool/branch/master/graph/badge.svg)](https://codecov.io/gh/nolze/msoffcrypto-tool)\n[![Documentation Status](https://readthedocs.org/projects/msoffcrypto-tool/badge/?version=latest)](http://msoffcrypto-tool.readthedocs.io/en/latest/?badge=latest)\n\nmsoffcrypto-tool is a Python tool and library for decrypting and encrypting MS Office files using a password or other keys.\n\n## Contents\n\n* [Installation](#installation)\n* [Examples](#examples)\n* [Supported encryption methods](#supported-encryption-methods)\n* [Tests](#tests)\n* [Todo](#todo)\n* [Resources](#resources)\n* [Use cases and mentions](#use-cases-and-mentions)\n* [Contributors](#contributors)\n* [Credits](#credits)\n\n## Installation\n\n```\npip install msoffcrypto-tool\n```\n\n## Examples\n\n### As CLI tool (with password)\n\n#### Decryption\n\nSpecify the password with `-p` flag:\n\n```\nmsoffcrypto-tool encrypted.docx decrypted.docx -p Passw0rd\n```\n\nPassword is prompted if you omit the password argument value:\n\n```bash\n$ msoffcrypto-tool encrypted.docx decrypted.docx -p\nPassword:\n```\n\nTo check if the file is encrypted or not, use `-t` flag:\n\n```\nmsoffcrypto-tool document.doc --test -v\n```\n\nIt returns `1` if the file is encrypted, `0` if not.\n\n#### Encryption (OOXML only, experimental)\n\n> [!IMPORTANT]\n> Encryption feature is experimental. Please use it at your own risk.\n\nTo password-protect a document, use `-e` flag along with `-p` flag:\n\n```\nmsoffcrypto-tool -e -p Passw0rd plain.docx encrypted.docx\n```\n\n### As library\n\nPassword and more key types are supported with library functions.\n\n#### Decryption\n\nBasic usage:\n\n```python\nimport msoffcrypto\n\nencrypted = open(\"encrypted.docx\", \"rb\")\nfile = msoffcrypto.OfficeFile(encrypted)\n\nfile.load_key(password=\"Passw0rd\")  # Use password\n\nwith open(\"decrypted.docx\", \"wb\") as f:\n    file.decrypt(f)\n\nencrypted.close()\n```\n\nIn-memory:\n\n```python\nimport msoffcrypto\nimport io\nimport pandas as pd\n\ndecrypted = io.BytesIO()\n\nwith open(\"encrypted.xlsx\", \"rb\") as f:\n    file = msoffcrypto.OfficeFile(f)\n    file.load_key(password=\"Passw0rd\")  # Use password\n    file.decrypt(decrypted)\n\ndf = pd.read_excel(decrypted)\nprint(df)\n```\n\nAdvanced usage:\n\n```python\n# Verify password before decryption (default: False)\n# The ECMA-376 Agile/Standard crypto system allows one to know whether the supplied password is correct before actually decrypting the file\n# Currently, the verify_password option is only meaningful for ECMA-376 Agile/Standard Encryption\nfile.load_key(password=\"Passw0rd\", verify_password=True)\n\n# Use private key\nfile.load_key(private_key=open(\"priv.pem\", \"rb\"))\n\n# Use intermediate key (secretKey)\nfile.load_key(secret_key=binascii.unhexlify(\"AE8C36E68B4BB9EA46E5544A5FDB6693875B2FDE1507CBC65C8BCF99E25C2562\"))\n\n# Check the HMAC of the data payload before decryption (default: False)\n# Currently, the verify_integrity option is only meaningful for ECMA-376 Agile Encryption\nfile.decrypt(open(\"decrypted.docx\", \"wb\"), verify_integrity=True)\n```\n\nSupported key types are\n\n- Passwords\n- Intermediate keys (optional)\n- Private keys used for generating escrow keys (escrow certificates) (optional)\n\nSee also [\"Backdooring MS Office documents with secret master keys\"](https://web.archive.org/web/20171008075059/http://secuinside.com/archive/2015/2015-1-9.pdf) for more information on the key types.\n\n#### Encryption (OOXML only, experimental)\n\n> [!IMPORTANT]\n> Encryption feature is experimental. Please use it at your own risk.\n\nBasic usage:\n\n```python\nfrom msoffcrypto.format.ooxml import OOXMLFile\n\nplain = open(\"plain.docx\", \"rb\")\nfile = OOXMLFile(plain)\n\nwith open(\"encrypted.docx\", \"wb\") as f:\n    file.encrypt(\"Passw0rd\", f)\n\nplain.close()\n```\n\nIn-memory:\n\n```python\nfrom msoffcrypto.format.ooxml import OOXMLFile\nimport io\n\nencrypted = io.BytesIO()\n\nwith open(\"plain.xlsx\", \"rb\") as f:\n    file = OOXMLFile(f)\n    file.encrypt(\"Passw0rd\", encrypted)\n\n# Do stuff with encrypted buffer; it contains an OLE container with an encrypted stream\n...\n```\n\n## Supported encryption methods\n\n### MS-OFFCRYPTO specs\n\n* [x] ECMA-376 (Agile Encryption/Standard Encryption)\n  * [x] MS-DOCX (OOXML) (Word 2007-)\n  * [x] MS-XLSX (OOXML) (Excel 2007-)\n  * [x] MS-PPTX (OOXML) (PowerPoint 2007-)\n* [x] Office Binary Document RC4 CryptoAPI\n  * [x] MS-DOC (Word 2002, 2003, 2004)\n  * [x] MS-XLS ([Excel 2002, 2003, 2007, 2010](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/a3ad4e36-ab66-426c-ba91-b84433312068#Appendix_A_22)) (experimental)\n  * [x] MS-PPT (PowerPoint 2002, 2003, 2004) (partial, experimental)\n* [x] Office Binary Document RC4\n  * [x] MS-DOC (Word 97, 98, 2000)\n  * [x] MS-XLS (Excel 97, 98, 2000) (experimental)\n* [ ] ECMA-376 (Extensible Encryption)\n* [x] XOR Obfuscation\n  * [x] MS-XLS ([Excel 2002, 2003](https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/a3ad4e36-ab66-426c-ba91-b84433312068#Appendix_A_21)) (experimental)\n  * [ ] MS-DOC (Word 2002, 2003, 2004?)\n\n### Other\n\n* [ ] Word 95 Encryption (Word 95 and prior)\n* [ ] Excel 95 Encryption (Excel 95 and prior)\n* [ ] PowerPoint 95 Encryption (PowerPoint 95 and prior)\n\nPRs are welcome!\n\n## Tests\n\nWith [coverage](https://github.com/nedbat/coveragepy) and [pytest](https://pytest.org/):\n\n```\npoetry install\npoetry run coverage run -m pytest -v\n```\n\n## Todo\n\n* [x] Add tests\n* [x] Support decryption with passwords\n* [x] Support older encryption schemes\n* [x] Add function-level tests\n* [x] Add API documents\n* [x] Publish to PyPI\n* [x] Add decryption tests for various file formats\n* [x] Integrate with more comprehensive projects handling MS Office files (such as [oletools](https://github.com/decalage2/oletools/)?) if possible\n* [x] Add the password prompt mode for CLI\n* [x] Improve error types (v4.12.0)\n* [ ] Add type hints\n* [ ] Introduce something like `ctypes.Structure`\n* [x] Support OOXML encryption\n* [ ] Support other encryption\n* [ ] Isolate parser\n* [ ] Redesign APIs (v6.0.0)\n\n## Resources\n\n* \"Backdooring MS Office documents with secret master keys\" [http://secuinside.com/archive/2015/2015-1-9.pdf](https://web.archive.org/web/20171008075059/http://secuinside.com/archive/2015/2015-1-9.pdf)\n* Technical Documents <https://msdn.microsoft.com/en-us/library/cc313105.aspx>\n  * [MS-OFFCRYPTO] Agile Encryption <https://msdn.microsoft.com/en-us/library/dd949735(v=office.12).aspx>\n* [MS-OFFDI] Microsoft Office File Format Documentation Introduction <https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offdi/24ed256c-eb5b-494e-b4f6-fb696ad2b4dc>\n* LibreOffice/core <https://github.com/LibreOffice/core>\n* LibreOffice/mso-dumper <https://github.com/LibreOffice/mso-dumper>\n* wvDecrypt <http://www.skynet.ie/~caolan/Packages/wvDecrypt.html>\n* Microsoft Office password protection - Wikipedia <https://en.wikipedia.org/wiki/Microsoft_Office_password_protection#History_of_Microsoft_Encryption_password>\n* office2john.py <https://github.com/magnumripper/JohnTheRipper/blob/bleeding-jumbo/run/office2john.py>\n\n## Alternatives\n\n* herumi/msoffice <https://github.com/herumi/msoffice>\n* DocRecrypt <https://blogs.technet.microsoft.com/office_resource_kit/2013/01/23/now-you-can-reset-or-remove-a-password-from-a-word-excel-or-powerpoint-filewith-office-2013/>\n* Apache POI - the Java API for Microsoft Documents <https://poi.apache.org/>\n\n## Use cases and mentions\n\n### General\n\n* <https://repology.org/project/python:msoffcrypto-tool/versions> (kudos to maintainers!)\n<!-- * <https://checkroth.com/unlocking-password-protected-files.html> (outdated) -->\n\n### Corporate\n\n* Workato <https://docs.workato.com/connectors/python.html#supported-features> <!-- https://web.archive.org/web/20240525062245/https://docs.workato.com/connectors/python.html#supported-features -->\n* Check Point <https://www.checkpoint.com/about-us/copyright-and-trademarks/> <!-- https://web.archive.org/web/20230326071230/https://www.checkpoint.com/about-us/copyright-and-trademarks/ -->\n\n### Malware/maldoc analysis\n\n* <https://github.com/jbremer/sflock/commit/3f6a96abe1dbb4405e4fb7fd0d16863f634b09fb>\n* <https://isc.sans.edu/forums/diary/Video+Analyzing+Encrypted+Malicious+Office+Documents/24572/>\n\n### CTF\n\n* <https://github.com/shombo/cyberstakes-writeps-2018/tree/master/word_up>\n* <https://github.com/willi123yao/Cyberthon2020_Writeups/blob/master/csit/Lost_Magic>\n\n### In other languages\n\n* <https://github.com/dtjohnson/xlsx-populate>\n* <https://github.com/opendocument-app/OpenDocument.core/blob/233663b039/src/internal/ooxml/ooxml_crypto.h>\n* <https://github.com/jaydadhania08/PHPDecryptXLSXWithPassword>\n* <https://github.com/epicentre-msf/rpxl>\n\n### In publications\n\n* [Excel、データ整理＆分析、画像処理の自動化ワザを完全網羅！ 超速Python仕事術大全](https://books.google.co.jp/books?id=TBdVEAAAQBAJ&q=msoffcrypto) (伊沢剛, 2022)\n* [\"Analyse de documents malveillants en 2021\"](https://twitter.com/decalage2/status/1435255507846053889), MISC Hors-série N° 24, \"Reverse engineering : apprenez à analyser des binaires\" (Lagadec Philippe, 2021)\n* [シゴトがはかどる Python自動処理の教科書](https://books.google.co.jp/books?id=XEYUEAAAQBAJ&q=msoffcrypto) (クジラ飛行机, 2020)\n\n## Contributors\n\n* <https://github.com/nolze/msoffcrypto-tool/graphs/contributors>\n\n## Credits\n\n* The sample file for XOR Obfuscation is from: <https://github.com/openwall/john-samples/tree/main/Office/Office_Secrets>\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/cli.rst",
    "content": "Command-line interface\n======================\n\n.. toctree::\n\n.. autoprogram:: msoffcrypto.__main__:parser\n   :prog: msoffcrypto-tool\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Project information -----------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\n\nproject = \"msoffcrypto-tool\"\ncopyright = \"nolze\"\nauthor = \"nolze\"\n\nversion = \"\"\nrelease = version\n\n# -- General configuration ---------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\n\nimport os\nimport sys\n\nsys.path.insert(0, os.path.abspath(\"../\"))\n\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinxcontrib.autoprogram\",\n    \"sphinx.ext.napoleon\",\n    \"sphinx.ext.viewcode\",\n    \"myst_parser\",\n]\n\ntemplates_path = [\"_templates\"]\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\n\n\n# -- Options for HTML output -------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\n\nhtml_theme = \"furo\"\nhtml_static_path = [\"_static\"]\n\n# html_title = \"<project> <version>\"\nhtml_title = \"msoffcrypto-tool\"\nhtml_theme_options = {\n    \"footer_icons\": [\n        {\n            \"name\": \"GitHub\",\n            \"url\": \"https://github.com/nolze/msoffcrypto-tool\",\n            \"html\": \"\"\"\n                <svg stroke=\"currentColor\" fill=\"currentColor\" stroke-width=\"0\" viewBox=\"0 0 16 16\">\n                    <path fill-rule=\"evenodd\" d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z\"></path>\n                </svg>\n            \"\"\",\n            \"class\": \"\",\n        },\n    ],\n}\n\nmyst_enable_extensions = [\"tasklist\"]\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. msoffcrypto-tool documentation master file, created by\n   sphinx-quickstart on Tue Oct 17 02:16:54 2023.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nmsoffcrypto-tool\n================\n\n.. include:: ../README.md\n   :parser: myst_parser.sphinx_\n   :start-after: msoffcrypto-tool\n\n.. toctree::\n   :hidden:\n   :maxdepth: 2\n   :caption: Contents:\n\n   cli\n   msoffcrypto\n\n.. * :ref:`genindex`\n.. * :ref:`modindex`\n.. * :ref:`search`\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "docs/modules.rst",
    "content": "msoffcrypto\n===========\n\n.. toctree::\n   :maxdepth: 1\n\n   msoffcrypto\n"
  },
  {
    "path": "docs/msoffcrypto.exceptions.rst",
    "content": "msoffcrypto.exceptions package\n==============================\n\nModule contents\n---------------\n\n.. automodule:: msoffcrypto.exceptions\n   :members:\n   :undoc-members:\n   :show-inheritance:\n"
  },
  {
    "path": "docs/msoffcrypto.format.rst",
    "content": "msoffcrypto.format package\n==========================\n\nSubmodules\n----------\n\nmsoffcrypto.format.base module\n------------------------------\n\n.. automodule:: msoffcrypto.format.base\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.format.common module\n--------------------------------\n\n.. automodule:: msoffcrypto.format.common\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.format.doc97 module\n-------------------------------\n\n.. automodule:: msoffcrypto.format.doc97\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.format.ooxml module\n-------------------------------\n\n.. automodule:: msoffcrypto.format.ooxml\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.format.ppt97 module\n-------------------------------\n\n.. automodule:: msoffcrypto.format.ppt97\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.format.xls97 module\n-------------------------------\n\n.. automodule:: msoffcrypto.format.xls97\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nModule contents\n---------------\n\n.. automodule:: msoffcrypto.format\n   :members:\n   :undoc-members:\n   :show-inheritance:\n"
  },
  {
    "path": "docs/msoffcrypto.method.container.rst",
    "content": "msoffcrypto.method.container package\n====================================\n\nSubmodules\n----------\n\nmsoffcrypto.method.container.ecma376\\_encrypted module\n------------------------------------------------------\n\n.. automodule:: msoffcrypto.method.container.ecma376_encrypted\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nModule contents\n---------------\n\n.. automodule:: msoffcrypto.method.container\n   :members:\n   :undoc-members:\n   :show-inheritance:\n"
  },
  {
    "path": "docs/msoffcrypto.method.rst",
    "content": "msoffcrypto.method package\n==========================\n\nSubpackages\n-----------\n\n.. toctree::\n   :maxdepth: 1\n\n   msoffcrypto.method.container\n\nSubmodules\n----------\n\nmsoffcrypto.method.ecma376\\_agile module\n----------------------------------------\n\n.. automodule:: msoffcrypto.method.ecma376_agile\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.method.ecma376\\_extensible module\n---------------------------------------------\n\n.. automodule:: msoffcrypto.method.ecma376_extensible\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.method.ecma376\\_standard module\n-------------------------------------------\n\n.. automodule:: msoffcrypto.method.ecma376_standard\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.method.rc4 module\n-----------------------------\n\n.. automodule:: msoffcrypto.method.rc4\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.method.rc4\\_cryptoapi module\n----------------------------------------\n\n.. automodule:: msoffcrypto.method.rc4_cryptoapi\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nmsoffcrypto.method.xor\\_obfuscation module\n------------------------------------------\n\n.. automodule:: msoffcrypto.method.xor_obfuscation\n   :members:\n   :undoc-members:\n   :show-inheritance:\n\nModule contents\n---------------\n\n.. automodule:: msoffcrypto.method\n   :members:\n   :undoc-members:\n   :show-inheritance:\n"
  },
  {
    "path": "docs/msoffcrypto.rst",
    "content": "msoffcrypto package\n===================\n\nSubpackages\n-----------\n\n.. toctree::\n   :maxdepth: 1\n\n   msoffcrypto.exceptions\n   msoffcrypto.format\n   msoffcrypto.method\n\nModule contents\n---------------\n\n.. automodule:: msoffcrypto\n   :members:\n   :undoc-members:\n   :show-inheritance:\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "accessible-pygments==0.0.5 ; python_version >= \"3.10\" and python_version < \"4.0\"\nalabaster==1.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nanyio==4.12.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nbabel==2.17.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nbeautifulsoup4==4.14.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncertifi==2026.1.4 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncharset-normalizer==3.4.4 ; python_version >= \"3.10\" and python_version < \"4.0\"\nclick==8.3.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncolorama==0.4.6 ; python_version >= \"3.10\" and python_version < \"4.0\"\ndocutils==0.21.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nexceptiongroup==1.3.1 ; python_version == \"3.10\"\nfuro==2025.12.19 ; python_version >= \"3.10\" and python_version < \"4.0\"\nh11==0.16.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nidna==3.11 ; python_version >= \"3.10\" and python_version < \"4.0\"\nimagesize==1.4.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\njinja2==3.1.6 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmarkdown-it-py==3.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmarkupsafe==3.0.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmdit-py-plugins==0.5.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmdurl==0.1.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmyst-parser==4.0.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\npackaging==25.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npygments==2.19.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\npyyaml==6.0.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nrequests==2.32.5 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsnowballstemmer==3.0.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsoupsieve==2.8.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinx-autobuild==2024.10.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinx-basic-ng==1.0.0b2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinx==8.1.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-applehelp==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-autoprogram==0.1.9 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-devhelp==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-htmlhelp==2.1.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-jsmath==1.0.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-qthelp==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-serializinghtml==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nstarlette==0.51.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntomli==2.4.0 ; python_version == \"3.10\"\ntyping-extensions==4.15.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nurllib3==2.6.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nuvicorn==0.40.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nwatchfiles==1.1.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nwebsockets==16.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\n"
  },
  {
    "path": "msoffcrypto/__init__.py",
    "content": "import zipfile\n\nimport olefile\n\nfrom msoffcrypto import exceptions\n\n\ndef OfficeFile(file):\n    \"\"\"Return an office file object based on the format of given file.\n\n    Args:\n        file (:obj:`_io.BufferedReader`): Input file.\n\n    Returns:\n        BaseOfficeFile object.\n\n    Examples:\n        >>> with open(\"tests/inputs/example_password.docx\", \"rb\") as f:\n        ...     officefile = OfficeFile(f)\n        ...     officefile.keyTypes\n        ('password', 'private_key', 'secret_key')\n\n        >>> with open(\"tests/inputs/example_password.docx\", \"rb\") as f:\n        ...     officefile = OfficeFile(f)\n        ...     officefile.load_key(password=\"Password1234_\", verify_password=True)\n\n        >>> with open(\"README.md\", \"rb\") as f:\n        ...     officefile = OfficeFile(f)\n        Traceback (most recent call last):\n            ...\n        msoffcrypto.exceptions.FileFormatError: ...\n\n        >>> with open(\"tests/inputs/example_password.docx\", \"rb\") as f:\n        ...     officefile = OfficeFile(f)\n        ...     officefile.load_key(password=\"0000\", verify_password=True)\n        Traceback (most recent call last):\n            ...\n        msoffcrypto.exceptions.InvalidKeyError: ...\n\n    Given file handle will not be closed, the file position will most certainly\n    change.\n    \"\"\"\n    file.seek(0)  # required by isOleFile\n    if olefile.isOleFile(file):\n        ole = olefile.OleFileIO(file)\n    elif zipfile.is_zipfile(file):  # Heuristic\n        from msoffcrypto.format.ooxml import OOXMLFile\n\n        return OOXMLFile(file)\n    else:\n        raise exceptions.FileFormatError(\"Unsupported file format\")\n\n    # TODO: Make format specifiable by option in case of obstruction\n    # Try this first; see https://github.com/nolze/msoffcrypto-tool/issues/17\n    if ole.exists(\"EncryptionInfo\"):\n        from msoffcrypto.format.ooxml import OOXMLFile\n\n        return OOXMLFile(file)\n    # MS-DOC: The WordDocument stream MUST be present in the file.\n    # https://msdn.microsoft.com/en-us/library/dd926131(v=office.12).aspx\n    elif ole.exists(\"wordDocument\"):\n        from msoffcrypto.format.doc97 import Doc97File\n\n        return Doc97File(file)\n    # MS-XLS: A file MUST contain exactly one Workbook Stream, ...\n    # https://msdn.microsoft.com/en-us/library/dd911009(v=office.12).aspx\n    elif ole.exists(\"Workbook\"):\n        from msoffcrypto.format.xls97 import Xls97File\n\n        return Xls97File(file)\n    # MS-PPT: A required stream whose name MUST be \"PowerPoint Document\".\n    # https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/1fc22d56-28f9-4818-bd45-67c2bf721ccf\n    elif ole.exists(\"PowerPoint Document\"):\n        from msoffcrypto.format.ppt97 import Ppt97File\n\n        return Ppt97File(file)\n    else:\n        raise exceptions.FileFormatError(\"Unrecognized file format\")\n"
  },
  {
    "path": "msoffcrypto/__main__.py",
    "content": "import argparse\nimport getpass\nimport logging\nimport sys\n\nimport olefile\n\nfrom msoffcrypto import OfficeFile, exceptions\nfrom msoffcrypto.format.ooxml import OOXMLFile, _is_ooxml\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\n\ndef _get_version():\n    if sys.version_info >= (3, 8):\n        from importlib import metadata\n\n        return metadata.version(\"msoffcrypto-tool\")\n    else:\n        import pkg_resources\n\n        return pkg_resources.get_distribution(\"msoffcrypto-tool\").version\n\n\ndef ifWIN32SetBinary(io):\n    if sys.platform == \"win32\":\n        import msvcrt\n        import os\n\n        msvcrt.setmode(io.fileno(), os.O_BINARY)\n\n\ndef is_encrypted(file):\n    r\"\"\"\n    Test if the file is encrypted.\n\n        >>> f = open(\"tests/inputs/plain.doc\", \"rb\")\n        >>> is_encrypted(f)\n        False\n    \"\"\"\n    # TODO: Validate file\n    if not olefile.isOleFile(file):\n        return False\n\n    file = OfficeFile(file)\n\n    return file.is_encrypted()\n\n\nparser = argparse.ArgumentParser()\ngroup = parser.add_mutually_exclusive_group(required=True)\ngroup.add_argument(\"-p\", \"--password\", nargs=\"?\", const=\"\", dest=\"password\", help=\"password text\")\ngroup.add_argument(\"-t\", \"--test\", dest=\"test_encrypted\", action=\"store_true\", help=\"test if the file is encrypted\")\nparser.add_argument(\"-e\", dest=\"encrypt\", action=\"store_true\", help=\"encryption mode (default is false)\")\nparser.add_argument(\"-v\", dest=\"verbose\", action=\"store_true\", help=\"print verbose information\")\nparser.add_argument(\"infile\", nargs=\"?\", type=argparse.FileType(\"rb\"), help=\"input file\")\nparser.add_argument(\"outfile\", nargs=\"?\", type=argparse.FileType(\"wb\"), help=\"output file (if blank, stdout is used)\")\n\n\ndef main():\n    args = parser.parse_args()\n\n    if args.verbose:\n        logger.removeHandler(logging.NullHandler())\n        logging.basicConfig(level=logging.DEBUG, format=\"%(message)s\")\n        version = _get_version()\n        logger.debug(\"Version: {}\".format(version))\n\n    if args.test_encrypted:\n        if not is_encrypted(args.infile):\n            print(\"{}: not encrypted\".format(args.infile.name), file=sys.stderr)\n            sys.exit(1)\n        else:\n            logger.debug(\"{}: encrypted\".format(args.infile.name))\n        return\n\n    if args.password:\n        password = args.password\n    else:\n        password = getpass.getpass()\n\n    if args.outfile is None:\n        ifWIN32SetBinary(sys.stdout)\n        if hasattr(sys.stdout, \"buffer\"):  # For Python 2\n            args.outfile = sys.stdout.buffer\n        else:\n            args.outfile = sys.stdout\n\n    if args.encrypt:\n        if not _is_ooxml(args.infile):\n            raise exceptions.FileFormatError(\"Not an OOXML file\")\n\n        # OOXML is the only format we support for encryption\n        file = OOXMLFile(args.infile)\n\n        file.encrypt(password, args.outfile)\n    else:\n        if not olefile.isOleFile(args.infile):\n            raise exceptions.FileFormatError(\"Not an OLE file\")\n\n        file = OfficeFile(args.infile)\n        file.load_key(password=password)\n\n        file.decrypt(args.outfile)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "msoffcrypto/exceptions/__init__.py",
    "content": "class FileFormatError(Exception):\n    \"\"\"Raised when the format of given file is unsupported or unrecognized.\"\"\"\n\n    pass\n\n\nclass ParseError(Exception):\n    \"\"\"Raised when the file cannot be parsed correctly.\"\"\"\n\n    pass\n\n\nclass DecryptionError(Exception):\n    \"\"\"Raised when the file cannot be decrypted.\"\"\"\n\n    pass\n\n\nclass EncryptionError(Exception):\n    \"\"\"Raised when the file cannot be encrypted.\"\"\"\n\n    pass\n\n\nclass InvalidKeyError(DecryptionError):\n    \"\"\"Raised when the given password or key is incorrect or cannot be verified.\"\"\"\n\n    pass\n"
  },
  {
    "path": "msoffcrypto/format/__init__.py",
    "content": ""
  },
  {
    "path": "msoffcrypto/format/base.py",
    "content": "import abc\n\n# For 2 and 3 compatibility\n# https://stackoverflow.com/questions/35673474/\nABC = abc.ABCMeta(\"ABC\", (object,), {\"__slots__\": ()})\n\n\nclass BaseOfficeFile(ABC):\n    def __init__(self):\n        pass\n\n    @abc.abstractmethod\n    def load_key(self):\n        pass\n\n    @abc.abstractmethod\n    def decrypt(self, outfile):\n        pass\n\n    @abc.abstractmethod\n    def is_encrypted(self) -> bool:\n        pass\n"
  },
  {
    "path": "msoffcrypto/format/common.py",
    "content": "import io\nimport logging\nfrom struct import unpack\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\n\n# https://msdn.microsoft.com/en-us/library/dd926359(v=office.12).aspx\ndef _parse_encryptionheader(blob):\n    (flags,) = unpack(\"<I\", blob.read(4))\n    # if mode == 'strict': compare values with spec.\n    (sizeExtra,) = unpack(\"<I\", blob.read(4))\n    (algId,) = unpack(\"<I\", blob.read(4))\n    (algIdHash,) = unpack(\"<I\", blob.read(4))\n    (keySize,) = unpack(\"<I\", blob.read(4))\n    (providerType,) = unpack(\"<I\", blob.read(4))\n    (reserved1,) = unpack(\"<I\", blob.read(4))\n    (reserved2,) = unpack(\"<I\", blob.read(4))\n    cspName = blob.read().decode(\"utf-16le\")\n    header = {\n        \"flags\": flags,\n        \"sizeExtra\": sizeExtra,\n        \"algId\": algId,\n        \"algIdHash\": algIdHash,\n        \"keySize\": keySize,\n        \"providerType\": providerType,\n        \"reserved1\": reserved1,\n        \"reserved2\": reserved2,\n        \"cspName\": cspName,\n    }\n    return header\n\n\n# https://msdn.microsoft.com/en-us/library/dd910568(v=office.12).aspx\ndef _parse_encryptionverifier(blob, algorithm: str):\n    (saltSize,) = unpack(\"<I\", blob.read(4))\n    salt = blob.read(16)\n\n    encryptedVerifier = blob.read(16)\n\n    (verifierHashSize,) = unpack(\"<I\", blob.read(4))\n\n    if algorithm == \"RC4\":\n        encryptedVerifierHash = blob.read(20)\n    elif algorithm == \"AES\":\n        encryptedVerifierHash = blob.read(32)\n    else:\n        raise ValueError(\"Invalid algorithm: {}\".format(algorithm))\n\n    verifier = {\n        \"saltSize\": saltSize,\n        \"salt\": salt,\n        \"encryptedVerifier\": encryptedVerifier,\n        \"verifierHashSize\": verifierHashSize,\n        \"encryptedVerifierHash\": encryptedVerifierHash,\n    }\n\n    return verifier\n\n\ndef _parse_header_RC4CryptoAPI(encryptionHeader):\n    _flags = encryptionHeader.read(4)  # TODO: Support flags\n    (headerSize,) = unpack(\"<I\", encryptionHeader.read(4))\n    logger.debug(headerSize)\n    blob = io.BytesIO(encryptionHeader.read(headerSize))\n    header = _parse_encryptionheader(blob)\n    logger.debug(header)\n    # NOTE: https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/36cfb17f-9b15-4a9b-911a-f401f60b3991\n    keySize = 0x00000028 if header[\"keySize\"] == 0 else header[\"keySize\"]\n\n    blob = io.BytesIO(encryptionHeader.read())\n    verifier = _parse_encryptionverifier(blob, \"RC4\")  # TODO: Fix (cf. ooxml.py)\n    logger.debug(verifier)\n    info = {\n        \"salt\": verifier[\"salt\"],\n        \"keySize\": keySize,\n        \"encryptedVerifier\": verifier[\"encryptedVerifier\"],\n        \"encryptedVerifierHash\": verifier[\"encryptedVerifierHash\"],\n    }\n    return info\n"
  },
  {
    "path": "msoffcrypto/format/doc97.py",
    "content": "import io\nimport logging\nimport shutil\nimport tempfile\nfrom collections import namedtuple\nfrom struct import pack, unpack, unpack_from\n\nimport olefile\n\nfrom msoffcrypto import exceptions\nfrom msoffcrypto.format import base\nfrom msoffcrypto.format.common import _parse_header_RC4CryptoAPI\nfrom msoffcrypto.method.rc4 import DocumentRC4\nfrom msoffcrypto.method.rc4_cryptoapi import DocumentRC4CryptoAPI\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\nFibBase = namedtuple(\n    \"FibBase\",\n    [\n        \"wIdent\",\n        \"nFib\",\n        \"unused\",\n        \"lid\",\n        \"pnNext\",\n        \"fDot\",\n        \"fGlsy\",\n        \"fComplex\",\n        \"fHasPic\",\n        \"cQuickSaves\",\n        \"fEncrypted\",\n        \"fWhichTblStm\",\n        \"fReadOnlyRecommended\",\n        \"fWriteReservation\",\n        \"fExtChar\",\n        \"fLoadOverride\",\n        \"fFarEast\",\n        \"nFibBack\",\n        \"fObfuscation\",\n        \"IKey\",\n        \"envr\",\n        \"fMac\",\n        \"fEmptySpecial\",\n        \"fLoadOverridePage\",\n        \"reserved1\",\n        \"reserved2\",\n        \"fSpare0\",\n        \"reserved3\",\n        \"reserved4\",\n        \"reserved5\",\n        \"reserved6\",\n    ],\n)\n\n\ndef _parseFibBase(blob):\n    r\"\"\"\n    Pasrse FibBase binary blob.\n\n        >>> blob = io.BytesIO(b'\\xec\\xa5\\xc1\\x00G\\x00\\t\\x04\\x00\\x00\\x00\\x13\\xbf\\x004\\x00\\\n        ... \\x00\\x00\\x00\\x10\\x00\\x00\\x00\\x00\\x00\\x04\\x00\\x00\\x16\\x04\\x00\\x00')\n        >>> fibbase = _parseFibBase(blob)\n        >>> hex(fibbase.wIdent)\n        '0xa5ec'\n        >>> hex(fibbase.nFib)\n        '0xc1'\n        >>> hex(fibbase.fExtChar)\n        '0x1'\n    \"\"\"\n    getBit = lambda bits, i: (bits & (1 << i)) >> i\n    getBitSlice = lambda bits, i, w: (bits & (2**w - 1 << i)) >> i\n\n    # https://msdn.microsoft.com/en-us/library/dd944620(v=office.12).aspx\n    (buf,) = unpack_from(\"<H\", blob.read(2))\n    wIdent = buf\n\n    (buf,) = unpack_from(\"<H\", blob.read(2))\n    nFib = buf\n\n    (buf,) = unpack_from(\"<H\", blob.read(2))\n    unused = buf\n\n    (buf,) = unpack_from(\"<H\", blob.read(2))\n    lid = buf\n\n    (buf,) = unpack_from(\"<H\", blob.read(2))\n    pnNext = buf\n\n    (buf,) = unpack_from(\"<H\", blob.read(2))\n    fDot = getBit(buf, 0)\n    fGlsy = getBit(buf, 1)\n    fComplex = getBit(buf, 2)\n    fHasPic = getBit(buf, 3)\n    cQuickSaves = getBitSlice(buf, 4, 4)\n    fEncrypted = getBit(buf, 8)\n    fWhichTblStm = getBit(buf, 9)\n    fReadOnlyRecommended = getBit(buf, 10)\n    fWriteReservation = getBit(buf, 11)\n    fExtChar = getBit(buf, 12)\n    fLoadOverride = getBit(buf, 13)\n    fFarEast = getBit(buf, 14)\n    fObfuscation = getBit(buf, 15)\n\n    (buf,) = unpack_from(\"<H\", blob.read(2))\n    nFibBack = buf\n\n    (buf,) = unpack_from(\"<I\", blob.read(4))\n    IKey = buf\n\n    (buf,) = unpack_from(\"<B\", blob.read(1))\n    envr = buf\n\n    (buf,) = unpack_from(\"<B\", blob.read(1))\n    fMac = getBit(buf, 0)\n    fEmptySpecial = getBit(buf, 1)\n    fLoadOverridePage = getBit(buf, 2)\n    reserved1 = getBit(buf, 3)\n    reserved2 = getBit(buf, 4)\n    fSpare0 = getBitSlice(buf, 5, 3)\n\n    (buf,) = unpack_from(\"<H\", blob.read(2))\n    reserved3 = buf\n\n    (buf,) = unpack_from(\"<H\", blob.read(2))\n    reserved4 = buf\n\n    (buf,) = unpack_from(\"<I\", blob.read(4))\n    reserved5 = buf\n\n    (buf,) = unpack_from(\"<I\", blob.read(4))\n    reserved6 = buf\n\n    fibbase = FibBase(\n        wIdent=wIdent,\n        nFib=nFib,\n        unused=unused,\n        lid=lid,\n        pnNext=pnNext,\n        fDot=fDot,\n        fGlsy=fGlsy,\n        fComplex=fComplex,\n        fHasPic=fHasPic,\n        cQuickSaves=cQuickSaves,\n        fEncrypted=fEncrypted,\n        fWhichTblStm=fWhichTblStm,\n        fReadOnlyRecommended=fReadOnlyRecommended,\n        fWriteReservation=fWriteReservation,\n        fExtChar=fExtChar,\n        fLoadOverride=fLoadOverride,\n        fFarEast=fFarEast,\n        nFibBack=nFibBack,\n        fObfuscation=fObfuscation,\n        IKey=IKey,\n        envr=envr,\n        fMac=fMac,\n        fEmptySpecial=fEmptySpecial,\n        fLoadOverridePage=fLoadOverridePage,\n        reserved1=reserved1,\n        reserved2=reserved2,\n        fSpare0=fSpare0,\n        reserved3=reserved3,\n        reserved4=reserved4,\n        reserved5=reserved5,\n        reserved6=reserved6,\n    )\n    return fibbase\n\n\ndef _packFibBase(fibbase):\n    setBit = lambda bits, i, v: (bits & ~(1 << i)) | (v << i)\n    setBitSlice = lambda bits, i, w, v: (bits & ~((2**w - 1) << i)) | (\n        (v & (2**w - 1)) << i\n    )\n\n    blob = io.BytesIO()\n    buf = pack(\"<H\", fibbase.wIdent)\n    blob.write(buf)\n\n    buf = pack(\"<H\", fibbase.nFib)\n    blob.write(buf)\n\n    buf = pack(\"<H\", fibbase.unused)\n    blob.write(buf)\n\n    buf = pack(\"<H\", fibbase.lid)\n    blob.write(buf)\n\n    buf = pack(\"<H\", fibbase.pnNext)\n    blob.write(buf)\n\n    _buf = 0xFFFF\n    _buf = setBit(_buf, 0, fibbase.fDot)\n    _buf = setBit(_buf, 1, fibbase.fGlsy)\n    _buf = setBit(_buf, 2, fibbase.fComplex)\n    _buf = setBit(_buf, 3, fibbase.fHasPic)\n    _buf = setBitSlice(_buf, 4, 4, fibbase.cQuickSaves)\n    _buf = setBit(_buf, 8, fibbase.fEncrypted)\n    _buf = setBit(_buf, 9, fibbase.fWhichTblStm)\n    _buf = setBit(_buf, 10, fibbase.fReadOnlyRecommended)\n    _buf = setBit(_buf, 11, fibbase.fWriteReservation)\n    _buf = setBit(_buf, 12, fibbase.fExtChar)\n    _buf = setBit(_buf, 13, fibbase.fLoadOverride)\n    _buf = setBit(_buf, 14, fibbase.fFarEast)\n    _buf = setBit(_buf, 15, fibbase.fObfuscation)\n    buf = pack(\"<H\", _buf)\n    blob.write(buf)\n\n    buf = pack(\"<H\", fibbase.nFibBack)\n    blob.write(buf)\n\n    buf = pack(\"<I\", fibbase.IKey)\n    blob.write(buf)\n\n    buf = pack(\"<B\", fibbase.envr)\n    blob.write(buf)\n\n    _buf = 0xFF\n    _buf = setBit(_buf, 0, fibbase.fMac)\n    _buf = setBit(_buf, 1, fibbase.fEmptySpecial)\n    _buf = setBit(_buf, 2, fibbase.fLoadOverridePage)\n    _buf = setBit(_buf, 3, fibbase.reserved1)\n    _buf = setBit(_buf, 4, fibbase.reserved2)\n    _buf = setBitSlice(_buf, 5, 3, fibbase.fSpare0)\n    buf = pack(\"<B\", _buf)\n    blob.write(buf)\n\n    buf = pack(\"<H\", fibbase.reserved3)\n    blob.write(buf)\n\n    buf = pack(\"<H\", fibbase.reserved4)\n    blob.write(buf)\n\n    buf = pack(\"<I\", fibbase.reserved5)\n    blob.write(buf)\n\n    buf = pack(\"<I\", fibbase.reserved6)\n    blob.write(buf)\n\n    blob.seek(0)\n    return blob\n\n\ndef _parseFib(blob):\n    Fib = namedtuple(\"Fib\", [\"base\"])\n    fib = Fib(base=_parseFibBase(blob))\n    return fib\n\n\ndef _parse_header_RC4(encryptionHeader):\n    # RC4: https://msdn.microsoft.com/en-us/library/dd908560(v=office.12).aspx\n    salt = encryptionHeader.read(16)\n    encryptedVerifier = encryptionHeader.read(16)\n    encryptedVerifierHash = encryptionHeader.read(16)\n    info = {\n        \"salt\": salt,\n        \"encryptedVerifier\": encryptedVerifier,\n        \"encryptedVerifierHash\": encryptedVerifierHash,\n    }\n    return info\n\n\nclass Doc97File(base.BaseOfficeFile):\n    \"\"\"Return a MS-DOC file object.\n\n    Examples:\n        >>> with open(\"tests/inputs/rc4cryptoapi_password.doc\", \"rb\") as f:\n        ...     officefile = Doc97File(f)\n        ...     officefile.load_key(password=\"Password1234_\")\n\n        >>> with open(\"tests/inputs/rc4cryptoapi_password.doc\", \"rb\") as f:\n        ...     officefile = Doc97File(f)\n        ...     officefile.load_key(password=\"0000\")\n        Traceback (most recent call last):\n            ...\n        msoffcrypto.exceptions.InvalidKeyError: ...\n    \"\"\"\n\n    def __init__(self, file):\n        self.file = file\n        ole = olefile.OleFileIO(file)  # do not close this, would close file\n        self.ole = ole\n        self.format = \"doc97\"\n        self.keyTypes = [\"password\"]\n        self.key = None\n        self.salt = None\n\n        # https://msdn.microsoft.com/en-us/library/dd944620(v=office.12).aspx\n        with ole.openstream(\"wordDocument\") as stream:\n            fib = _parseFib(stream)\n\n        # https://msdn.microsoft.com/en-us/library/dd923367(v=office.12).aspx\n        tablename = \"1Table\" if fib.base.fWhichTblStm == 1 else \"0Table\"\n\n        Info = namedtuple(\"Info\", [\"fib\", \"tablename\"])\n        self.info = Info(\n            fib=fib,\n            tablename=tablename,\n        )\n\n    def load_key(self, password=None):\n        fib = self.info.fib\n        logger.debug(\n            \"fEncrypted: {}, fObfuscation: {}\".format(\n                fib.base.fEncrypted, fib.base.fObfuscation\n            )\n        )\n\n        if fib.base.fEncrypted == 1:\n            if fib.base.fObfuscation == 1:  # Using XOR obfuscation\n                xor_obf_password_verifier = fib.base.IKey\n                logger.debug(hex(xor_obf_password_verifier))\n            else:  # elif fib.base.fObfuscation == 0:\n                encryptionHeader_size = fib.base.IKey\n                logger.debug(\n                    \"encryptionHeader_size: {}\".format(hex(encryptionHeader_size))\n                )\n                with self.ole.openstream(self.info.tablename) as table:\n                    encryptionHeader = (\n                        table  # TODO why create a 2nd reference to same stream?\n                    )\n                    encryptionVersionInfo = table.read(4)\n                    vMajor, vMinor = unpack(\"<HH\", encryptionVersionInfo)\n                    logger.debug(\"Version: {} {}\".format(vMajor, vMinor))\n                    if vMajor == 0x0001 and vMinor == 0x0001:  # RC4\n                        info = _parse_header_RC4(encryptionHeader)\n                        if DocumentRC4.verifypw(\n                            password,\n                            info[\"salt\"],\n                            info[\"encryptedVerifier\"],\n                            info[\"encryptedVerifierHash\"],\n                        ):\n                            self.type = \"rc4\"\n                            self.key = password\n                            self.salt = info[\"salt\"]\n                        else:\n                            raise exceptions.InvalidKeyError(\n                                \"Failed to verify password\"\n                            )\n                    elif (\n                        vMajor in [0x0002, 0x0003, 0x0004] and vMinor == 0x0002\n                    ):  # RC4 CryptoAPI\n                        info = _parse_header_RC4CryptoAPI(encryptionHeader)\n                        if DocumentRC4CryptoAPI.verifypw(\n                            password,\n                            info[\"salt\"],\n                            info[\"keySize\"],\n                            info[\"encryptedVerifier\"],\n                            info[\"encryptedVerifierHash\"],\n                        ):\n                            self.type = \"rc4_cryptoapi\"\n                            self.key = password\n                            self.salt = info[\"salt\"]\n                            self.keySize = info[\"keySize\"]\n                        else:\n                            raise exceptions.InvalidKeyError(\n                                \"Failed to verify password\"\n                            )\n                    else:\n                        raise exceptions.DecryptionError(\n                            \"Unsupported encryption method\"\n                        )\n        else:\n            raise exceptions.DecryptionError(\"File is not encrypted\")\n\n    def decrypt(self, outfile):\n        # fd, _outfile_path = tempfile.mkstemp()\n\n        # shutil.copyfile(os.path.realpath(self.file.name), _outfile_path)\n        # outole = olefile.OleFileIO(_outfile_path, write_mode=True)\n\n        obuf1 = io.BytesIO()\n        fibbase = FibBase(\n            wIdent=self.info.fib.base.wIdent,\n            nFib=self.info.fib.base.nFib,\n            unused=self.info.fib.base.unused,\n            lid=self.info.fib.base.lid,\n            pnNext=self.info.fib.base.pnNext,\n            fDot=self.info.fib.base.fDot,\n            fGlsy=self.info.fib.base.fGlsy,\n            fComplex=self.info.fib.base.fComplex,\n            fHasPic=self.info.fib.base.fHasPic,\n            cQuickSaves=self.info.fib.base.cQuickSaves,\n            fEncrypted=0,\n            fWhichTblStm=self.info.fib.base.fWhichTblStm,\n            fReadOnlyRecommended=self.info.fib.base.fReadOnlyRecommended,\n            fWriteReservation=self.info.fib.base.fWriteReservation,\n            fExtChar=self.info.fib.base.fExtChar,\n            fLoadOverride=self.info.fib.base.fLoadOverride,\n            fFarEast=self.info.fib.base.fFarEast,\n            nFibBack=self.info.fib.base.nFibBack,\n            fObfuscation=0,\n            IKey=0,\n            envr=self.info.fib.base.envr,\n            fMac=self.info.fib.base.fMac,\n            fEmptySpecial=self.info.fib.base.fEmptySpecial,\n            fLoadOverridePage=self.info.fib.base.fLoadOverridePage,\n            reserved1=self.info.fib.base.reserved1,\n            reserved2=self.info.fib.base.reserved2,\n            fSpare0=self.info.fib.base.fSpare0,\n            reserved3=self.info.fib.base.reserved3,\n            reserved4=self.info.fib.base.reserved4,\n            reserved5=self.info.fib.base.reserved5,\n            reserved6=self.info.fib.base.reserved6,\n        )\n        FIB_LENGTH = 0x44\n\n        header = _packFibBase(fibbase).read()\n        logger.debug(len(header))\n        obuf1.seek(0)\n        obuf1.write(header)\n\n        with self.ole.openstream(\"wordDocument\") as worddocument:\n            worddocument.seek(len(header))\n            header = worddocument.read(FIB_LENGTH - len(header))\n            worddocument.seek(0)\n            logger.debug(len(header))\n            obuf1.write(header)\n\n            if self.type == \"rc4\":\n                dec1 = DocumentRC4.decrypt(self.key, self.salt, worddocument)\n            elif self.type == \"rc4_cryptoapi\":\n                dec1 = DocumentRC4CryptoAPI.decrypt(\n                    self.key, self.salt, self.keySize, worddocument\n                )\n            else:\n                raise exceptions.DecryptionError(\n                    \"Unsupported encryption method: {}\".format(self.type)\n                )\n\n            dec1.seek(FIB_LENGTH)\n            obuf1.write(dec1.read())\n            obuf1.seek(0)\n\n        # TODO: Preserve header\n        obuf2 = io.BytesIO()\n\n        if self.type == \"rc4\":\n            with self.ole.openstream(self.info.tablename) as stream:\n                dec2 = DocumentRC4.decrypt(self.key, self.salt, stream)\n        elif self.type == \"rc4_cryptoapi\":\n            with self.ole.openstream(self.info.tablename) as stream:\n                dec2 = DocumentRC4CryptoAPI.decrypt(\n                    self.key, self.salt, self.keySize, stream\n                )\n        else:\n            raise exceptions.DecryptionError(\n                \"Unsupported encryption method: {}\".format(self.type)\n            )\n\n        obuf2.write(dec2.read())\n        obuf2.seek(0)\n\n        obuf3 = None\n        if self.ole.exists(\"Data\"):\n            obuf3 = io.BytesIO()\n            if self.type == \"rc4\":\n                with self.ole.openstream(\"Data\") as data_stream:\n                    dec3 = DocumentRC4.decrypt(self.key, self.salt, data_stream)\n            elif self.type == \"rc4_cryptoapi\":\n                with self.ole.openstream(\"Data\") as data_stream:\n                    dec3 = DocumentRC4CryptoAPI.decrypt(\n                        self.key, self.salt, self.keySize, data_stream\n                    )\n            else:\n                raise exceptions.DecryptionError(\n                    \"Unsupported encryption method: {}\".format(self.type)\n                )\n            obuf3.write(dec3.read())\n            obuf3.seek(0)\n\n        with tempfile.TemporaryFile() as _outfile:\n            self.file.seek(0)\n            shutil.copyfileobj(self.file, _outfile)\n            outole = olefile.OleFileIO(_outfile, write_mode=True)\n\n            outole.write_stream(\"wordDocument\", obuf1.read())\n            outole.write_stream(self.info.tablename, obuf2.read())\n            if obuf3:\n                outole.write_stream(\"Data\", obuf3.read())\n\n            # _outfile = open(_outfile_path, 'rb')\n\n            _outfile.seek(0)\n\n            shutil.copyfileobj(_outfile, outfile)\n\n    def is_encrypted(self):\n        r\"\"\"\n        Test if the file is encrypted.\n\n            >>> f = open(\"tests/inputs/plain.doc\", \"rb\")\n            >>> file = Doc97File(f)\n            >>> file.is_encrypted()\n            False\n            >>> f = open(\"tests/inputs/rc4cryptoapi_password.doc\", \"rb\")\n            >>> file = Doc97File(f)\n            >>> file.is_encrypted()\n            True\n        \"\"\"\n        return True if self.info.fib.base.fEncrypted == 1 else False\n"
  },
  {
    "path": "msoffcrypto/format/ooxml.py",
    "content": "import base64\nimport io\nimport logging\nimport zipfile\nfrom struct import unpack\nfrom xml.dom.minidom import parseString\n\nimport olefile\n\nfrom msoffcrypto import exceptions\nfrom msoffcrypto.format import base\nfrom msoffcrypto.format.common import _parse_encryptionheader, _parse_encryptionverifier\nfrom msoffcrypto.method.ecma376_agile import ECMA376Agile\nfrom msoffcrypto.method.ecma376_standard import ECMA376Standard\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\n\ndef _is_ooxml(file):\n    if not zipfile.is_zipfile(file):\n        return False\n    try:\n        zfile = zipfile.ZipFile(file)\n        with zfile.open(\"[Content_Types].xml\") as stream:\n            xml = parseString(stream.read())\n            # Heuristic\n            if (\n                xml.documentElement.tagName == \"Types\"\n                and xml.documentElement.namespaceURI\n                == \"http://schemas.openxmlformats.org/package/2006/content-types\"\n            ):\n                return True\n            else:\n                return False\n    except Exception:\n        return False\n\n\ndef _parseinfo_standard(ole):\n    (headerFlags,) = unpack(\"<I\", ole.read(4))\n    (encryptionHeaderSize,) = unpack(\"<I\", ole.read(4))\n    block = ole.read(encryptionHeaderSize)\n    blob = io.BytesIO(block)\n    header = _parse_encryptionheader(blob)\n    block = ole.read()\n    blob = io.BytesIO(block)\n    algIdMap = {\n        0x0000660E: \"AES-128\",\n        0x0000660F: \"AES-192\",\n        0x00006610: \"AES-256\",\n    }\n    verifier = _parse_encryptionverifier(\n        blob, \"AES\" if header[\"algId\"] & 0xFF00 == 0x6600 else \"RC4\"\n    )  # TODO: Fix\n    info = {\n        \"header\": header,\n        \"verifier\": verifier,\n    }\n    return info\n\n\ndef _parseinfo_agile(ole):\n    ole.seek(8)\n    xml = parseString(ole.read())\n    keyDataSalt = base64.b64decode(\n        xml.getElementsByTagName(\"keyData\")[0].getAttribute(\"saltValue\")\n    )\n    keyDataHashAlgorithm = xml.getElementsByTagName(\"keyData\")[0].getAttribute(\n        \"hashAlgorithm\"\n    )\n    keyDataBlockSize = int(\n        xml.getElementsByTagName(\"keyData\")[0].getAttribute(\"blockSize\")\n    )\n    encryptedHmacKey = base64.b64decode(\n        xml.getElementsByTagName(\"dataIntegrity\")[0].getAttribute(\"encryptedHmacKey\")\n    )\n    encryptedHmacValue = base64.b64decode(\n        xml.getElementsByTagName(\"dataIntegrity\")[0].getAttribute(\"encryptedHmacValue\")\n    )\n    password_node = xml.getElementsByTagNameNS(\n        \"http://schemas.microsoft.com/office/2006/keyEncryptor/password\", \"encryptedKey\"\n    )[0]\n    spinValue = int(password_node.getAttribute(\"spinCount\"))\n    encryptedKeyValue = base64.b64decode(\n        password_node.getAttribute(\"encryptedKeyValue\")\n    )\n    encryptedVerifierHashInput = base64.b64decode(\n        password_node.getAttribute(\"encryptedVerifierHashInput\")\n    )\n    encryptedVerifierHashValue = base64.b64decode(\n        password_node.getAttribute(\"encryptedVerifierHashValue\")\n    )\n    passwordSalt = base64.b64decode(password_node.getAttribute(\"saltValue\"))\n    passwordHashAlgorithm = password_node.getAttribute(\"hashAlgorithm\")\n    passwordKeyBits = int(password_node.getAttribute(\"keyBits\"))\n    info = {\n        \"keyDataSalt\": keyDataSalt,\n        \"keyDataHashAlgorithm\": keyDataHashAlgorithm,\n        \"keyDataBlockSize\": keyDataBlockSize,\n        \"encryptedHmacKey\": encryptedHmacKey,\n        \"encryptedHmacValue\": encryptedHmacValue,\n        \"encryptedVerifierHashInput\": encryptedVerifierHashInput,\n        \"encryptedVerifierHashValue\": encryptedVerifierHashValue,\n        \"encryptedKeyValue\": encryptedKeyValue,\n        \"spinValue\": spinValue,\n        \"passwordSalt\": passwordSalt,\n        \"passwordHashAlgorithm\": passwordHashAlgorithm,\n        \"passwordKeyBits\": passwordKeyBits,\n    }\n    return info\n\n\ndef _parseinfo(ole):\n    versionMajor, versionMinor = unpack(\"<HH\", ole.read(4))\n    if versionMajor == 4 and versionMinor == 4:  # Agile\n        return \"agile\", _parseinfo_agile(ole)\n    elif versionMajor in [2, 3, 4] and versionMinor == 2:  # Standard\n        return \"standard\", _parseinfo_standard(ole)\n    elif versionMajor in [3, 4] and versionMinor == 3:  # Extensible\n        raise exceptions.DecryptionError(\n            \"Unsupported EncryptionInfo version (Extensible Encryption)\"\n        )\n    raise exceptions.DecryptionError(\n        \"Unsupported EncryptionInfo version ({}:{})\".format(versionMajor, versionMinor)\n    )\n\n\nclass OOXMLFile(base.BaseOfficeFile):\n    \"\"\"Return an OOXML file object.\n\n    Examples:\n        >>> with open(\"tests/inputs/example_password.docx\", \"rb\") as f:\n        ...     officefile = OOXMLFile(f)\n        ...     officefile.load_key(password=\"Password1234_\", verify_password=True)\n\n        >>> with open(\"tests/inputs/example_password.docx\", \"rb\") as f:\n        ...     officefile = OOXMLFile(f)\n        ...     officefile.load_key(password=\"0000\", verify_password=True)\n        Traceback (most recent call last):\n            ...\n        msoffcrypto.exceptions.InvalidKeyError: ...\n    \"\"\"\n\n    def __init__(self, file):\n        self.format = \"ooxml\"\n\n        file.seek(0)  # TODO: Investigate the effect (required for olefile.isOleFile)\n\n        # olefile cannot process non password protected ooxml files.\n        # TODO: this code is duplicate of OfficeFile(). Merge?\n        if olefile.isOleFile(file):\n            ole = olefile.OleFileIO(file)\n            self.file = ole\n\n            try:\n                with self.file.openstream(\"EncryptionInfo\") as stream:\n                    self.type, self.info = _parseinfo(stream)\n            except IOError:\n                raise exceptions.FileFormatError(\n                    \"Supposed to be an encrypted OOXML file, but no EncryptionInfo stream found\"\n                )\n\n            logger.debug(\"OOXMLFile.type: {}\".format(self.type))\n\n            self.secret_key = None\n\n            if self.type == \"agile\":\n                # TODO: Support aliases?\n                self.keyTypes = (\"password\", \"private_key\", \"secret_key\")\n            elif self.type == \"standard\":\n                self.keyTypes = (\"password\", \"secret_key\")\n            elif self.type == \"extensible\":\n                pass\n        elif _is_ooxml(file):\n            self.type = \"plain\"\n            self.file = file\n        else:\n            raise exceptions.FileFormatError(\"Unsupported file format\")\n\n    def load_key(\n        self, password=None, private_key=None, secret_key=None, verify_password=False\n    ):\n        \"\"\"\n        >>> with open(\"tests/outputs/ecma376standard_password_plain.docx\", \"rb\") as f:\n        ...     officefile = OOXMLFile(f)\n        ...     officefile.load_key(\"1234\")\n        \"\"\"\n        if password:\n            if self.type == \"agile\":\n                self.secret_key = ECMA376Agile.makekey_from_password(\n                    password,\n                    self.info[\"passwordSalt\"],\n                    self.info[\"passwordHashAlgorithm\"],\n                    self.info[\"encryptedKeyValue\"],\n                    self.info[\"spinValue\"],\n                    self.info[\"passwordKeyBits\"],\n                )\n                if verify_password:\n                    verified = ECMA376Agile.verify_password(\n                        password,\n                        self.info[\"passwordSalt\"],\n                        self.info[\"passwordHashAlgorithm\"],\n                        self.info[\"encryptedVerifierHashInput\"],\n                        self.info[\"encryptedVerifierHashValue\"],\n                        self.info[\"spinValue\"],\n                        self.info[\"passwordKeyBits\"],\n                    )\n                    if not verified:\n                        raise exceptions.InvalidKeyError(\"Key verification failed\")\n            elif self.type == \"standard\":\n                self.secret_key = ECMA376Standard.makekey_from_password(\n                    password,\n                    self.info[\"header\"][\"algId\"],\n                    self.info[\"header\"][\"algIdHash\"],\n                    self.info[\"header\"][\"providerType\"],\n                    self.info[\"header\"][\"keySize\"],\n                    self.info[\"verifier\"][\"saltSize\"],\n                    self.info[\"verifier\"][\"salt\"],\n                )\n                if verify_password:\n                    verified = ECMA376Standard.verifykey(\n                        self.secret_key,\n                        self.info[\"verifier\"][\"encryptedVerifier\"],\n                        self.info[\"verifier\"][\"encryptedVerifierHash\"],\n                    )\n                    if not verified:\n                        raise exceptions.InvalidKeyError(\"Key verification failed\")\n            elif self.type == \"extensible\":\n                pass\n            elif self.type == \"plain\":\n                pass\n        elif private_key:\n            if self.type == \"agile\":\n                self.secret_key = ECMA376Agile.makekey_from_privkey(\n                    private_key, self.info[\"encryptedKeyValue\"]\n                )\n            else:\n                raise exceptions.DecryptionError(\n                    \"Unsupported key type for the encryption method\"\n                )\n        elif secret_key:\n            self.secret_key = secret_key\n        else:\n            raise exceptions.DecryptionError(\"No key specified\")\n\n    def decrypt(self, outfile, verify_integrity=False):\n        \"\"\"\n        >>> from msoffcrypto import exceptions\n        >>> from io import BytesIO; outfile = BytesIO()\n        >>> with open(\"tests/outputs/ecma376standard_password_plain.docx\", \"rb\") as f:\n        ...     officefile = OOXMLFile(f)\n        ...     officefile.load_key(\"1234\")\n        ...     officefile.decrypt(outfile)\n        Traceback (most recent call last):\n        msoffcrypto.exceptions.DecryptionError: Document is not encrypted\n        \"\"\"\n        if self.type == \"agile\":\n            with self.file.openstream(\"EncryptedPackage\") as stream:\n                if verify_integrity:\n                    verified = ECMA376Agile.verify_integrity(\n                        self.secret_key,\n                        self.info[\"keyDataSalt\"],\n                        self.info[\"keyDataHashAlgorithm\"],\n                        self.info[\"keyDataBlockSize\"],\n                        self.info[\"encryptedHmacKey\"],\n                        self.info[\"encryptedHmacValue\"],\n                        stream,\n                    )\n                    if not verified:\n                        raise exceptions.InvalidKeyError(\n                            \"Payload integrity verification failed\"\n                        )\n\n                obuf = ECMA376Agile.decrypt(\n                    self.secret_key,\n                    self.info[\"keyDataSalt\"],\n                    self.info[\"keyDataHashAlgorithm\"],\n                    stream,\n                )\n            outfile.write(obuf)\n        elif self.type == \"standard\":\n            with self.file.openstream(\"EncryptedPackage\") as stream:\n                obuf = ECMA376Standard.decrypt(self.secret_key, stream)\n            outfile.write(obuf)\n        elif self.type == \"plain\":\n            raise exceptions.DecryptionError(\"Document is not encrypted\")\n        else:\n            raise exceptions.DecryptionError(\"Unsupported encryption method\")\n\n        # If the file is successfully decrypted, there must be a valid OOXML file, i.e. a valid zip file\n        if not zipfile.is_zipfile(io.BytesIO(obuf)):\n            raise exceptions.InvalidKeyError(\n                \"The file could not be decrypted with this password\"\n            )\n\n    def encrypt(self, password, outfile):\n        \"\"\"\n        >>> from msoffcrypto.format.ooxml import OOXMLFile\n        >>> from io import BytesIO; outfile = BytesIO()\n        >>> with open(\"tests/outputs/example.docx\", \"rb\") as f:\n        ...     officefile = OOXMLFile(f)\n        ...     officefile.encrypt(\"1234\", outfile)\n        \"\"\"\n        if self.is_encrypted():\n            raise exceptions.EncryptionError(\"File is already encrypted\")\n\n        self.file.seek(0)\n\n        buf = ECMA376Agile.encrypt(password, self.file)\n\n        if not olefile.isOleFile(buf):\n            raise exceptions.EncryptionError(\"Unable to encrypt this file\")\n\n        outfile.write(buf)\n\n    def is_encrypted(self):\n        \"\"\"\n        >>> with open(\"tests/inputs/example_password.docx\", \"rb\") as f:\n        ...     officefile = OOXMLFile(f)\n        ...     officefile.is_encrypted()\n        True\n        >>> with open(\"tests/outputs/ecma376standard_password_plain.docx\", \"rb\") as f:\n        ...     officefile = OOXMLFile(f)\n        ...     officefile.is_encrypted()\n        False\n        \"\"\"\n        # Heuristic\n        if self.type == \"plain\":\n            return False\n        elif isinstance(self.file, olefile.OleFileIO):\n            return True\n        else:\n            return False\n"
  },
  {
    "path": "msoffcrypto/format/ppt97.py",
    "content": "import io\nimport logging\nimport shutil\nimport tempfile\nfrom collections import namedtuple\nfrom struct import pack, unpack\n\nimport olefile\n\nfrom msoffcrypto import exceptions\nfrom msoffcrypto.format import base\nfrom msoffcrypto.format.common import _parse_header_RC4CryptoAPI\nfrom msoffcrypto.method.rc4_cryptoapi import DocumentRC4CryptoAPI\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\n\nRecordHeader = namedtuple(\n    \"RecordHeader\",\n    [\n        \"recVer\",\n        \"recInstance\",\n        \"recType\",\n        \"recLen\",\n    ],\n)\n\n\ndef _parseRecordHeader(blob):\n    # RecordHeader: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/df201194-0cd0-4dfb-bf10-eea353d8eabc\n    getBitSlice = lambda bits, i, w: (bits & (2**w - 1 << i)) >> i\n\n    blob.seek(0)\n\n    (buf,) = unpack(\"<H\", blob.read(2))\n    recVer = getBitSlice(buf, 0, 4)\n    recInstance = getBitSlice(buf, 4, 12)\n\n    (recType,) = unpack(\"<H\", blob.read(2))\n    (recLen,) = unpack(\"<I\", blob.read(4))\n\n    rh = RecordHeader(\n        recVer=recVer,\n        recInstance=recInstance,\n        recType=recType,\n        recLen=recLen,\n    )\n\n    return rh\n\n\ndef _packRecordHeader(rh):\n    setBitSlice = lambda bits, i, w, v: (bits & ~((2**w - 1) << i)) | (\n        (v & (2**w - 1)) << i\n    )\n\n    blob = io.BytesIO()\n\n    _buf = 0xFFFF\n    _buf = setBitSlice(_buf, 0, 4, rh.recVer)\n    _buf = setBitSlice(_buf, 4, 12, rh.recInstance)\n    buf = pack(\"<H\", _buf)\n    blob.write(buf)\n\n    buf = pack(\"<H\", rh.recType)\n    blob.write(buf)\n\n    buf = pack(\"<I\", rh.recLen)\n    blob.write(buf)\n\n    blob.seek(0)\n\n    return blob\n\n\nCurrentUserAtom = namedtuple(\n    \"CurrentUserAtom\",\n    [\n        \"rh\",\n        \"size\",\n        \"headerToken\",\n        \"offsetToCurrentEdit\",\n        \"lenUserName\",\n        \"docFileVersion\",\n        \"majorVersion\",\n        \"minorVersion\",\n        \"unused\",\n        \"ansiUserName\",\n        \"relVersion\",\n        \"unicodeUserName\",\n    ],\n)\n\n\ndef _parseCurrentUserAtom(blob):\n    # CurrentUserAtom: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/940d5700-e4d7-4fc0-ab48-fed5dbc48bc1\n\n    # rh (8 bytes): A RecordHeader structure...\n    buf = io.BytesIO(blob.read(8))\n    rh = _parseRecordHeader(buf)\n    # logger.debug(rh)\n\n    # ...Sub-fields are further specified in the following table.\n    assert rh.recVer == 0x0\n    assert rh.recInstance == 0x000\n    assert rh.recType == 0x0FF6\n\n    (size,) = unpack(\"<I\", blob.read(4))\n    # logger.debug(hex(size))\n\n    # size (4 bytes): ...It MUST be 0x00000014.\n    assert size == 0x00000014\n\n    # headerToken (4 bytes): An unsigned integer that specifies\n    # a token used to identify whether the file is encrypted.\n    (headerToken,) = unpack(\"<I\", blob.read(4))\n\n    # TODO: Check headerToken value\n\n    (offsetToCurrentEdit,) = unpack(\"<I\", blob.read(4))\n\n    (lenUserName,) = unpack(\"<H\", blob.read(2))\n    (docFileVersion,) = unpack(\"<H\", blob.read(2))\n    (\n        majorVersion,\n        minorVersion,\n    ) = unpack(\"<BB\", blob.read(2))\n    unused = blob.read(2)\n    ansiUserName = blob.read(lenUserName)\n    (relVersion,) = unpack(\"<I\", blob.read(4))\n    unicodeUserName = blob.read(2 * lenUserName)\n\n    return CurrentUserAtom(\n        rh=rh,\n        size=size,\n        headerToken=headerToken,\n        offsetToCurrentEdit=offsetToCurrentEdit,\n        lenUserName=lenUserName,\n        docFileVersion=docFileVersion,\n        majorVersion=majorVersion,\n        minorVersion=minorVersion,\n        unused=unused,\n        ansiUserName=ansiUserName,\n        relVersion=relVersion,\n        unicodeUserName=unicodeUserName,\n    )\n\n\ndef _packCurrentUserAtom(currentuseratom):\n    blob = io.BytesIO()\n\n    buf = _packRecordHeader(currentuseratom.rh).read()\n    blob.write(buf)\n    buf = pack(\"<I\", currentuseratom.size)\n    blob.write(buf)\n    buf = pack(\"<I\", currentuseratom.headerToken)\n    blob.write(buf)\n    buf = pack(\"<I\", currentuseratom.offsetToCurrentEdit)\n    blob.write(buf)\n    buf = pack(\"<H\", currentuseratom.lenUserName)\n    blob.write(buf)\n    buf = pack(\"<H\", currentuseratom.docFileVersion)\n    blob.write(buf)\n    buf = pack(\"<BB\", currentuseratom.majorVersion, currentuseratom.minorVersion)\n    blob.write(buf)\n    buf = currentuseratom.unused\n    blob.write(buf)\n    buf = currentuseratom.ansiUserName\n    blob.write(buf)\n    buf = pack(\"<I\", currentuseratom.relVersion)\n    blob.write(buf)\n    buf = currentuseratom.unicodeUserName\n    blob.write(buf)\n\n    blob.seek(0)\n\n    return blob\n\n\nCurrentUser = namedtuple(\"CurrentUser\", [\"currentuseratom\"])\n\n\ndef _parseCurrentUser(blob):\n    # Current User Stream: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/76cfa657-07a6-464b-81ab-4c017c611f64\n    currentuser = CurrentUser(currentuseratom=_parseCurrentUserAtom(blob))\n    return currentuser\n\n\ndef _packCurrentUser(currentuser):\n    blob = io.BytesIO()\n\n    buf = _packCurrentUserAtom(currentuser.currentuseratom).read()\n    blob.write(buf)\n\n    blob.seek(0)\n\n    return blob\n\n\nUserEditAtom = namedtuple(\n    \"UserEditAtom\",\n    [\n        \"rh\",\n        \"lastSlideIdRef\",\n        \"version\",\n        \"minorVersion\",\n        \"majorVersion\",\n        \"offsetLastEdit\",\n        \"offsetPersistDirectory\",\n        \"docPersistIdRef\",\n        \"persistIdSeed\",\n        \"lastView\",\n        \"unused\",\n        \"encryptSessionPersistIdRef\",\n    ],\n)\n\n\ndef _parseUserEditAtom(blob):\n    # UserEditAtom: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/3ffb3fab-95de-4873-98aa-d508fbbac981\n\n    # rh (8 bytes): A RecordHeader structure...\n    buf = io.BytesIO(blob.read(8))\n    rh = _parseRecordHeader(buf)\n    # logger.debug(rh)\n\n    # ...Sub-fields are further specified in the following table.\n    assert rh.recVer == 0x0\n    assert rh.recInstance == 0x000\n    assert rh.recType == 0x0FF5\n    assert (\n        rh.recLen == 0x0000001C or rh.recLen == 0x00000020\n    )  # 0x0000001c + len(encryptSessionPersistIdRef)\n\n    (lastSlideIdRef,) = unpack(\"<I\", blob.read(4))\n    (version,) = unpack(\"<H\", blob.read(2))\n    (\n        minorVersion,\n        majorVersion,\n    ) = unpack(\"<BB\", blob.read(2))\n    # majorVersion, minorVersion, = unpack(\"<BB\", blob.read(2))\n\n    (offsetLastEdit,) = unpack(\"<I\", blob.read(4))\n    (offsetPersistDirectory,) = unpack(\"<I\", blob.read(4))\n    (docPersistIdRef,) = unpack(\"<I\", blob.read(4))\n\n    (persistIdSeed,) = unpack(\"<I\", blob.read(4))\n    (lastView,) = unpack(\"<H\", blob.read(2))\n    unused = blob.read(2)\n\n    # encryptSessionPersistIdRef (4 bytes): An optional PersistIdRef\n    # that specifies the value to look up in the persist object directory\n    # to find the offset of the CryptSession10Container record (section 2.3.7).\n    buf = blob.read(4)\n    if len(buf) == 4:\n        (encryptSessionPersistIdRef,) = unpack(\"<I\", buf)\n    else:\n        encryptSessionPersistIdRef = None\n\n    return UserEditAtom(\n        rh=rh,\n        lastSlideIdRef=lastSlideIdRef,\n        version=version,\n        minorVersion=minorVersion,\n        majorVersion=majorVersion,\n        offsetLastEdit=offsetLastEdit,\n        offsetPersistDirectory=offsetPersistDirectory,\n        docPersistIdRef=docPersistIdRef,\n        persistIdSeed=persistIdSeed,\n        lastView=lastView,\n        unused=unused,\n        encryptSessionPersistIdRef=encryptSessionPersistIdRef,\n    )\n\n\ndef _packUserEditAtom(usereditatom):\n    blob = io.BytesIO()\n\n    buf = _packRecordHeader(usereditatom.rh).read()\n    blob.write(buf)\n    buf = pack(\"<I\", usereditatom.lastSlideIdRef)\n    blob.write(buf)\n    buf = pack(\"<H\", usereditatom.version)\n    blob.write(buf)\n    buf = pack(\"<BB\", usereditatom.minorVersion, usereditatom.majorVersion)\n    blob.write(buf)\n    buf = pack(\"<I\", usereditatom.offsetLastEdit)\n    blob.write(buf)\n    buf = pack(\"<I\", usereditatom.offsetPersistDirectory)\n    blob.write(buf)\n    buf = pack(\"<I\", usereditatom.docPersistIdRef)\n    blob.write(buf)\n    buf = pack(\"<I\", usereditatom.persistIdSeed)\n    blob.write(buf)\n    buf = pack(\"<H\", usereditatom.lastView)\n    blob.write(buf)\n    buf = usereditatom.unused\n    blob.write(buf)\n    # Optional value\n    if usereditatom.encryptSessionPersistIdRef is not None:\n        buf = pack(\"<I\", usereditatom.encryptSessionPersistIdRef)\n        blob.write(buf)\n\n    blob.seek(0)\n\n    return blob\n\n\nPersistDirectoryEntry = namedtuple(\n    \"PersistDirectoryEntry\",\n    [\n        \"persistId\",\n        \"cPersist\",\n        \"rgPersistOffset\",\n    ],\n)\n\n\ndef _parsePersistDirectoryEntry(blob):\n    # PersistDirectoryEntry: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/6214b5a6-7ca2-4a86-8a0e-5fd3d3eff1c9\n    getBitSlice = lambda bits, i, w: (bits & (2**w - 1 << i)) >> i\n\n    (buf,) = unpack(\"<I\", blob.read(4))\n    persistId = getBitSlice(buf, 0, 20)\n    cPersist = getBitSlice(buf, 20, 12)\n\n    # cf. PersistOffsetEntry: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/a056484a-2132-4e1e-aa54-6e387f9695cf\n    size_rgPersistOffset = 4 * cPersist\n    _rgPersistOffset = blob.read(size_rgPersistOffset)\n    _rgPersistOffset = io.BytesIO(_rgPersistOffset)\n    rgPersistOffset = []\n    pos = 0\n    while pos < size_rgPersistOffset:\n        (persistoffsetentry,) = unpack(\"<I\", _rgPersistOffset.read(4))\n        rgPersistOffset.append(persistoffsetentry)\n        pos += 4\n\n    return PersistDirectoryEntry(\n        persistId=persistId,\n        cPersist=cPersist,\n        rgPersistOffset=rgPersistOffset,\n    )\n\n\ndef _packPersistDirectoryEntry(directoryentry):\n    setBitSlice = lambda bits, i, w, v: (bits & ~((2**w - 1) << i)) | (\n        (v & (2**w - 1)) << i\n    )\n\n    blob = io.BytesIO()\n\n    _buf = 0xFFFFFFFF\n    _buf = setBitSlice(_buf, 0, 20, directoryentry.persistId)\n    _buf = setBitSlice(_buf, 20, 12, directoryentry.cPersist)\n    buf = pack(\"<I\", _buf)\n    blob.write(buf)\n\n    for v in directoryentry.rgPersistOffset:\n        buf = pack(\"<I\", v)\n        blob.write(buf)\n\n    blob.seek(0)\n\n    return blob\n\n\nPersistDirectoryAtom = namedtuple(\n    \"PersistDirectoryAtom\",\n    [\n        \"rh\",\n        \"rgPersistDirEntry\",\n    ],\n)\n\n\ndef _parsePersistDirectoryAtom(blob):\n    # PersistDirectoryAtom: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/d10a093d-860f-409c-b065-aeb24b830505\n\n    # rh (8 bytes): A RecordHeader structure...\n    buf = io.BytesIO(blob.read(8))\n    rh = _parseRecordHeader(buf)\n    # logger.debug(rh)\n\n    # ...Sub-fields are further specified in the following table.\n    assert rh.recVer == 0x0\n    assert rh.recInstance == 0x000\n    assert rh.recType == 0x1772\n\n    _rgPersistDirEntry = blob.read(rh.recLen)\n    _rgPersistDirEntry = io.BytesIO(_rgPersistDirEntry)\n    rgPersistDirEntry = []\n    pos = 0\n    while pos < rh.recLen:\n        persistdirectoryentry = _parsePersistDirectoryEntry(_rgPersistDirEntry)\n        size_persistdirectoryentry = 4 + 4 * len(persistdirectoryentry.rgPersistOffset)\n        # logger.debug((persistdirectoryentry, size_persistdirectoryentry))\n        rgPersistDirEntry.append(persistdirectoryentry)\n        pos += size_persistdirectoryentry\n\n    return PersistDirectoryAtom(\n        rh=rh,\n        rgPersistDirEntry=rgPersistDirEntry,\n    )\n\n\ndef _packPersistDirectoryAtom(directoryatom):\n    blob = io.BytesIO()\n\n    buf = _packRecordHeader(directoryatom.rh).read()\n    blob.write(buf)\n\n    for v in directoryatom.rgPersistDirEntry:\n        buf = _packPersistDirectoryEntry(v)\n        blob.write(buf.read())\n\n    blob.seek(0)\n\n    return blob\n\n\ndef _parseCryptSession10Container(blob):\n    # CryptSession10Container: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/b0963334-4408-4621-879a-ef9c54551fd8\n\n    CryptSession10Container = namedtuple(\n        \"CryptSession10Container\",\n        [\n            \"rh\",\n            \"data\",\n        ],\n    )\n\n    # rh (8 bytes): A RecordHeader structure...\n    buf = io.BytesIO(blob.read(8))\n    rh = _parseRecordHeader(buf)\n    # logger.debug(rh)\n\n    # ...Sub-fields are further specified in the following table.\n    assert rh.recVer == 0xF\n    # The specified value fails\n    # assert rh.recInstance == 0x000\n    assert rh.recType == 0x2F14\n\n    data = blob.read(rh.recLen)\n\n    return CryptSession10Container(\n        rh=rh,\n        data=data,\n    )\n\n\ndef construct_persistobjectdirectory(data):\n    # PowerPoint Document Stream: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/1fc22d56-28f9-4818-bd45-67c2bf721ccf\n\n    # 1. Read the CurrentUserAtom record (section 2.3.2) from the Current User Stream (section 2.1.1). ...\n    data.currentuser.seek(0)\n    currentuser = _parseCurrentUser(data.currentuser)\n    # logger.debug(currentuser)\n\n    # 2. Seek, in the PowerPoint Document Stream, to the offset specified by the offsetToCurrentEdit field of\n    # the CurrentUserAtom record identified in step 1.\n    data.powerpointdocument.seek(currentuser.currentuseratom.offsetToCurrentEdit)\n\n    persistdirectoryatom_stack = []\n\n    # The stream MUST contain exactly one UserEditAtom record.\n    # https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/b0963334-4408-4621-879a-ef9c54551fd8\n    for i in range(1):\n        # 3. Read the UserEditAtom record at the current offset. ...\n        usereditatom = _parseUserEditAtom(data.powerpointdocument)\n        # logger.debug(usereditatom)\n\n        # 4. Seek to the offset specified by the offsetPersistDirectory field of the UserEditAtom record identified in step 3.\n        data.powerpointdocument.seek(usereditatom.offsetPersistDirectory)\n\n        # 5. Read the PersistDirectoryAtom record at the current offset. ...\n        persistdirectoryatom = _parsePersistDirectoryAtom(data.powerpointdocument)\n        # logger.debug(persistdirectoryatom)\n        persistdirectoryatom_stack.append(persistdirectoryatom)\n\n        # 6. Seek to the offset specified by the offsetLastEdit field in the UserEditAtom record identified in step 3.\n        # 7. Repeat steps 3 through 6 until offsetLastEdit is 0x00000000.\n        if usereditatom.offsetLastEdit == 0x00000000:\n            break\n        else:\n            data.powerpointdocument.seek(usereditatom.offsetLastEdit)\n\n    # 8. Construct the complete persist object directory for this file as follows:\n    persistobjectdirectory = {}\n\n    # 8a. For each PersistDirectoryAtom record previously identified in step 5,\n    # add the persist object identifier and persist object stream offset pairs to\n    # the persist object directory starting with the PersistDirectoryAtom record\n    # last identified, that is, the one closest to the beginning of the stream.\n    # 8b. Continue adding these pairs to the persist object directory for each PersistDirectoryAtom record\n    # in the reverse order that they were identified in step 5; that is, the pairs from the PersistDirectoryAtom record\n    # closest to the end of the stream are added last.\n    # 8c. When adding a new pair to the persist object directory, if the persist object identifier\n    # already exists in the persist object directory, the persist object stream offset from\n    # the new pair replaces the existing persist object stream offset for that persist object identifier.\n    while len(persistdirectoryatom_stack) > 0:\n        persistdirectoryatom = persistdirectoryatom_stack.pop()\n        for entry in persistdirectoryatom.rgPersistDirEntry:\n            # logger.debug(\"persistId: %d\" % entry.persistId)\n            for i, offset in enumerate(entry.rgPersistOffset):\n                persistobjectdirectory[entry.persistId + i] = offset\n\n    return persistobjectdirectory\n\n\nclass Ppt97File(base.BaseOfficeFile):\n    \"\"\"Return a MS-PPT file object.\n\n    Examples:\n        >>> with open(\"tests/inputs/rc4cryptoapi_password.ppt\", \"rb\") as f:\n        ...     officefile = Ppt97File(f)\n        ...     officefile.load_key(password=\"Password1234_\")\n\n        >>> with open(\"tests/inputs/rc4cryptoapi_password.ppt\", \"rb\") as f:\n        ...     officefile = Ppt97File(f)\n        ...     officefile.load_key(password=\"0000\")\n        Traceback (most recent call last):\n            ...\n        msoffcrypto.exceptions.InvalidKeyError: ...\n    \"\"\"\n\n    def __init__(self, file):\n        self.file = file\n        ole = olefile.OleFileIO(file)  # do not close this, would close file\n        self.ole = ole\n        self.format = \"ppt97\"\n        self.keyTypes = [\"password\"]\n        self.key = None\n        self.salt = None\n\n        # streams closed in destructor:\n        currentuser = ole.openstream(\"Current User\")\n        powerpointdocument = ole.openstream(\"PowerPoint Document\")\n\n        Data = namedtuple(\"Data\", [\"currentuser\", \"powerpointdocument\"])\n        self.data = Data(\n            currentuser=currentuser,\n            powerpointdocument=powerpointdocument,\n        )\n\n    def __del__(self):\n        \"\"\"Destructor, closes opened streams.\"\"\"\n        if hasattr(self, \"data\") and self.data:\n            if self.data.currentuser:\n                self.data.currentuser.close()\n            if self.data.powerpointdocument:\n                self.data.powerpointdocument.close()\n\n    def load_key(self, password=None):\n        persistobjectdirectory = construct_persistobjectdirectory(self.data)\n        logger.debug(\"[*] persistobjectdirectory: {}\".format(persistobjectdirectory))\n\n        self.data.currentuser.seek(0)\n        currentuser = _parseCurrentUser(self.data.currentuser)\n        logger.debug(\"[*] currentuser: {}\".format(currentuser))\n\n        self.data.powerpointdocument.seek(\n            currentuser.currentuseratom.offsetToCurrentEdit\n        )\n        usereditatom = _parseUserEditAtom(self.data.powerpointdocument)\n        logger.debug(\"[*] usereditatom: {}\".format(usereditatom))\n\n        # cf. Part 2 in https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/1fc22d56-28f9-4818-bd45-67c2bf721ccf\n        cryptsession10container_offset = persistobjectdirectory[\n            usereditatom.encryptSessionPersistIdRef\n        ]\n        logger.debug(\n            \"[*] cryptsession10container_offset: {}\".format(\n                cryptsession10container_offset\n            )\n        )\n\n        self.data.powerpointdocument.seek(cryptsession10container_offset)\n        cryptsession10container = _parseCryptSession10Container(\n            self.data.powerpointdocument\n        )\n        logger.debug(\"[*] cryptsession10container: {}\".format(cryptsession10container))\n\n        encryptionInfo = io.BytesIO(cryptsession10container.data)\n\n        encryptionVersionInfo = encryptionInfo.read(4)\n        vMajor, vMinor = unpack(\"<HH\", encryptionVersionInfo)\n        logger.debug(\"[*] encryption version: {} {}\".format(vMajor, vMinor))\n\n        assert vMajor in [0x0002, 0x0003, 0x0004] and vMinor == 0x0002  # RC4 CryptoAPI\n\n        info = _parse_header_RC4CryptoAPI(encryptionInfo)\n        if DocumentRC4CryptoAPI.verifypw(\n            password,\n            info[\"salt\"],\n            info[\"keySize\"],\n            info[\"encryptedVerifier\"],\n            info[\"encryptedVerifierHash\"],\n        ):\n            self.type = \"rc4_cryptoapi\"\n            self.key = password\n            self.salt = info[\"salt\"]\n            self.keySize = info[\"keySize\"]\n        else:\n            raise exceptions.InvalidKeyError(\"Failed to verify password\")\n\n    def decrypt(self, outfile):\n        # Current User Stream\n        self.data.currentuser.seek(0)\n        currentuser = _parseCurrentUser(self.data.currentuser)\n        # logger.debug(currentuser)\n\n        cuatom = currentuser.currentuseratom\n\n        currentuser_new = CurrentUser(\n            currentuseratom=CurrentUserAtom(\n                rh=cuatom.rh,\n                size=cuatom.size,\n                # https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/940d5700-e4d7-4fc0-ab48-fed5dbc48bc1\n                # 0xE391C05F: The file SHOULD NOT<6> be an encrypted document.\n                headerToken=0xE391C05F,\n                offsetToCurrentEdit=cuatom.offsetToCurrentEdit,\n                lenUserName=cuatom.lenUserName,\n                docFileVersion=cuatom.docFileVersion,\n                majorVersion=cuatom.majorVersion,\n                minorVersion=cuatom.minorVersion,\n                unused=cuatom.unused,\n                ansiUserName=cuatom.ansiUserName,\n                relVersion=cuatom.relVersion,\n                unicodeUserName=cuatom.unicodeUserName,\n            )\n        )\n\n        buf = _packCurrentUser(currentuser_new)\n        buf.seek(0)\n        currentuser_buf = buf\n\n        # List of encrypted parts: https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-ppt/b0963334-4408-4621-879a-ef9c54551fd8\n\n        # PowerPoint Document Stream\n\n        self.data.powerpointdocument.seek(0)\n        powerpointdocument_size = len(self.data.powerpointdocument.read())\n        logger.debug(\"[*] powerpointdocument_size: {}\".format(powerpointdocument_size))\n\n        self.data.powerpointdocument.seek(0)\n        dec_bytearray = bytearray(self.data.powerpointdocument.read())\n\n        # UserEditAtom\n        self.data.powerpointdocument.seek(\n            currentuser.currentuseratom.offsetToCurrentEdit\n        )\n        # currentuseratom_raw = self.data.powerpointdocument.read(40)\n\n        self.data.powerpointdocument.seek(\n            currentuser.currentuseratom.offsetToCurrentEdit\n        )\n        usereditatom = _parseUserEditAtom(self.data.powerpointdocument)\n        # logger.debug(usereditatom)\n        # logger.debug([\"offsetToCurrentEdit\", currentuser.currentuseratom.offsetToCurrentEdit])\n\n        rh_new = RecordHeader(\n            recVer=usereditatom.rh.recVer,\n            recInstance=usereditatom.rh.recInstance,\n            recType=usereditatom.rh.recType,\n            recLen=usereditatom.rh.recLen - 4,  # Omit encryptSessionPersistIdRef field\n        )\n\n        # logger.debug([_packRecordHeader(usereditatom.rh).read(), _packRecordHeader(rh_new).read()])\n\n        usereditatom_new = UserEditAtom(\n            rh=rh_new,\n            lastSlideIdRef=usereditatom.lastSlideIdRef,\n            version=usereditatom.version,\n            minorVersion=usereditatom.minorVersion,\n            majorVersion=usereditatom.majorVersion,\n            offsetLastEdit=usereditatom.offsetLastEdit,\n            offsetPersistDirectory=usereditatom.offsetPersistDirectory,\n            docPersistIdRef=usereditatom.docPersistIdRef,\n            persistIdSeed=usereditatom.persistIdSeed,\n            lastView=usereditatom.lastView,\n            unused=usereditatom.unused,\n            encryptSessionPersistIdRef=0x00000000,  # Clear\n        )\n\n        # logger.debug(currentuseratom_raw)\n        # logger.debug(_packUserEditAtom(usereditatom).read())\n        # logger.debug(_packUserEditAtom(usereditatom_new).read())\n\n        buf = _packUserEditAtom(usereditatom_new)\n        buf.seek(0)\n        buf_bytes = bytearray(buf.read())\n        offset = currentuser.currentuseratom.offsetToCurrentEdit\n        dec_bytearray[offset : offset + len(buf_bytes)] = buf_bytes\n\n        # PersistDirectoryAtom\n        self.data.powerpointdocument.seek(\n            currentuser.currentuseratom.offsetToCurrentEdit\n        )\n        usereditatom = _parseUserEditAtom(self.data.powerpointdocument)\n        # logger.debug(usereditatom)\n\n        self.data.powerpointdocument.seek(usereditatom.offsetPersistDirectory)\n        persistdirectoryatom = _parsePersistDirectoryAtom(self.data.powerpointdocument)\n        # logger.debug(persistdirectoryatom)\n\n        persistdirectoryatom_new = PersistDirectoryAtom(\n            rh=persistdirectoryatom.rh,\n            rgPersistDirEntry=[\n                PersistDirectoryEntry(\n                    persistId=persistdirectoryatom.rgPersistDirEntry[0].persistId,\n                    # Omit CryptSession10Container\n                    cPersist=persistdirectoryatom.rgPersistDirEntry[0].cPersist - 1,\n                    rgPersistOffset=persistdirectoryatom.rgPersistDirEntry[\n                        0\n                    ].rgPersistOffset,\n                ),\n            ],\n        )\n\n        self.data.powerpointdocument.seek(usereditatom.offsetPersistDirectory)\n        buf = _packPersistDirectoryAtom(persistdirectoryatom_new)\n        buf_bytes = bytearray(buf.read())\n        offset = usereditatom.offsetPersistDirectory\n        dec_bytearray[offset : offset + len(buf_bytes)] = buf_bytes\n\n        # Persist Objects\n        self.data.powerpointdocument.seek(0)\n        persistobjectdirectory = construct_persistobjectdirectory(self.data)\n\n        directory_items = list(persistobjectdirectory.items())\n\n        for i, (persistId, offset) in enumerate(directory_items):\n            self.data.powerpointdocument.seek(offset)\n            buf = self.data.powerpointdocument.read(8)\n            rh = _parseRecordHeader(io.BytesIO(buf))\n            logger.debug(\"[*] rh: {}\".format(rh))\n\n            # CryptSession10Container\n            if rh.recType == 0x2F14:\n                logger.debug(\"[*] CryptSession10Container found\")\n                # Remove encryption, pad by zero to preserve stream size\n                dec_bytearray[offset : offset + (8 + rh.recLen)] = b\"\\x00\" * (\n                    8 + rh.recLen\n                )\n                continue\n\n            # The UserEditAtom record (section 2.3.3) and the PersistDirectoryAtom record (section 2.3.4) MUST NOT be encrypted.\n            if rh.recType in [0x0FF5, 0x1772]:\n                logger.debug(\"[*] UserEditAtom/PersistDirectoryAtom found\")\n                continue\n\n            # TODO: Fix here\n            recLen = directory_items[i + 1][1] - offset - 8\n            logger.debug(\"[*] recLen: {}\".format(recLen))\n\n            self.data.powerpointdocument.seek(offset)\n            enc_buf = io.BytesIO(self.data.powerpointdocument.read(8 + recLen))\n            blocksize = self.keySize * (\n                (8 + recLen) // self.keySize + 1\n            )  # Undocumented\n            dec = DocumentRC4CryptoAPI.decrypt(\n                self.key,\n                self.salt,\n                self.keySize,\n                enc_buf,\n                blocksize=blocksize,\n                block=persistId,\n            )\n            dec_bytes = bytearray(dec.read())\n            dec_bytearray[offset : offset + len(dec_bytes)] = dec_bytes\n\n        # To BytesIO\n        dec_buf = io.BytesIO(dec_bytearray)\n\n        dec_buf.seek(0)\n        for i, (persistId, offset) in enumerate(directory_items):\n            dec_buf.seek(offset)\n            buf = dec_buf.read(8)\n            rh = _parseRecordHeader(io.BytesIO(buf))\n            logger.debug(\"[*] rh: {}\".format(rh))\n\n        dec_buf.seek(0)\n        logger.debug(\n            \"[*] powerpointdocument_size={}, len(dec_buf.read())={}\".format(\n                powerpointdocument_size, len(dec_buf.read())\n            )\n        )\n\n        dec_buf.seek(0)\n        powerpointdocument_dec_buf = dec_buf\n\n        # TODO: Pictures Stream\n        # TODO: Encrypted Summary Info Stream\n\n        with tempfile.TemporaryFile() as _outfile:\n            self.file.seek(0)\n            shutil.copyfileobj(self.file, _outfile)\n            outole = olefile.OleFileIO(_outfile, write_mode=True)\n\n            outole.write_stream(\"Current User\", currentuser_buf.read())\n            outole.write_stream(\n                \"PowerPoint Document\", powerpointdocument_dec_buf.read()\n            )\n\n            # Finalize\n            _outfile.seek(0)\n            shutil.copyfileobj(_outfile, outfile)\n\n        return\n\n    def is_encrypted(self):\n        r\"\"\"\n        Test if the file is encrypted.\n\n            >>> f = open(\"tests/inputs/plain.ppt\", \"rb\")\n            >>> file = Ppt97File(f)\n            >>> file.is_encrypted()\n            False\n            >>> f = open(\"tests/inputs/rc4cryptoapi_password.ppt\", \"rb\")\n            >>> file = Ppt97File(f)\n            >>> file.is_encrypted()\n            True\n        \"\"\"\n        self.data.currentuser.seek(0)\n        currentuser = _parseCurrentUser(self.data.currentuser)\n        logger.debug(\"[*] currentuser: {}\".format(currentuser))\n\n        self.data.powerpointdocument.seek(\n            currentuser.currentuseratom.offsetToCurrentEdit\n        )\n        usereditatom = _parseUserEditAtom(self.data.powerpointdocument)\n        logger.debug(\"[*] usereditatom: {}\".format(usereditatom))\n\n        if usereditatom.rh.recLen == 0x00000020:  # Cf. _parseUserEditAtom\n            return True\n        else:\n            return False\n"
  },
  {
    "path": "msoffcrypto/format/xls97.py",
    "content": "import io\nimport logging\nimport shutil\nimport tempfile\nfrom collections import namedtuple\nfrom struct import pack, unpack\n\nimport olefile\n\nfrom msoffcrypto import exceptions\nfrom msoffcrypto.format import base\nfrom msoffcrypto.format.common import _parse_header_RC4CryptoAPI\nfrom msoffcrypto.method.rc4 import DocumentRC4\nfrom msoffcrypto.method.rc4_cryptoapi import DocumentRC4CryptoAPI\nfrom msoffcrypto.method.xor_obfuscation import DocumentXOR\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\n\nrecordNameNum = {\n    \"Formula\": 6,\n    \"EOF\": 10,\n    \"CalcCount\": 12,\n    \"CalcMode\": 13,\n    \"CalcPrecision\": 14,\n    \"CalcRefMode\": 15,\n    \"CalcDelta\": 16,\n    \"CalcIter\": 17,\n    \"Protect\": 18,\n    \"Password\": 19,\n    \"Header\": 20,\n    \"Footer\": 21,\n    \"ExternSheet\": 23,\n    \"Lbl\": 24,\n    \"WinProtect\": 25,\n    \"VerticalPageBreaks\": 26,\n    \"HorizontalPageBreaks\": 27,\n    \"Note\": 28,\n    \"Selection\": 29,\n    \"Date1904\": 34,\n    \"ExternName\": 35,\n    \"LeftMargin\": 38,\n    \"RightMargin\": 39,\n    \"TopMargin\": 40,\n    \"BottomMargin\": 41,\n    \"PrintRowCol\": 42,\n    \"PrintGrid\": 43,\n    \"FilePass\": 47,\n    \"Font\": 49,\n    \"PrintSize\": 51,\n    \"Continue\": 60,\n    \"Window1\": 61,\n    \"Backup\": 64,\n    \"Pane\": 65,\n    \"CodePage\": 66,\n    \"Pls\": 77,\n    \"DCon\": 80,\n    \"DConRef\": 81,\n    \"DConName\": 82,\n    \"DefColWidth\": 85,\n    \"XCT\": 89,\n    \"CRN\": 90,\n    \"FileSharing\": 91,\n    \"WriteAccess\": 92,\n    \"Obj\": 93,\n    \"Uncalced\": 94,\n    \"CalcSaveRecalc\": 95,\n    \"Template\": 96,\n    \"Intl\": 97,\n    \"ObjProtect\": 99,\n    \"ColInfo\": 125,\n    \"Guts\": 128,\n    \"WsBool\": 129,\n    \"GridSet\": 130,\n    \"HCenter\": 131,\n    \"VCenter\": 132,\n    \"BoundSheet8\": 133,\n    \"WriteProtect\": 134,\n    \"Country\": 140,\n    \"HideObj\": 141,\n    \"Sort\": 144,\n    \"Palette\": 146,\n    \"Sync\": 151,\n    \"LPr\": 152,\n    \"DxGCol\": 153,\n    \"FnGroupName\": 154,\n    \"FilterMode\": 155,\n    \"BuiltInFnGroupCount\": 156,\n    \"AutoFilterInfo\": 157,\n    \"AutoFilter\": 158,\n    \"Scl\": 160,\n    \"Setup\": 161,\n    \"ScenMan\": 174,\n    \"SCENARIO\": 175,\n    \"SxView\": 176,\n    \"Sxvd\": 177,\n    \"SXVI\": 178,\n    \"SxIvd\": 180,\n    \"SXLI\": 181,\n    \"SXPI\": 182,\n    \"DocRoute\": 184,\n    \"RecipName\": 185,\n    \"MulRk\": 189,\n    \"MulBlank\": 190,\n    \"Mms\": 193,\n    \"SXDI\": 197,\n    \"SXDB\": 198,\n    \"SXFDB\": 199,\n    \"SXDBB\": 200,\n    \"SXNum\": 201,\n    \"SxBool\": 202,\n    \"SxErr\": 203,\n    \"SXInt\": 204,\n    \"SXString\": 205,\n    \"SXDtr\": 206,\n    \"SxNil\": 207,\n    \"SXTbl\": 208,\n    \"SXTBRGIITM\": 209,\n    \"SxTbpg\": 210,\n    \"ObProj\": 211,\n    \"SXStreamID\": 213,\n    \"DBCell\": 215,\n    \"SXRng\": 216,\n    \"SxIsxoper\": 217,\n    \"BookBool\": 218,\n    \"DbOrParamQry\": 220,\n    \"ScenarioProtect\": 221,\n    \"OleObjectSize\": 222,\n    \"XF\": 224,\n    \"InterfaceHdr\": 225,\n    \"InterfaceEnd\": 226,\n    \"SXVS\": 227,\n    \"MergeCells\": 229,\n    \"BkHim\": 233,\n    \"MsoDrawingGroup\": 235,\n    \"MsoDrawing\": 236,\n    \"MsoDrawingSelection\": 237,\n    \"PhoneticInfo\": 239,\n    \"SxRule\": 240,\n    \"SXEx\": 241,\n    \"SxFilt\": 242,\n    \"SxDXF\": 244,\n    \"SxItm\": 245,\n    \"SxName\": 246,\n    \"SxSelect\": 247,\n    \"SXPair\": 248,\n    \"SxFmla\": 249,\n    \"SxFormat\": 251,\n    \"SST\": 252,\n    \"LabelSst\": 253,\n    \"ExtSST\": 255,\n    \"SXVDEx\": 256,\n    \"SXFormula\": 259,\n    \"SXDBEx\": 290,\n    \"RRDInsDel\": 311,\n    \"RRDHead\": 312,\n    \"RRDChgCell\": 315,\n    \"RRTabId\": 317,\n    \"RRDRenSheet\": 318,\n    \"RRSort\": 319,\n    \"RRDMove\": 320,\n    \"RRFormat\": 330,\n    \"RRAutoFmt\": 331,\n    \"RRInsertSh\": 333,\n    \"RRDMoveBegin\": 334,\n    \"RRDMoveEnd\": 335,\n    \"RRDInsDelBegin\": 336,\n    \"RRDInsDelEnd\": 337,\n    \"RRDConflict\": 338,\n    \"RRDDefName\": 339,\n    \"RRDRstEtxp\": 340,\n    \"LRng\": 351,\n    \"UsesELFs\": 352,\n    \"DSF\": 353,\n    \"CUsr\": 401,\n    \"CbUsr\": 402,\n    \"UsrInfo\": 403,\n    \"UsrExcl\": 404,\n    \"FileLock\": 405,\n    \"RRDInfo\": 406,\n    \"BCUsrs\": 407,\n    \"UsrChk\": 408,\n    \"UserBView\": 425,\n    \"UserSViewBegin\": 426,\n    \"UserSViewBegin_Chart\": 426,\n    \"UserSViewEnd\": 427,\n    \"RRDUserView\": 428,\n    \"Qsi\": 429,\n    \"SupBook\": 430,\n    \"Prot4Rev\": 431,\n    \"CondFmt\": 432,\n    \"CF\": 433,\n    \"DVal\": 434,\n    \"DConBin\": 437,\n    \"TxO\": 438,\n    \"RefreshAll\": 439,\n    \"HLink\": 440,\n    \"Lel\": 441,\n    \"CodeName\": 442,\n    \"SXFDBType\": 443,\n    \"Prot4RevPass\": 444,\n    \"ObNoMacros\": 445,\n    \"Dv\": 446,\n    \"Excel9File\": 448,\n    \"RecalcId\": 449,\n    \"EntExU2\": 450,\n    \"Dimensions\": 512,\n    \"Blank\": 513,\n    \"Number\": 515,\n    \"Label\": 516,\n    \"BoolErr\": 517,\n    \"String\": 519,\n    \"Row\": 520,\n    \"Index\": 523,\n    \"Array\": 545,\n    \"DefaultRowHeight\": 549,\n    \"Table\": 566,\n    \"Window2\": 574,\n    \"RK\": 638,\n    \"Style\": 659,\n    \"BigName\": 1048,\n    \"Format\": 1054,\n    \"ContinueBigName\": 1084,\n    \"ShrFmla\": 1212,\n    \"HLinkTooltip\": 2048,\n    \"WebPub\": 2049,\n    \"QsiSXTag\": 2050,\n    \"DBQueryExt\": 2051,\n    \"ExtString\": 2052,\n    \"TxtQry\": 2053,\n    \"Qsir\": 2054,\n    \"Qsif\": 2055,\n    \"RRDTQSIF\": 2056,\n    \"BOF\": 2057,\n    \"OleDbConn\": 2058,\n    \"WOpt\": 2059,\n    \"SXViewEx\": 2060,\n    \"SXTH\": 2061,\n    \"SXPIEx\": 2062,\n    \"SXVDTEx\": 2063,\n    \"SXViewEx9\": 2064,\n    \"ContinueFrt\": 2066,\n    \"RealTimeData\": 2067,\n    \"ChartFrtInfo\": 2128,\n    \"FrtWrapper\": 2129,\n    \"StartBlock\": 2130,\n    \"EndBlock\": 2131,\n    \"StartObject\": 2132,\n    \"EndObject\": 2133,\n    \"CatLab\": 2134,\n    \"YMult\": 2135,\n    \"SXViewLink\": 2136,\n    \"PivotChartBits\": 2137,\n    \"FrtFontList\": 2138,\n    \"SheetExt\": 2146,\n    \"BookExt\": 2147,\n    \"SXAddl\": 2148,\n    \"CrErr\": 2149,\n    \"HFPicture\": 2150,\n    \"FeatHdr\": 2151,\n    \"Feat\": 2152,\n    \"DataLabExt\": 2154,\n    \"DataLabExtContents\": 2155,\n    \"CellWatch\": 2156,\n    \"FeatHdr11\": 2161,\n    \"Feature11\": 2162,\n    \"DropDownObjIds\": 2164,\n    \"ContinueFrt11\": 2165,\n    \"DConn\": 2166,\n    \"List12\": 2167,\n    \"Feature12\": 2168,\n    \"CondFmt12\": 2169,\n    \"CF12\": 2170,\n    \"CFEx\": 2171,\n    \"XFCRC\": 2172,\n    \"XFExt\": 2173,\n    \"AutoFilter12\": 2174,\n    \"ContinueFrt12\": 2175,\n    \"MDTInfo\": 2180,\n    \"MDXStr\": 2181,\n    \"MDXTuple\": 2182,\n    \"MDXSet\": 2183,\n    \"MDXProp\": 2184,\n    \"MDXKPI\": 2185,\n    \"MDB\": 2186,\n    \"PLV\": 2187,\n    \"Compat12\": 2188,\n    \"DXF\": 2189,\n    \"TableStyles\": 2190,\n    \"TableStyle\": 2191,\n    \"TableStyleElement\": 2192,\n    \"StyleExt\": 2194,\n    \"NamePublish\": 2195,\n    \"NameCmt\": 2196,\n    \"SortData\": 2197,\n    \"Theme\": 2198,\n    \"GUIDTypeLib\": 2199,\n    \"FnGrp12\": 2200,\n    \"NameFnGrp12\": 2201,\n    \"MTRSettings\": 2202,\n    \"CompressPictures\": 2203,\n    \"HeaderFooter\": 2204,\n    \"CrtLayout12\": 2205,\n    \"CrtMlFrt\": 2206,\n    \"CrtMlFrtContinue\": 2207,\n    \"ForceFullCalculation\": 2211,\n    \"ShapePropsStream\": 2212,\n    \"TextPropsStream\": 2213,\n    \"RichTextStream\": 2214,\n    \"CrtLayout12A\": 2215,\n    \"Units\": 4097,\n    \"Chart\": 4098,\n    \"Series\": 4099,\n    \"DataFormat\": 4102,\n    \"LineFormat\": 4103,\n    \"MarkerFormat\": 4105,\n    \"AreaFormat\": 4106,\n    \"PieFormat\": 4107,\n    \"AttachedLabel\": 4108,\n    \"SeriesText\": 4109,\n    \"ChartFormat\": 4116,\n    \"Legend\": 4117,\n    \"SeriesList\": 4118,\n    \"Bar\": 4119,\n    \"Line\": 4120,\n    \"Pie\": 4121,\n    \"Area\": 4122,\n    \"Scatter\": 4123,\n    \"CrtLine\": 4124,\n    \"Axis\": 4125,\n    \"Tick\": 4126,\n    \"ValueRange\": 4127,\n    \"CatSerRange\": 4128,\n    \"AxisLine\": 4129,\n    \"CrtLink\": 4130,\n    \"DefaultText\": 4132,\n    \"Text\": 4133,\n    \"FontX\": 4134,\n    \"ObjectLink\": 4135,\n    \"Frame\": 4146,\n    \"Begin\": 4147,\n    \"End\": 4148,\n    \"PlotArea\": 4149,\n    \"Chart3d\": 4154,\n    \"PicF\": 4156,\n    \"DropBar\": 4157,\n    \"Radar\": 4158,\n    \"Surf\": 4159,\n    \"RadarArea\": 4160,\n    \"AxisParent\": 4161,\n    \"LegendException\": 4163,\n    \"ShtProps\": 4164,\n    \"SerToCrt\": 4165,\n    \"AxesUsed\": 4166,\n    \"SBaseRef\": 4168,\n    \"SerParent\": 4170,\n    \"SerAuxTrend\": 4171,\n    \"IFmtRecord\": 4174,\n    \"Pos\": 4175,\n    \"AlRuns\": 4176,\n    \"BRAI\": 4177,\n    \"SerAuxErrBar\": 4187,\n    \"ClrtClient\": 4188,\n    \"SerFmt\": 4189,\n    \"Chart3DBarShape\": 4191,\n    \"Fbi\": 4192,\n    \"BopPop\": 4193,\n    \"AxcExt\": 4194,\n    \"Dat\": 4195,\n    \"PlotGrowth\": 4196,\n    \"SIIndex\": 4197,\n    \"GelFrame\": 4198,\n    \"BopPopCustom\": 4199,\n    \"Fbi2\": 4200,\n}\n\n\ndef _parse_header_RC4(encryptionInfo):\n    # RC4: https://msdn.microsoft.com/en-us/library/dd908560(v=office.12).aspx\n    salt = encryptionInfo.read(16)\n    encryptedVerifier = encryptionInfo.read(16)\n    encryptedVerifierHash = encryptionInfo.read(16)\n    info = {\n        \"salt\": salt,\n        \"encryptedVerifier\": encryptedVerifier,\n        \"encryptedVerifierHash\": encryptedVerifierHash,\n    }\n    return info\n\n\nclass _BIFFStream:\n    def __init__(self, data):\n        self.data = data\n\n    def has_record(self, target):\n        pos = self.data.tell()\n        while True:\n            h = self.data.read(4)\n            if not h:\n                self.data.seek(pos)\n                return False\n            num, size = unpack(\"<HH\", h)\n            if num == target:\n                self.data.seek(pos)\n                return True\n            else:\n                self.data.read(size)\n\n    def skip_to(self, target):\n        while True:\n            h = self.data.read(4)\n            if not h:\n                raise exceptions.ParseError(\"Record not found\")\n            num, size = unpack(\"<HH\", h)\n            if num == target:\n                return num, size\n            else:\n                self.data.read(size)\n\n    def iter_record(self):\n        while True:\n            h = self.data.read(4)\n            if not h:\n                break\n            num, size = unpack(\"<HH\", h)\n            record = io.BytesIO(self.data.read(size))\n            yield num, size, record\n\n\nclass Xls97File(base.BaseOfficeFile):\n    \"\"\"Return a MS-XLS file object.\n\n    Examples:\n        >>> with open(\"tests/inputs/rc4cryptoapi_password.xls\", \"rb\") as f:\n        ...     officefile = Xls97File(f)\n        ...     officefile.load_key(password=\"Password1234_\")\n\n        >>> with open(\"tests/inputs/xor_password_123456789012345.xls\", \"rb\") as f:\n        ...     officefile = Xls97File(f)\n        ...     officefile.load_key(password=\"123456789012345\")\n\n        >>> with open(\"tests/inputs/rc4cryptoapi_password.xls\", \"rb\") as f:\n        ...     officefile = Xls97File(f)\n        ...     officefile.load_key(password=\"0000\")\n        Traceback (most recent call last):\n            ...\n        msoffcrypto.exceptions.InvalidKeyError: ...\n    \"\"\"\n\n    def __init__(self, file):\n        self.file = file\n        ole = olefile.OleFileIO(file)  # do not close this, would close file\n        self.ole = ole\n        self.format = \"xls97\"\n        self.keyTypes = [\"password\"]\n        self.key = None\n        self.salt = None\n\n        workbook = ole.openstream(\"Workbook\")  # closed in destructor\n\n        Data = namedtuple(\"Data\", [\"workbook\"])\n        self.data = Data(\n            workbook=workbook,\n        )\n\n    def __del__(self):\n        \"\"\"Destructor, closes opened stream.\"\"\"\n        if hasattr(self, \"data\") and self.data and self.data.workbook:\n            self.data.workbook.close()\n\n    def load_key(self, password=None):\n        self.data.workbook.seek(0)\n        workbook = _BIFFStream(self.data.workbook)\n\n        # workbook stream consists of records, each of which begins with its ID number.\n        # Record IDs (in decimal) are listed here: https://msdn.microsoft.com/en-us/library/dd945945(v=office.12).aspx\n        # workbook stream's structure is WORKBOOK = BOF WORKBOOKCONTENT and so forth\n        # as in https://msdn.microsoft.com/en-us/library/dd952177(v=office.12).aspx\n        # A record begins with its length (in bytes).\n\n        (num,) = unpack(\"<H\", workbook.data.read(2))\n\n        assert num == 2057  # BOF\n\n        (size,) = unpack(\"<H\", workbook.data.read(2))\n        workbook.data.read(size)  # Skip BOF\n\n        num, size = workbook.skip_to(\n            recordNameNum[\"FilePass\"]\n        )  # Skip to FilePass; TODO: Raise exception if not encrypted\n\n        # FilePass: https://msdn.microsoft.com/en-us/library/dd952596(v=office.12).aspx\n        # If this record exists, the workbook MUST be encrypted.\n        (wEncryptionType,) = unpack(\"<H\", workbook.data.read(2))\n\n        encryptionInfo = io.BytesIO(workbook.data.read(size - 2))\n\n        if wEncryptionType == 0x0000:  # XOR obfuscation\n            key, verificationBytes = unpack(\"<HH\", encryptionInfo.read(4))\n\n            if DocumentXOR.verifypw(password, verificationBytes):\n                self.type = \"xor\"\n                self.key = password\n                self.loc_index = 0\n            else:\n                raise exceptions.InvalidKeyError(\"Failed to verify password\")\n\n        elif wEncryptionType == 0x0001:  # RC4\n            encryptionVersionInfo = encryptionInfo.read(4)\n            vMajor, vMinor = unpack(\"<HH\", encryptionVersionInfo)\n\n            logger.debug(\"Version: {} {}\".format(vMajor, vMinor))\n\n            if vMajor == 0x0001 and vMinor == 0x0001:  # RC4\n                info = _parse_header_RC4(encryptionInfo)\n\n                if DocumentRC4.verifypw(\n                    password,\n                    info[\"salt\"],\n                    info[\"encryptedVerifier\"],\n                    info[\"encryptedVerifierHash\"],\n                ):\n                    self.type = \"rc4\"\n                    self.key = password\n                    self.salt = info[\"salt\"]\n                else:\n                    raise exceptions.InvalidKeyError(\"Failed to verify password\")\n\n            elif (\n                vMajor in [0x0002, 0x0003, 0x0004] and vMinor == 0x0002\n            ):  # RC4 CryptoAPI\n                info = _parse_header_RC4CryptoAPI(encryptionInfo)\n\n                if DocumentRC4CryptoAPI.verifypw(\n                    password,\n                    info[\"salt\"],\n                    info[\"keySize\"],\n                    info[\"encryptedVerifier\"],\n                    info[\"encryptedVerifierHash\"],\n                ):\n                    self.type = \"rc4_cryptoapi\"\n                    self.key = password\n                    self.salt = info[\"salt\"]\n                    self.keySize = info[\"keySize\"]\n                else:\n                    raise exceptions.InvalidKeyError(\"Failed to verify password\")\n\n            else:\n                raise exceptions.DecryptionError(\"Unsupported encryption method\")\n\n    def decrypt(self, outfile):\n        # fd, _outfile_path = tempfile.mkstemp()\n\n        # shutil.copyfile(os.path.realpath(self.file.name), _outfile_path)\n        # outole = olefile.OleFileIO(_outfile_path, write_mode=True)\n\n        # List of encrypted parts: https://msdn.microsoft.com/en-us/library/dd905723(v=office.12).aspx\n\n        # Workbook stream\n        self.data.workbook.seek(0)\n        workbook = _BIFFStream(self.data.workbook)\n\n        plain_buf = []\n        encrypted_buf = io.BytesIO()\n        record_info = []\n\n        for i, (num, size, record) in enumerate(workbook.iter_record()):\n            # Remove encryption, pad by zero to preserve stream size\n            if num == recordNameNum[\"FilePass\"]:\n                plain_buf += [0, 0] + list(pack(\"<H\", size)) + [0] * size\n                encrypted_buf.write(b\"\\x00\" * (4 + size))\n            # The following records MUST NOT be obfuscated or encrypted: BOF (section 2.4.21),\n            # FilePass (section 2.4.117), UsrExcl (section 2.4.339), FileLock (section 2.4.116),\n            # InterfaceHdr (section 2.4.146), RRDInfo (section 2.4.227), and RRDHead (section 2.4.226).\n            elif num in [\n                recordNameNum[\"BOF\"],\n                recordNameNum[\"FilePass\"],\n                recordNameNum[\"UsrExcl\"],\n                recordNameNum[\"FileLock\"],\n                recordNameNum[\"InterfaceHdr\"],\n                recordNameNum[\"RRDInfo\"],\n                recordNameNum[\"RRDHead\"],\n            ]:\n                header = pack(\"<HH\", num, size)\n                plain_buf += list(header) + list(record.read())\n                encrypted_buf.write(b\"\\x00\" * (4 + size))\n            # The lbPlyPos field of the BoundSheet8 record (section 2.4.28) MUST NOT be encrypted.\n            elif num == recordNameNum[\"BoundSheet8\"]:\n                header = pack(\"<HH\", num, size)\n                plain_buf += (\n                    list(header) + list(record.read(4)) + [-2] * (size - 4)\n                )  # Preserve lbPlyPos\n                encrypted_buf.write(b\"\\x00\" * 4 + b\"\\x00\" * 4 + record.read())\n            else:\n                header = pack(\"<HH\", num, size)\n                plain_buf += list(header) + [-1] * size\n                encrypted_buf.write(b\"\\x00\" * 4 + record.read())\n\n        self.data_size = encrypted_buf.tell()\n        encrypted_buf.seek(0)\n\n        if self.type == \"rc4\":\n            dec = DocumentRC4.decrypt(\n                self.key, self.salt, encrypted_buf, blocksize=1024\n            )\n        elif self.type == \"rc4_cryptoapi\":\n            dec = DocumentRC4CryptoAPI.decrypt(\n                self.key, self.salt, self.keySize, encrypted_buf, blocksize=1024\n            )\n        elif self.type == \"xor\":\n            dec = DocumentXOR.decrypt(\n                self.key, encrypted_buf, plain_buf, record_info, 10\n            )\n        else:\n            raise exceptions.DecryptionError(\n                \"Unsupported encryption method: {}\".format(self.type)\n            )\n\n        for c in plain_buf:\n            if c == -1 or c == -2:\n                dec.seek(1, 1)\n            else:\n                dec.write(bytearray([c]))\n\n        dec.seek(0)\n\n        # f = open('Workbook', 'wb')\n        # f.write(dec.read())\n        # dec.seek(0)\n\n        workbook_dec = dec\n\n        with tempfile.TemporaryFile() as _outfile:\n            self.file.seek(0)\n            shutil.copyfileobj(self.file, _outfile)\n            outole = olefile.OleFileIO(_outfile, write_mode=True)\n\n            outole.write_stream(\"Workbook\", workbook_dec.read())\n\n            # _outfile = open(_outfile_path, 'rb')\n\n            _outfile.seek(0)\n\n            shutil.copyfileobj(_outfile, outfile)\n\n        return\n\n    def is_encrypted(self):\n        r\"\"\"\n        Test if the file is encrypted.\n\n            >>> f = open(\"tests/inputs/plain.xls\", \"rb\")\n            >>> file = Xls97File(f)\n            >>> file.is_encrypted()\n            False\n            >>> f = open(\"tests/inputs/rc4cryptoapi_password.xls\", \"rb\")\n            >>> file = Xls97File(f)\n            >>> file.is_encrypted()\n            True\n        \"\"\"\n        # Utilising the method above, check for encryption type.\n        self.data.workbook.seek(0)\n        workbook = _BIFFStream(self.data.workbook)\n\n        (num,) = unpack(\"<H\", workbook.data.read(2))\n        assert num == 2057\n\n        (size,) = unpack(\"<H\", workbook.data.read(2))\n        workbook.data.read(size)\n\n        if not workbook.has_record(recordNameNum[\"FilePass\"]):\n            return False\n\n        num, size = workbook.skip_to(recordNameNum[\"FilePass\"])\n        (wEncryptionType,) = unpack(\"<H\", workbook.data.read(2))\n\n        if wEncryptionType == 0x0001:  # RC4\n            return True\n        elif wEncryptionType == 0x0000:  # XOR obfuscation\n            return True\n        else:\n            return False\n"
  },
  {
    "path": "msoffcrypto/method/__init__.py",
    "content": ""
  },
  {
    "path": "msoffcrypto/method/container/__init__.py",
    "content": ""
  },
  {
    "path": "msoffcrypto/method/container/ecma376_encrypted.py",
    "content": "import io\nfrom datetime import datetime\nfrom struct import pack\n\nimport olefile\n\n# An encrypted ECMA376 file is stored as an OLE container.\n#\n# At this point, creating an Ole file is somewhat of a chore, since\n# the latest OleFile (v0.47) does not really do it.\n#\n# See https://github.com/decalage2/olefile/issues/6\n#\n# This file is not meant to support all manners of OLE files; it creates\n# what we need (an OLE file with an encrypted stream + supporting streams).\n# Nothing more, nothing less. So, unlike OleFile, we can take _a lot_ of\n# shortcuts.\n#\n# Probably very brittle.\n#\n# File format:\n#\n# https://github.com/libyal/libolecf/blob/main/documentation/OLE%20Compound%20File%20format.asciidoc\n#\n# Initial C++ code from https://github.com/herumi/msoffice (BSD-3)\n\n\ndef datetime2filetime(dt):\n    \"\"\"\n    Convert Python datetime.datetime to FILETIME (64 bits unsigned int)\n\n    A file time is a 64-bit value that represents the number of 100-nanosecond intervals that have elapsed\n    since 12:00 A.M. January 1, 1601 Coordinated Universal Time (UTC).\n\n    https://learn.microsoft.com/en-us/windows/win32/sysinfo/file-times\n    \"\"\"\n    _FILETIME_NULL_DATE = datetime(1601, 1, 1, 0, 0, 0)\n    return int((dt - _FILETIME_NULL_DATE).total_seconds() * 10000000)\n\n\nclass RedBlack:\n    RED = 0  # Note that this is per-spec; olefile.py shows the opposite\n    BLACK = 1\n\n\nclass DirectoryEntryType:\n    EMPTY = 0\n    STORAGE = 1\n    STREAM = 2\n    LOCK_BYTES = 3\n    PROPERTY = 4\n    ROOT_STORAGE = 5\n\n\nclass SectorTypes:\n    MAXREGSECT = 0xFFFFFFFA\n    DIFSECT = 0xFFFFFFFC\n    FATSECT = 0xFFFFFFFD\n    ENDOFCHAIN = 0xFFFFFFFE\n    FREESECT = 0xFFFFFFFF\n    NOSTREAM = 0xFFFFFFFF\n\n\nclass DSPos:\n    # Order in the directories array; must be in sync with getDirectoryEntries()\n\n    iRoot = 0\n    iEncryptionPackage = 1\n    iDataSpaces = 2\n    iVersion = 3\n    iDataSpaceMap = 4\n    iDataSpaceInfo = 5\n    iStongEncryptionDataSpace = 6\n    iTransformInfo = 7\n    iStrongEncryptionTransform = 8\n    iPrimary = 9\n    iEncryptionInfo = 10\n    dirNum = 11\n\n\nclass DefaultContent:\n    # Lifted off of Herumi/msoffice (C++ package)\n    # https://github.com/herumi/msoffice/blob/master/include/resource.hpp\n\n    Version = b\"\\x3c\\x00\\x00\\x00\\x4d\\x00\\x69\\x00\\x63\\x00\\x72\\x00\\x6f\\x00\\x73\\x00\\x6f\\x00\\x66\\x00\\x74\\x00\\x2e\\x00\\x43\\x00\\x6f\\x00\\x6e\\x00\\x74\\x00\\x61\\x00\\x69\\x00\\x6e\\x00\\x65\\x00\\x72\\x00\\x2e\\x00\\x44\\x00\\x61\\x00\\x74\\x00\\x61\\x00\\x53\\x00\\x70\\x00\\x61\\x00\\x63\\x00\\x65\\x00\\x73\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00\"\n    Primary = b\"\\x58\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x4c\\x00\\x00\\x00\\x7b\\x00\\x46\\x00\\x46\\x00\\x39\\x00\\x41\\x00\\x33\\x00\\x46\\x00\\x30\\x00\\x33\\x00\\x2d\\x00\\x35\\x00\\x36\\x00\\x45\\x00\\x46\\x00\\x2d\\x00\\x34\\x00\\x36\\x00\\x31\\x00\\x33\\x00\\x2d\\x00\\x42\\x00\\x44\\x00\\x44\\x00\\x35\\x00\\x2d\\x00\\x35\\x00\\x41\\x00\\x34\\x00\\x31\\x00\\x43\\x00\\x31\\x00\\x44\\x00\\x30\\x00\\x37\\x00\\x32\\x00\\x34\\x00\\x36\\x00\\x7d\\x00\\x4e\\x00\\x00\\x00\\x4d\\x00\\x69\\x00\\x63\\x00\\x72\\x00\\x6f\\x00\\x73\\x00\\x6f\\x00\\x66\\x00\\x74\\x00\\x2e\\x00\\x43\\x00\\x6f\\x00\\x6e\\x00\\x74\\x00\\x61\\x00\\x69\\x00\\x6e\\x00\\x65\\x00\\x72\\x00\\x2e\\x00\\x45\\x00\\x6e\\x00\\x63\\x00\\x72\\x00\\x79\\x00\\x70\\x00\\x74\\x00\\x69\\x00\\x6f\\x00\\x6e\\x00\\x54\\x00\\x72\\x00\\x61\\x00\\x6e\\x00\\x73\\x00\\x66\\x00\\x6f\\x00\\x72\\x00\\x6d\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04\\x00\\x00\\x00\"\n    DataSpaceMap = b\"\\x08\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x68\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x20\\x00\\x00\\x00\\x45\\x00\\x6e\\x00\\x63\\x00\\x72\\x00\\x79\\x00\\x70\\x00\\x74\\x00\\x65\\x00\\x64\\x00\\x50\\x00\\x61\\x00\\x63\\x00\\x6b\\x00\\x61\\x00\\x67\\x00\\x65\\x00\\x32\\x00\\x00\\x00\\x53\\x00\\x74\\x00\\x72\\x00\\x6f\\x00\\x6e\\x00\\x67\\x00\\x45\\x00\\x6e\\x00\\x63\\x00\\x72\\x00\\x79\\x00\\x70\\x00\\x74\\x00\\x69\\x00\\x6f\\x00\\x6e\\x00\\x44\\x00\\x61\\x00\\x74\\x00\\x61\\x00\\x53\\x00\\x70\\x00\\x61\\x00\\x63\\x00\\x65\\x00\\x00\\x00\"\n    StrongEncryptionDataSpace = b\"\\x08\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x32\\x00\\x00\\x00\\x53\\x00\\x74\\x00\\x72\\x00\\x6f\\x00\\x6e\\x00\\x67\\x00\\x45\\x00\\x6e\\x00\\x63\\x00\\x72\\x00\\x79\\x00\\x70\\x00\\x74\\x00\\x69\\x00\\x6f\\x00\\x6e\\x00\\x54\\x00\\x72\\x00\\x61\\x00\\x6e\\x00\\x73\\x00\\x66\\x00\\x6f\\x00\\x72\\x00\\x6d\\x00\\x00\\x00\"\n\n\nclass Header:\n    FIRSTNUMDIFAT = 109\n    BUFFER_SIZE = 512  # Size taken when writing out to disk/buffer\n\n    def __init__(self):\n        self.minorVersion = 0x003E\n        self.majorVersion = 3\n        self.sectorShift = 9\n        self.numDirectorySectors = 0\n        self.numFatSectors = 0\n        self.firstDirectorySectorLocation = SectorTypes.ENDOFCHAIN\n        self.transactionSignatureNumber = 0\n        self.firstMiniFatSectorLocation = SectorTypes.ENDOFCHAIN\n        self.numMiniFatSectors = 0\n        self.firstDifatSectorLocation = SectorTypes.ENDOFCHAIN\n        self.numDifatSectors = 0\n        self.sectorSize = 1 << self.sectorShift\n        self.difat = []\n\n    def write_to(self, obuf):\n        obuf.write(olefile.MAGIC)\n        obuf.write(b\"\\0\" * 16)  # CLSID\n\n        byteOrder = 0xFFFE  # Little-Endian\n        miniSectorShift = 6\n        miniStreamCutoffSize = 0x1000\n        reserved = 0\n\n        obuf.write(\n            pack(\n                \"<HHHHHHHHIIIIIIIII\",\n                self.minorVersion,\n                self.majorVersion,\n                byteOrder,\n                self.sectorShift,\n                miniSectorShift,\n                reserved,\n                reserved,\n                reserved,\n                self.numDirectorySectors,\n                self.numFatSectors,\n                self.firstDirectorySectorLocation,\n                self.transactionSignatureNumber,\n                miniStreamCutoffSize,\n                self.firstMiniFatSectorLocation,\n                self.numMiniFatSectors,\n                self.firstDifatSectorLocation,\n                self.numDifatSectors,\n            )\n        )\n\n        difatSize = len(self.difat)\n\n        for i in range(min(difatSize, Header.FIRSTNUMDIFAT)):\n            obuf.write(pack(\"<I\", self.difat[i]))\n\n        for i in range(difatSize, Header.FIRSTNUMDIFAT):\n            obuf.write(pack(\"<I\", SectorTypes.NOSTREAM))\n\n\nclass DirectoryEntry:\n    def __init__(\n        self,\n        name=\"\",\n        _type=DirectoryEntryType.EMPTY,\n        color=RedBlack.RED,\n        leftId=SectorTypes.NOSTREAM,\n        rightId=SectorTypes.NOSTREAM,\n        childId=SectorTypes.NOSTREAM,\n        clsid=\"\",\n        bits=0,\n        ct=0,\n        mt=0,\n        loc=0,\n        content=b\"\",\n    ):\n        self.Name = name\n        self.Type = _type\n        self.Color = color\n        self.LeftSiblingId = leftId\n        self.RightSiblingId = rightId\n        self.ChildId = childId\n        self.CLSID = clsid\n        self.StateBits = bits\n        self.CreationTime = ct\n        self.ModificationTime = mt\n        self.StartingSectorLocation = loc\n        self.Content = content\n\n    def write_header_to(self, obuf):\n        \"\"\"\n        Write 128 bytes header in the output buffer. The Name property needs to be converted to UTF-16; Content is _not_\n        written out by this method.\n        \"\"\"\n        name16 = self.Name.encode(\n            \"UTF-16-LE\"\n        )  # Write in Little Endian; omit the BOM at the start of the output\n        directoryNameSize = len(name16) + 2  # Count the null terminator in the size\n\n        obuf.write(\n            name16 + b\"\\0\" * 2\n        )  # Specs calls for us to store the null-terminator\n        obuf.write(\n            b\"\\0\" * (64 - directoryNameSize)\n        )  # Pad name to 64 bytes (thus max 31 chars + \\x00\\x00)\n        obuf.write(pack(\"<H\", directoryNameSize if directoryNameSize > 2 else 0))\n        obuf.write(\n            pack(\n                \"<BBIII\",\n                self.Type,\n                self.Color,\n                self.LeftSiblingId,\n                self.RightSiblingId,\n                self.ChildId,\n            )\n        )\n\n        if self.CLSID:\n            obuf.write(self.CLSID)\n        else:\n            obuf.write(b\"\\0\" * 16)\n\n        obuf.write(pack(\"<I\", self.StateBits))\n\n        self.write_filetime(obuf, self.CreationTime)\n        self.write_filetime(obuf, self.ModificationTime)\n\n        obuf.write(pack(\"<IQ\", self.StartingSectorLocation, len(self.Content)))\n\n    def write_filetime(self, obuf, ft):\n        # Write the lower 32 bits and upper 32 bits, in this order.\n        obuf.write(pack(\"<II\", ft & 0xFFFFFFFF, ft >> 32))\n\n    @property\n    def Name(self):\n        return self._Name\n\n    @Name.setter\n    def Name(self, n):\n        if len(n) > 31:\n            raise ValueError(\"Name cannot be longer than 31 characters\")\n\n        if set(\"!:/\").intersection(n):\n            raise ValueError(\"Name contains invalid characters (!:/)\")\n\n        self._Name = n\n\n    @property\n    def CLSID(self):\n        return self._CLSID\n\n    @CLSID.setter\n    def CLSID(self, c):\n        if c and len(c) != 16:\n            raise ValueError(\"CLSID must be blank, or 16 characters long\")\n\n        self._CLSID = c\n\n    @property\n    def LeftSiblingId(self):\n        return self._LeftSiblingId\n\n    @LeftSiblingId.setter\n    def LeftSiblingId(self, id):\n        self._valid_id(id)\n        self._LeftSiblingId = id\n\n    @property\n    def RightSiblingId(self):\n        return self._RightSiblingId\n\n    @RightSiblingId.setter\n    def RightSiblingId(self, id):\n        self._valid_id(id)\n        self._RightSiblingId = id\n\n    @property\n    def ChildId(self):\n        return self._ChildId\n\n    @ChildId.setter\n    def ChildId(self, id):\n        self._valid_id(id)\n        self._ChildId = id\n\n    def _valid_id(self, id):\n        if not ((id <= SectorTypes.MAXREGSECT) or (id == SectorTypes.NOSTREAM)):\n            raise ValueError(\"Invalid id received\")\n\n\nclass ECMA376EncryptedLayout:\n    def __init__(self, sectorSize):\n        self.sectorSize = sectorSize\n        self.miniFatNum = 0\n        self.miniFatDataSectorNum = 0\n        self.miniFatSectors = 0\n        self.numMiniFatSectors = 1\n        self.difatSectorNum = 0\n        self.fatSectorNum = 0\n        self.difatPos = 0\n        self.directoryEntrySectorNum = 0\n        self.encryptionPackageSectorNum = 0\n\n    @property\n    def fatPos(self):\n        return self.difatPos + self.difatSectorNum\n\n    @property\n    def miniFatPos(self):\n        return self.fatPos + self.fatSectorNum\n\n    @property\n    def directoryEntryPos(self):\n        return self.miniFatPos + self.numMiniFatSectors\n\n    @property\n    def miniFatDataPos(self):\n        return self.directoryEntryPos + self.directoryEntrySectorNum\n\n    @property\n    def contentSectorNum(self):\n        return (\n            self.numMiniFatSectors\n            + self.directoryEntrySectorNum\n            + self.miniFatDataSectorNum\n            + self.encryptionPackageSectorNum\n        )\n\n    @property\n    def encryptionPackagePos(self):\n        return self.miniFatDataPos + self.miniFatDataSectorNum\n\n    @property\n    def totalSectors(self):\n        return self.difatSectorNum + self.fatSectorNum + self.contentSectorNum\n\n    @property\n    def totalSize(self):\n        return Header.BUFFER_SIZE + self.totalSectors * self.sectorSize\n\n    @property\n    def offsetDirectoryEntries(self):\n        return Header.BUFFER_SIZE + self.directoryEntryPos * self.sectorSize\n\n    @property\n    def offsetMiniFatData(self):\n        return Header.BUFFER_SIZE + self.miniFatDataPos * self.sectorSize\n\n    @property\n    def offsetFat(self):\n        return Header.BUFFER_SIZE + self.fatPos * self.sectorSize\n\n    @property\n    def offsetMiniFat(self):\n        return Header.BUFFER_SIZE + self.miniFatPos * self.sectorSize\n\n    def offsetDifat(self, n):\n        return Header.BUFFER_SIZE + (self.difatPos + n) * self.sectorSize\n\n    def offsetData(self, startingSectorLocation):\n        return Header.BUFFER_SIZE + startingSectorLocation * self.sectorSize\n\n    def offsetMiniData(self, startingSectorLocation):\n        return self.offsetMiniFatData + startingSectorLocation * 64\n\n\nclass ECMA376Encrypted:\n    def __init__(self, encryptedPackage=b\"\", encryptionInfo=b\"\"):\n        self._header = self._get_default_header()\n        self._dirs = self._get_directory_entries()\n\n        self.set_payload(encryptedPackage, encryptionInfo)\n\n    def write_to(self, obuf):\n        \"\"\"\n        Writes the encrypted data to obuf\n        \"\"\"\n\n        # Create a temporary buffer with seek/tell capabilities, we do not want to assume the passed-in buffer has such\n        # capabilities (ie: piping to stdout).\n        _obuf = io.BytesIO()\n\n        self._write_to(_obuf)\n\n        # Finalize and write to client buffer.\n        obuf.write(_obuf.getvalue())\n\n    def set_payload(self, encryptedPackage, encryptionInfo):\n        self._dirs[DSPos.iEncryptionPackage].Content = encryptedPackage\n        self._dirs[DSPos.iEncryptionInfo].Content = encryptionInfo\n\n    def _get_default_header(self):\n        return Header()\n\n    def _get_directory_entries(self):\n        ft = datetime2filetime(datetime.now())\n\n        directories = [  # Must follow DSPos ordering\n            DirectoryEntry(\n                \"Root Entry\",\n                DirectoryEntryType.ROOT_STORAGE,\n                RedBlack.RED,\n                ct=ft,\n                mt=ft,\n                childId=DSPos.iEncryptionInfo,\n            ),\n            DirectoryEntry(\n                \"EncryptedPackage\",\n                DirectoryEntryType.STREAM,\n                RedBlack.RED,\n                ct=ft,\n                mt=ft,\n            ),\n            DirectoryEntry(\n                \"\\x06DataSpaces\",\n                DirectoryEntryType.STORAGE,\n                RedBlack.RED,\n                ct=ft,\n                mt=ft,\n                childId=DSPos.iDataSpaceMap,\n            ),\n            DirectoryEntry(\n                \"Version\",\n                DirectoryEntryType.STREAM,\n                RedBlack.BLACK,\n                ct=ft,\n                mt=ft,\n                content=DefaultContent.Version,\n            ),\n            DirectoryEntry(\n                \"DataSpaceMap\",\n                DirectoryEntryType.STREAM,\n                RedBlack.BLACK,\n                ct=ft,\n                mt=ft,\n                leftId=DSPos.iVersion,\n                rightId=DSPos.iDataSpaceInfo,\n                content=DefaultContent.DataSpaceMap,\n            ),\n            DirectoryEntry(\n                \"DataSpaceInfo\",\n                DirectoryEntryType.STORAGE,\n                RedBlack.BLACK,\n                ct=ft,\n                mt=ft,\n                rightId=DSPos.iTransformInfo,\n                childId=DSPos.iStongEncryptionDataSpace,\n            ),\n            DirectoryEntry(\n                \"StrongEncryptionDataSpace\",\n                DirectoryEntryType.STREAM,\n                RedBlack.BLACK,\n                ct=ft,\n                mt=ft,\n                content=DefaultContent.StrongEncryptionDataSpace,\n            ),\n            DirectoryEntry(\n                \"TransformInfo\",\n                DirectoryEntryType.STORAGE,\n                RedBlack.RED,\n                ct=ft,\n                mt=ft,\n                childId=DSPos.iStrongEncryptionTransform,\n            ),\n            DirectoryEntry(\n                \"StrongEncryptionTransform\",\n                DirectoryEntryType.STORAGE,\n                RedBlack.BLACK,\n                ct=ft,\n                mt=ft,\n                childId=DSPos.iPrimary,\n            ),\n            DirectoryEntry(\n                \"\\x06Primary\",\n                DirectoryEntryType.STREAM,\n                RedBlack.BLACK,\n                ct=ft,\n                mt=ft,\n                content=DefaultContent.Primary,\n            ),\n            DirectoryEntry(\n                \"EncryptionInfo\",\n                DirectoryEntryType.STREAM,\n                RedBlack.BLACK,\n                ct=ft,\n                mt=ft,\n                leftId=DSPos.iDataSpaces,\n                rightId=DSPos.iEncryptionPackage,\n            ),\n        ]\n\n        return directories\n\n    def _write_to(self, obuf):\n        layout = ECMA376EncryptedLayout(self._header.sectorSize)\n\n        self._set_sector_locations_of_streams(layout)\n        self._detect_sector_num(layout)\n\n        self._header.firstDirectorySectorLocation = layout.directoryEntryPos\n        self._header.firstMiniFatSectorLocation = layout.miniFatPos\n        self._header.numMiniFatSectors = layout.numMiniFatSectors\n\n        self._dirs[DSPos.iRoot].StartingSectorLocation = layout.miniFatDataPos\n        self._dirs[DSPos.iRoot].Content = b\"\\0\" * (64 * layout.miniFatNum)\n        self._dirs[\n            DSPos.iEncryptionPackage\n        ].StartingSectorLocation = layout.encryptionPackagePos\n\n        for i in range(min(layout.fatSectorNum, Header.FIRSTNUMDIFAT)):\n            self._header.difat.append(layout.fatPos + i)\n\n        self._header.numFatSectors = layout.fatSectorNum\n        self._header.numDifatSectors = layout.difatSectorNum\n\n        if layout.difatSectorNum > 0:\n            self._header.firstDifatSectorLocation = layout.difatPos\n\n        # Zero out the output buffer; some sections pad, some sections don't ... but we need the buffer to have the proper size\n        # so we can jump around\n        obuf.write(b\"\\0\" * layout.totalSize)\n        obuf.seek(0)\n\n        self._header.write_to(obuf)\n\n        self._write_DIFAT(obuf, layout)\n        self._write_FAT_start(obuf, layout)\n        self._write_MiniFAT(obuf, layout)\n\n        self._write_directory_entries(obuf, layout)\n        self._write_Content(obuf, layout)\n\n    def _write_directory_entries(self, obuf, layout: ECMA376EncryptedLayout):\n        obuf.seek(layout.offsetDirectoryEntries)\n\n        for d in self._dirs:\n            d.write_header_to(obuf)  # This must write 128 bytes, no more, no less.\n\n        if obuf.tell() != (layout.offsetDirectoryEntries + len(self._dirs) * 128):\n            # TODO: Use appropriate custom exception\n            raise Exception(\n                \"Buffer did not advance as expected when writing out directory entries\"\n            )\n\n    def _write_Content(self, obuf, layout: ECMA376EncryptedLayout):\n        for d in self._dirs:\n            size = len(d.Content)\n\n            if size:\n                if size <= 4096:  # Small content goes in the minifat section\n                    obuf.seek(layout.offsetMiniData(d.StartingSectorLocation))\n                    obuf.write(d.Content)\n                else:\n                    obuf.seek(layout.offsetData(d.StartingSectorLocation))\n                    obuf.write(d.Content)\n\n    def _write_FAT_start(self, obuf, layout: ECMA376EncryptedLayout):\n        v = ([SectorTypes.DIFSECT] * layout.difatSectorNum) + (\n            [SectorTypes.FATSECT] * layout.fatSectorNum\n        )\n        v += [\n            layout.numMiniFatSectors,\n            layout.directoryEntrySectorNum,\n            layout.miniFatDataSectorNum,\n            layout.encryptionPackageSectorNum,\n        ]\n\n        obuf.seek(layout.offsetFat)\n        self._write_FAT(obuf, v, layout.fatSectorNum * layout.sectorSize)\n\n    def _write_MiniFAT(self, obuf, layout: ECMA376EncryptedLayout):\n        obuf.seek(layout.offsetMiniFat)\n        self._write_FAT(\n            obuf, layout.miniFatSectors, layout.numMiniFatSectors * layout.sectorSize\n        )\n\n    def _write_FAT(self, obuf, entries, blockSize):\n        v = 0\n\n        startPos = obuf.tell()\n        max_n = blockSize // 4  # 4 bytes per entry with <I\n\n        # TODO: Use appropriate custom exception\n        for e in entries:\n            if e <= SectorTypes.MAXREGSECT:\n                for j in range(1, e):\n                    v += 1\n\n                    if v > max_n:\n                        raise Exception(\"Attempting to write beyond block size\")\n\n                    obuf.write(pack(\"<I\", v))\n\n                if v == max_n:\n                    raise Exception(\"Attempting to write beyond block size\")\n\n                obuf.write(pack(\"<I\", SectorTypes.ENDOFCHAIN))\n            else:\n                if v == max_n:\n                    raise Exception(\"Attempting to write beyond block size\")\n\n                obuf.write(pack(\"<I\", e))\n\n            v += 1\n\n        obuf.write(pack(\"<I\", SectorTypes.FREESECT) * (max_n - v))\n\n        if obuf.tell() - startPos != blockSize:\n            # TODO: Use appropriate custom exception\n            raise Exception(\"_write_FAT() did not completely fill the block space.\")\n\n    def _write_DIFAT(self, obuf, layout: ECMA376EncryptedLayout):\n        if layout.difatSectorNum < 1:\n            return\n\n        v = Header.FIRSTNUMDIFAT + layout.difatSectorNum\n\n        for i in range(layout.difatSectorNum):\n            obuf.seek(layout.offsetDifat(i))\n\n            for j in range(layout.sectorSize // 4 - 1):  # 4 == sizeof(32 bit int)\n                obuf.write(pack(\"<I\", v))\n\n                v += 1\n\n                if v > layout.difatSectorNum + layout.fatSectorNum:\n                    for k in range(j, layout.sectorSize // 4 - 1):\n                        obuf.write(pack(\"<I\", SectorTypes.FREESECT))\n\n                    obuf.write(pack(\"<I\", SectorTypes.ENDOFCHAIN))\n                    return\n\n            # The next seek is _probably_ not needed...\n            obuf.seek(layout.offsetDifat(i) + layout.sectorSize - 4)\n            obuf.write(pack(\"<I\", layout.difatPos + i + 1))\n\n    def _detect_sector_num(self, layout: ECMA376EncryptedLayout):\n        numInFat = layout.sectorSize // 4  # Number of 4-bytes integers\n\n        difatSectorNum = 0\n        fatSectorNum = 0\n\n        for i in range(10):\n            a = self._get_block_num(\n                difatSectorNum + fatSectorNum + layout.contentSectorNum, numInFat\n            )\n            b = (\n                0\n                if a <= Header.FIRSTNUMDIFAT\n                else self._get_block_num(a - Header.FIRSTNUMDIFAT, numInFat - 1)\n            )\n\n            if (b == difatSectorNum) and (a == fatSectorNum):\n                layout.fatSectorNum = fatSectorNum\n                layout.difatSectorNum = difatSectorNum\n\n                return\n\n            difatSectorNum = b\n            fatSectorNum = a\n\n        raise IndexError(\n            \"Unable to detect sector number within a reasonsable amount of loops\"\n        )\n\n    def _set_sector_locations_of_streams(self, layout: ECMA376EncryptedLayout):\n        # Use all streams, except the encrypted package which is special (and the main reason why we're doing all this!)\n        streamsOfInterest = list(\n            filter(\n                lambda d: d.Type == DirectoryEntryType.STREAM\n                and d.Name != \"EncryptedPackage\",\n                self._dirs,\n            )\n        )\n\n        miniFatSectors = []\n        miniFatNum = 0\n        miniFatDataSectorNum = 0\n\n        pos = 0\n        for s in streamsOfInterest:\n            n = self._get_MiniFAT_sector_number(len(s.Content))\n\n            miniFatSectors.append(n)\n\n            s.StartingSectorLocation = pos\n            pos += n\n\n        miniFatNum = pos\n        miniFatDataSectorNum = self._get_block_num(\n            miniFatNum, (self._header.sectorSize // 64)\n        )\n\n        if self._get_block_num(miniFatDataSectorNum, 128) > 1:\n            raise ValueError(\"Unexpected layout size; too large\")\n\n        layout.miniFatNum = miniFatNum\n        layout.miniFatDataSectorNum = miniFatDataSectorNum\n        layout.miniFatSectors = miniFatSectors\n\n        layout.directoryEntrySectorNum = self._get_block_num(len(self._dirs), 4)\n        layout.encryptionPackageSectorNum = self._get_block_num(\n            len(self._dirs[DSPos.iEncryptionPackage].Content), layout.sectorSize\n        )\n\n    def _get_MiniFAT_sector_number(self, size):\n        return self._get_block_num(size, 64)\n\n    def _get_block_num(self, x, block):\n        return (x + block - 1) // block\n"
  },
  {
    "path": "msoffcrypto/method/ecma376_agile.py",
    "content": "from __future__ import annotations\n\nimport base64\nimport functools\nimport hmac\nimport io\nimport logging\nimport secrets\nfrom hashlib import sha1, sha256, sha384, sha512\nfrom struct import pack, unpack\n\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import padding\nfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\n\nfrom msoffcrypto import exceptions\nfrom msoffcrypto.method.container.ecma376_encrypted import ECMA376Encrypted\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\nALGORITHM_HASH = {\n    \"SHA1\": sha1,\n    \"SHA256\": sha256,\n    \"SHA384\": sha384,\n    \"SHA512\": sha512,\n}\n\nblkKey_VerifierHashInput = bytearray([0xFE, 0xA7, 0xD2, 0x76, 0x3B, 0x4B, 0x9E, 0x79])\nblkKey_encryptedVerifierHashValue = bytearray(\n    [0xD7, 0xAA, 0x0F, 0x6D, 0x30, 0x61, 0x34, 0x4E]\n)\nblkKey_encryptedKeyValue = bytearray([0x14, 0x6E, 0x0B, 0xE7, 0xAB, 0xAC, 0xD0, 0xD6])\nblkKey_dataIntegrity1 = bytearray([0x5F, 0xB2, 0xAD, 0x01, 0x0C, 0xB9, 0xE1, 0xF6])\nblkKey_dataIntegrity2 = bytearray([0xA0, 0x67, 0x7F, 0x02, 0xB2, 0x2C, 0x84, 0x33])\n\n\ndef _random_buffer(sz):\n    return secrets.token_bytes(sz)\n\n\ndef _get_num_blocks(sz, block):\n    return (sz + block - 1) // block\n\n\ndef _round_up(sz, block):\n    return _get_num_blocks(sz, block) * block\n\n\ndef _resize_buffer(buf, n, c=b\"\\0\"):\n    if len(buf) >= n:\n        return buf[:n]\n\n    return buf + c * (n - len(buf))\n\n\ndef _normalize_key(key, n):\n    return _resize_buffer(key, n, b\"\\x36\")\n\n\ndef _get_hash_func(algorithm):\n    return ALGORITHM_HASH.get(algorithm, sha1)\n\n\ndef _decrypt_aes_cbc(data, key, iv):\n    aes = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())\n    decryptor = aes.decryptor()\n    decrypted = decryptor.update(data) + decryptor.finalize()\n    return decrypted\n\n\ndef _encrypt_aes_cbc(data, key, iv):\n    aes = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())\n\n    encryptor = aes.encryptor()\n    encrypted = encryptor.update(data) + encryptor.finalize()\n\n    return encrypted\n\n\ndef _encrypt_aes_cbc_padded(data, key, iv, blockSize):\n    buf = data\n\n    if len(buf) % blockSize:\n        buf = _resize_buffer(buf, _round_up(len(buf), blockSize))\n\n    return _encrypt_aes_cbc(buf, key, iv)\n\n\ndef _get_salt(salt_value=None, salt_size=16):\n    if salt_value is not None:\n        if len(salt_value) != salt_size:\n            raise exceptions.EncryptionError(\n                f\"Invalid salt value size, should be {salt_size}\"\n            )\n\n        return salt_value\n\n    return _random_buffer(salt_size)\n\n\n# Hardcoded to AES256 + SHA512 for OOXML.\nclass ECMA376AgileCipherParams:\n    def __init__(self):\n        self.cipherName = \"AES\"\n        self.hashName = \"SHA512\"\n        self.saltSize = 16\n        self.blockSize = 16\n        self.keyBits = 256\n        self.hashSize = 64\n        self.saltValue: bytes | None = None\n\n\ndef _enc64(b):\n    return base64.b64encode(b).decode(\"UTF-8\")\n\n\nclass ECMA376AgileEncryptionInfo:\n    def __init__(self):\n        self.spinCount = 100000\n        self.keyData = ECMA376AgileCipherParams()\n        self.encryptedHmacKey: bytes | None = None\n        self.encryptedHmacValue: bytes | None = None\n\n        self.encryptedKey = ECMA376AgileCipherParams()\n        self.encryptedVerifierHashInput: bytes | None = None\n        self.encryptedVerifierHashValue: bytes | None = None\n        self.encryptedKeyValue: bytes | None = None\n\n    def getEncryptionDescriptorHeader(self):\n        # https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/87020a34-e73f-4139-99bc-bbdf6cf6fa55\n        return pack(\"<HHI\", 4, 4, 0x40)\n\n    def toEncryptionDescriptor(self):\n        \"\"\"\n        Returns an XML description of the encryption information.\n        \"\"\"\n        return f\"\"\"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<encryption xmlns=\"http://schemas.microsoft.com/office/2006/encryption\" xmlns:p=\"http://schemas.microsoft.com/office/2006/keyEncryptor/password\" xmlns:c=\"http://schemas.microsoft.com/office/2006/keyEncryptor/certificate\">\n    <keyData saltSize=\"{self.keyData.saltSize}\" blockSize=\"{self.keyData.blockSize}\" keyBits=\"{self.keyData.keyBits}\" hashSize=\"{self.keyData.hashSize}\"\n             cipherAlgorithm=\"{self.keyData.cipherName}\" cipherChaining=\"ChainingModeCBC\" hashAlgorithm=\"{self.keyData.hashName}\" saltValue=\"{_enc64(self.keyData.saltValue)}\" />\n    <dataIntegrity encryptedHmacKey=\"{_enc64(self.encryptedHmacKey)}\" encryptedHmacValue=\"{_enc64(self.encryptedHmacValue)}\" />\n    <keyEncryptors>\n        <keyEncryptor uri=\"http://schemas.microsoft.com/office/2006/keyEncryptor/password\">\n            <p:encryptedKey spinCount=\"{self.spinCount}\" saltSize=\"{self.encryptedKey.saltSize}\" blockSize=\"{self.encryptedKey.blockSize}\" keyBits=\"{self.encryptedKey.keyBits}\"\n                            hashSize=\"{self.encryptedKey.hashSize}\" cipherAlgorithm=\"{self.encryptedKey.cipherName}\" cipherChaining=\"ChainingModeCBC\" hashAlgorithm=\"{self.encryptedKey.hashName}\"\n                            saltValue=\"{_enc64(self.encryptedKey.saltValue)}\" encryptedVerifierHashInput=\"{_enc64(self.encryptedVerifierHashInput)}\"\n                            encryptedVerifierHashValue=\"{_enc64(self.encryptedVerifierHashValue)}\" encryptedKeyValue=\"{_enc64(self.encryptedKeyValue)}\" />\n        </keyEncryptor>\n    </keyEncryptors>\n</encryption>\n\"\"\"\n\n\ndef _generate_iv(params: ECMA376AgileCipherParams, blkKey, salt_value):\n    if not blkKey:\n        return _normalize_key(salt_value, params.blockSize)\n\n    hashCalc = _get_hash_func(params.hashName)\n\n    return _normalize_key(hashCalc(salt_value + blkKey).digest(), params.blockSize)\n\n\nclass ECMA376Agile:\n    def __init__(self):\n        pass\n\n    @staticmethod\n    def _derive_iterated_hash_from_password(\n        password, saltValue, hashAlgorithm, spinValue\n    ):\n        r\"\"\"\n        Do a partial password-based hash derivation.\n        Note the block key is not taken into consideration in this function.\n        \"\"\"\n        # TODO: This function is quite expensive and it should only be called once.\n        # We need to save the result for later use.\n        # This is not covered by the specification, but MS Word does so.\n\n        hashCalc = _get_hash_func(hashAlgorithm)\n\n        # NOTE: Initial round sha512(salt + password)\n        h = hashCalc(saltValue + password.encode(\"UTF-16LE\"))\n\n        # NOTE: Iteration of 0 -> spincount-1; hash = sha512(iterator + hash)\n        for i in range(0, spinValue, 1):\n            h = hashCalc(pack(\"<I\", i) + h.digest())\n\n        return h\n\n    @staticmethod\n    def _derive_encryption_key(h, blockKey, hashAlgorithm, keyBits):\n        r\"\"\"\n        Finish the password-based key derivation by hashing last hash + blockKey.\n        \"\"\"\n        hashCalc = _get_hash_func(hashAlgorithm)\n        h_final = hashCalc(h + blockKey)\n\n        # NOTE: Needed to truncate encryption key to bitsize\n        encryption_key = h_final.digest()[: keyBits // 8]\n\n        return encryption_key\n\n    @staticmethod\n    def decrypt(key, keyDataSalt, hashAlgorithm, ibuf):\n        r\"\"\"\n        Return decrypted data.\n\n            >>> key = b'@ f\\t\\xd9\\xfa\\xad\\xf2K\\x07j\\xeb\\xf2\\xc45\\xb7B\\x92\\xc8\\xb8\\xa7\\xaa\\x81\\xbcg\\x9b\\xe8\\x97\\x11\\xb0*\\xc2'\n            >>> keyDataSalt = b'\\x8f\\xc7x\"+P\\x8d\\xdcL\\xe6\\x8c\\xdd\\x15<\\x16\\xb4'\n            >>> hashAlgorithm = 'SHA512'\n        \"\"\"\n        # NOTE: See https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/9e61da63-8ddb-4c0a-b25d-f85d990f44c8\n        SEGMENT_LENGTH = 4096\n        hashCalc = _get_hash_func(hashAlgorithm)\n\n        obuf = io.BytesIO()\n\n        # NOTE: See https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/b60c8b35-2db2-4409-8710-59d88a793f83\n        ibuf.seek(0)\n        totalSize = unpack(\"<Q\", ibuf.read(8))\n        totalSize = totalSize[0]\n        logger.debug(\"totalSize: {}\".format(totalSize))\n        remaining = totalSize\n        for i, buf in enumerate(\n            iter(functools.partial(ibuf.read, SEGMENT_LENGTH), b\"\")\n        ):\n            saltWithBlockKey = keyDataSalt + pack(\"<I\", i)\n            iv = hashCalc(saltWithBlockKey).digest()\n            iv = iv[:16]\n            aes = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())\n            decryptor = aes.decryptor()\n            dec = decryptor.update(buf) + decryptor.finalize()\n            # TODO: Check\n            if remaining < len(dec):\n                dec = dec[:remaining]\n            obuf.write(dec)\n            remaining -= len(dec)\n            # TODO: Check if this is needed\n            if remaining <= 0:\n                break\n        return obuf.getvalue()  # return obuf.getbuffer()\n\n    @staticmethod\n    def encrypt(key, ibuf, salt_value=None, spin_count=100000):\n        \"\"\"\n        Return an OLE compound file buffer (complete with headers) which contains ibuf encrypted into a single stream.\n\n        When salt_value is not specified (the default), we generate a random one.\n        \"\"\"\n\n        # Encryption ported from C++ (https://github.com/herumi/msoffice, BSD-3)\n\n        info, secret_key = ECMA376Agile.generate_encryption_parameters(\n            key, salt_value, spin_count\n        )\n        encrypted_data = ECMA376Agile.encrypt_payload(\n            ibuf, info.encryptedKey, secret_key, info.keyData.saltValue\n        )\n        encryption_info = ECMA376Agile.get_encryption_information(\n            info, encrypted_data, secret_key\n        )\n\n        obuf = io.BytesIO()\n        ECMA376Encrypted(encrypted_data, encryption_info).write_to(obuf)\n\n        return obuf.getvalue()\n\n    @staticmethod\n    def get_encryption_information(\n        info: ECMA376AgileEncryptionInfo, encrypted_data, secretKey\n    ):\n        \"\"\"\n        Return the content of an EncryptionInfo Stream, including the short header, per the specifications at\n\n        https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/87020a34-e73f-4139-99bc-bbdf6cf6fa55\n        \"\"\"\n        hmacKey, hmacValue = ECMA376Agile.generate_integrity_parameter(\n            encrypted_data, info.keyData, secretKey, info.keyData.saltValue\n        )\n\n        info.encryptedHmacKey = hmacKey\n        info.encryptedHmacValue = hmacValue\n\n        xml_descriptor = info.toEncryptionDescriptor().encode(\"UTF-8\")\n        header_descriptor = info.getEncryptionDescriptorHeader()\n\n        return header_descriptor + xml_descriptor\n\n    @staticmethod\n    def generate_encryption_parameters(key, salt_value=None, spin_count=100000):\n        \"\"\"\n        Generates encryption parameters used to encrypt a payload.\n\n        Returns the information + a secret key.\n        \"\"\"\n        info = ECMA376AgileEncryptionInfo()\n        info.spinCount = spin_count\n\n        info.encryptedKey.saltValue = _get_salt(salt_value, info.encryptedKey.saltSize)\n\n        h = ECMA376Agile._derive_iterated_hash_from_password(\n            key, info.encryptedKey.saltValue, info.encryptedKey.hashName, info.spinCount\n        ).digest()\n\n        key1 = ECMA376Agile._derive_encryption_key(\n            h,\n            blkKey_VerifierHashInput,\n            info.encryptedKey.hashName,\n            info.encryptedKey.keyBits,\n        )\n        key2 = ECMA376Agile._derive_encryption_key(\n            h,\n            blkKey_encryptedVerifierHashValue,\n            info.encryptedKey.hashName,\n            info.encryptedKey.keyBits,\n        )\n        key3 = ECMA376Agile._derive_encryption_key(\n            h,\n            blkKey_encryptedKeyValue,\n            info.encryptedKey.hashName,\n            info.encryptedKey.keyBits,\n        )\n\n        verifierHashInput = _random_buffer(info.encryptedKey.saltSize)\n        verifierHashInput = _resize_buffer(\n            verifierHashInput,\n            _round_up(len(verifierHashInput), info.encryptedKey.blockSize),\n        )\n\n        info.encryptedVerifierHashInput = _encrypt_aes_cbc(\n            verifierHashInput, key1, info.encryptedKey.saltValue\n        )\n\n        hashedVerifier = _get_hash_func(info.encryptedKey.hashName)(\n            verifierHashInput\n        ).digest()\n        hashedVerifier = _resize_buffer(\n            hashedVerifier, _round_up(len(hashedVerifier), info.encryptedKey.blockSize)\n        )\n\n        info.encryptedVerifierHashValue = _encrypt_aes_cbc(\n            hashedVerifier, key2, info.encryptedKey.saltValue\n        )\n\n        secret_key = _random_buffer(info.encryptedKey.saltSize)\n        secret_key = _normalize_key(secret_key, info.encryptedKey.keyBits // 8)\n\n        info.encryptedKeyValue = _encrypt_aes_cbc(\n            secret_key, key3, info.encryptedKey.saltValue\n        )\n\n        info.keyData.saltValue = _get_salt(salt_size=info.keyData.saltSize)\n\n        return info, secret_key\n\n    @staticmethod\n    def encrypt_payload(ibuf, params: ECMA376AgileCipherParams, secret_key, salt_value):\n        \"\"\"\n        Encrypts a payload using the params and secrets passed in.\n\n        Returns the encrypted data as a byte array.\n        \"\"\"\n        # Specifications calls for storing the original (unpadded) size as a 64 bit little-endian\n        # number at the start of the buffer. We'll loop while there's data, and come back at the\n        # end to update the total size, instead of seeking to the end of ibuf to get the size,\n        # just in case ibuf is a streaming buffer...\n        total_size = 0\n        obuf = io.BytesIO()\n        obuf.write(pack(\"<Q\", total_size))\n\n        hashCalc = _get_hash_func(params.hashName)\n        SEGMENT_LENGTH = 4096\n\n        i = 0\n        while True:\n            buf = ibuf.read(SEGMENT_LENGTH)\n            if not buf:\n                break\n\n            iv = _normalize_key(\n                hashCalc(salt_value + pack(\"<I\", i)).digest(), params.saltSize\n            )\n\n            # Per the specifications, we need to make sure the last chunk is padded to our\n            # block size\n            enc = _encrypt_aes_cbc_padded(buf, secret_key, iv, params.blockSize)\n\n            obuf.write(enc)\n            total_size += len(buf)\n\n            i += 1\n\n        # Update size in the header\n        obuf.seek(0)\n        obuf.write(pack(\"<Q\", total_size))\n\n        return obuf.getvalue()\n\n    @staticmethod\n    def generate_integrity_parameter(\n        encrypted_data, params: ECMA376AgileCipherParams, secret_key, salt_value\n    ):\n        \"\"\"\n        Returns the encrypted HmacKey and HmacValue.\n        \"\"\"\n        salt = _random_buffer(params.hashSize)\n\n        iv1 = _generate_iv(params, blkKey_dataIntegrity1, salt_value)\n        iv2 = _generate_iv(params, blkKey_dataIntegrity2, salt_value)\n\n        encryptedHmacKey = _encrypt_aes_cbc(salt, secret_key, iv1)\n\n        msg_hmac = hmac.new(salt, encrypted_data, _get_hash_func(params.hashName))\n        hmacValue = msg_hmac.digest()\n\n        encryptedHmacValue = _encrypt_aes_cbc(hmacValue, secret_key, iv2)\n\n        return encryptedHmacKey, encryptedHmacValue\n\n    @staticmethod\n    def verify_password(\n        password,\n        saltValue,\n        hashAlgorithm,\n        encryptedVerifierHashInput,\n        encryptedVerifierHashValue,\n        spinValue,\n        keyBits,\n    ):\n        r\"\"\"\n        Return True if the given password is valid.\n\n            >>> password = 'Password1234_'\n            >>> saltValue = b'\\xcb\\xca\\x1c\\x99\\x93C\\xfb\\xad\\x92\\x07V4\\x15\\x004\\xb0'\n            >>> hashAlgorithm = 'SHA512'\n            >>> encryptedVerifierHashInput = b'9\\xee\\xa5N&\\xe5\\x14y\\x8c(K\\xc7qM8\\xac'\n            >>> encryptedVerifierHashValue = b'\\x147mm\\x81s4\\xe6\\xb0\\xffO\\xd8\"\\x1a|g\\x8e]\\x8axN\\x8f\\x99\\x9fL\\x18\\x890\\xc3jK)\\xc5\\xb33`' + \\\n            ... b'[\\\\\\xd4\\x03\\xb0P\\x03\\xad\\xcf\\x18\\xcc\\xa8\\xcb\\xab\\x8d\\xeb\\xe3s\\xc6V\\x04\\xa0\\xbe\\xcf\\xae\\\\\\n\\xd0'\n            >>> spinValue = 100000\n            >>> keyBits = 256\n            >>> ECMA376Agile.verify_password(password, saltValue, hashAlgorithm, encryptedVerifierHashInput, encryptedVerifierHashValue, spinValue, keyBits)\n            True\n        \"\"\"\n        # NOTE: See https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/a57cb947-554f-4e5e-b150-3f2978225e92\n\n        h = ECMA376Agile._derive_iterated_hash_from_password(\n            password, saltValue, hashAlgorithm, spinValue\n        )\n\n        key1 = ECMA376Agile._derive_encryption_key(\n            h.digest(), blkKey_VerifierHashInput, hashAlgorithm, keyBits\n        )\n        key2 = ECMA376Agile._derive_encryption_key(\n            h.digest(), blkKey_encryptedVerifierHashValue, hashAlgorithm, keyBits\n        )\n\n        hash_input = _decrypt_aes_cbc(encryptedVerifierHashInput, key1, saltValue)\n        hashCalc = _get_hash_func(hashAlgorithm)\n        acutal_hash = hashCalc(hash_input)\n        acutal_hash = acutal_hash.digest()\n\n        expected_hash = _decrypt_aes_cbc(encryptedVerifierHashValue, key2, saltValue)\n\n        return acutal_hash == expected_hash\n\n    @staticmethod\n    def verify_integrity(\n        secretKey,\n        keyDataSalt,\n        keyDataHashAlgorithm,\n        keyDataBlockSize,\n        encryptedHmacKey,\n        encryptedHmacValue,\n        stream,\n    ):\n        r\"\"\"\n        Return True if the HMAC of the data payload is valid.\n        \"\"\"\n        # NOTE: See https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/63d9c262-82b9-4fa3-a06d-d087b93e3b00\n\n        hashCalc = _get_hash_func(keyDataHashAlgorithm)\n\n        iv1 = hashCalc(keyDataSalt + blkKey_dataIntegrity1).digest()\n        iv1 = iv1[:keyDataBlockSize]\n        iv2 = hashCalc(keyDataSalt + blkKey_dataIntegrity2).digest()\n        iv2 = iv2[:keyDataBlockSize]\n\n        hmacKey = _decrypt_aes_cbc(encryptedHmacKey, secretKey, iv1)\n        hmacValue = _decrypt_aes_cbc(encryptedHmacValue, secretKey, iv2)\n\n        msg_hmac = hmac.new(hmacKey, stream.read(), hashCalc)\n        actualHmac = msg_hmac.digest()\n        stream.seek(0)\n\n        return hmacValue == actualHmac\n\n    @staticmethod\n    def makekey_from_privkey(privkey, encryptedKeyValue):\n        privkey = serialization.load_pem_private_key(\n            privkey.read(), password=None, backend=default_backend()\n        )\n        skey = privkey.decrypt(encryptedKeyValue, padding.PKCS1v15())\n        return skey\n\n    @staticmethod\n    def makekey_from_password(\n        password, saltValue, hashAlgorithm, encryptedKeyValue, spinValue, keyBits\n    ):\n        r\"\"\"\n        Generate intermediate key from given password.\n\n            >>> password = 'Password1234_'\n            >>> saltValue = b'Lr]E\\xdca\\x0f\\x93\\x94\\x12\\xa0M\\xa7\\x91\\x04f'\n            >>> hashAlgorithm = 'SHA512'\n            >>> encryptedKeyValue = b\"\\xa1l\\xd5\\x16Zz\\xb9\\xd2q\\x11>\\xd3\\x86\\xa7\\x8c\\xf4\\x96\\x92\\xe8\\xe5'\\xb0\\xc5\\xfc\\x00U\\xed\\x08\\x0b|\\xb9K\"\n            >>> spinValue = 100000\n            >>> keyBits = 256\n            >>> expected = b'@ f\\t\\xd9\\xfa\\xad\\xf2K\\x07j\\xeb\\xf2\\xc45\\xb7B\\x92\\xc8\\xb8\\xa7\\xaa\\x81\\xbcg\\x9b\\xe8\\x97\\x11\\xb0*\\xc2'\n            >>> ECMA376Agile.makekey_from_password(password, saltValue, hashAlgorithm, encryptedKeyValue, spinValue, keyBits) == expected\n            True\n        \"\"\"\n\n        h = ECMA376Agile._derive_iterated_hash_from_password(\n            password, saltValue, hashAlgorithm, spinValue\n        )\n        encryption_key = ECMA376Agile._derive_encryption_key(\n            h.digest(), blkKey_encryptedKeyValue, hashAlgorithm, keyBits\n        )\n\n        skey = _decrypt_aes_cbc(encryptedKeyValue, encryption_key, saltValue)\n\n        return skey\n"
  },
  {
    "path": "msoffcrypto/method/ecma376_extensible.py",
    "content": "class ECMA376Extensible:\n    def __init__(self):\n        pass\n"
  },
  {
    "path": "msoffcrypto/method/ecma376_standard.py",
    "content": "import io\nimport logging\nfrom hashlib import sha1\nfrom struct import pack, unpack\n\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\n\nclass ECMA376Standard:\n    def __init__(self):\n        pass\n\n    @staticmethod\n    def decrypt(key, ibuf):\n        r\"\"\"\n        Return decrypted data.\n\n        \"\"\"\n        obuf = io.BytesIO()\n        totalSize = unpack(\"<I\", ibuf.read(4))[0]\n        logger.debug(\"totalSize: {}\".format(totalSize))\n        ibuf.seek(8)\n        aes = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())\n        decryptor = aes.decryptor()\n        x = ibuf.read()\n        dec = decryptor.update(x) + decryptor.finalize()\n        obuf.write(dec[:totalSize])\n        return obuf.getvalue()  # return obuf.getbuffer()\n\n    @staticmethod\n    def verifykey(key, encryptedVerifier, encryptedVerifierHash):\n        r\"\"\"\n        Return True if the given intermediate key is valid.\n\n            >>> key = b'@\\xb1:q\\xf9\\x0b\\x96n7T\\x08\\xf2\\xd1\\x81\\xa1\\xaa'\n            >>> encryptedVerifier = b'Qos.\\x96o\\xac\\x17\\xb1\\xc5\\xd7\\xd8\\xcc6\\xc9('\n            >>> encryptedVerifierHash = b'+ah\\xda\\xbe)\\x11\\xad+\\xd3|\\x17Ft\\\\\\x14\\xd3\\xcf\\x1b\\xb1@\\xa4\\x8fNo=#\\x88\\x08r\\xb1j'\n            >>> ECMA376Standard.verifykey(key, encryptedVerifier, encryptedVerifierHash)\n            True\n        \"\"\"\n        # TODO: For consistency with Agile, rename method to verify_password or the like\n        logger.debug([key, encryptedVerifier, encryptedVerifierHash])\n        # https://msdn.microsoft.com/en-us/library/dd926426(v=office.12).aspx\n        aes = Cipher(algorithms.AES(key), modes.ECB(), backend=default_backend())\n        decryptor = aes.decryptor()\n        verifier = decryptor.update(encryptedVerifier)\n        expected_hash = sha1(verifier).digest()\n        decryptor = aes.decryptor()\n        verifierHash = decryptor.update(encryptedVerifierHash)[: sha1().digest_size]\n        return expected_hash == verifierHash\n\n    @staticmethod\n    def makekey_from_password(\n        password, algId, algIdHash, providerType, keySize, saltSize, salt\n    ):\n        r\"\"\"\n        Generate intermediate key from given password.\n\n            >>> password = 'Password1234_'\n            >>> algId = 0x660e\n            >>> algIdHash = 0x8004\n            >>> providerType = 0x18\n            >>> keySize = 128\n            >>> saltSize = 16\n            >>> salt = b'\\xe8\\x82fI\\x0c[\\xd1\\xee\\xbd+C\\x94\\xe3\\xf80\\xef'\n            >>> expected = b'@\\xb1:q\\xf9\\x0b\\x96n7T\\x08\\xf2\\xd1\\x81\\xa1\\xaa'\n            >>> ECMA376Standard.makekey_from_password(password, algId, algIdHash, providerType, keySize, saltSize, salt) == expected\n            True\n        \"\"\"\n        logger.debug(\n            [\n                password,\n                hex(algId),\n                hex(algIdHash),\n                hex(providerType),\n                keySize,\n                saltSize,\n                salt,\n            ]\n        )\n        xor_bytes = lambda a, b: bytearray(\n            [p ^ q for p, q in zip(bytearray(a), bytearray(b))]\n        )  # bytearray() for Python 2 compat.\n\n        # https://msdn.microsoft.com/en-us/library/dd925430(v=office.12).aspx\n        ITER_COUNT = 50000\n\n        password = password.encode(\"UTF-16LE\")\n        h = sha1(salt + password).digest()\n        for i in range(ITER_COUNT):\n            ibytes = pack(\"<I\", i)\n            h = sha1(ibytes + h).digest()\n        block = 0\n        blockbytes = pack(\"<I\", block)\n        hfinal = sha1(h + blockbytes).digest()\n        cbRequiredKeyLength = keySize // 8\n        cbHash = sha1().digest_size\n        buf1 = b\"\\x36\" * 64\n        buf1 = xor_bytes(hfinal, buf1[:cbHash]) + buf1[cbHash:]\n        x1 = sha1(buf1).digest()\n        buf2 = b\"\\x5c\" * 64\n        buf2 = xor_bytes(hfinal, buf2[:cbHash]) + buf2[cbHash:]\n        x2 = sha1(buf2).digest()  # In spec but unused\n        x3 = x1 + x2\n        keyDerived = x3[:cbRequiredKeyLength]\n        logger.debug(keyDerived)\n        return keyDerived\n"
  },
  {
    "path": "msoffcrypto/method/rc4.py",
    "content": "import functools\nimport io\nimport logging\nfrom hashlib import md5\nfrom struct import pack\n\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.ciphers import Cipher\n\ntry:\n    # NOTE: Avoid DeprecationWarning since cryptography>=43.0\n    # TODO: .algorithm differs from the official documentation\n    from cryptography.hazmat.decrepit.ciphers.algorithms import ARC4\nexcept ImportError:\n    from cryptography.hazmat.primitives.ciphers.algorithms import ARC4\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\n\ndef _makekey(password, salt, block):\n    r\"\"\"\n    Return a intermediate key.\n\n        >>> password = 'password1'\n        >>> salt = b'\\xe8w,\\x1d\\x91\\xc5j7\\x96Ga\\xb2\\x80\\x182\\x17'\n        >>> block = 0\n        >>> expected = b' \\xbf2\\xdd\\xf5@\\x85\\x8cQ7D\\xaf\\x0f$\\xe0<'\n        >>> _makekey(password, salt, block) == expected\n        True\n    \"\"\"\n    # https://msdn.microsoft.com/en-us/library/dd920360(v=office.12).aspx\n    password = password.encode(\"UTF-16LE\")\n    h0 = md5(password).digest()\n    truncatedHash = h0[:5]\n    intermediateBuffer = (truncatedHash + salt) * 16\n    h1 = md5(intermediateBuffer).digest()\n    truncatedHash = h1[:5]\n    blockbytes = pack(\"<I\", block)\n    hfinal = md5(truncatedHash + blockbytes).digest()\n    key = hfinal[: 128 // 8]\n    return key\n\n\nclass DocumentRC4:\n    def __init__(self):\n        pass\n\n    @staticmethod\n    def verifypw(password, salt, encryptedVerifier, encryptedVerifierHash):\n        r\"\"\"\n        Return True if the given password is valid.\n\n            >>> password = 'password1'\n            >>> salt = b'\\xe8w,\\x1d\\x91\\xc5j7\\x96Ga\\xb2\\x80\\x182\\x17'\n            >>> encryptedVerifier = b'\\xc9\\xe9\\x97\\xd4T\\x97=1\\x0b\\xb1\\xbap\\x14&\\x83~'\n            >>> encryptedVerifierHash = b'\\xb1\\xde\\x17\\x8f\\x07\\xe9\\x89\\xc4M\\xae^L\\xf9j\\xc4\\x07'\n            >>> DocumentRC4.verifypw(password, salt, encryptedVerifier, encryptedVerifierHash)\n            True\n        \"\"\"\n        # https://msdn.microsoft.com/en-us/library/dd952648(v=office.12).aspx\n        block = 0\n        key = _makekey(password, salt, block)\n        cipher = Cipher(ARC4(key), mode=None, backend=default_backend())\n        decryptor = cipher.decryptor()\n        verifier = decryptor.update(encryptedVerifier)\n        verfiferHash = decryptor.update(encryptedVerifierHash)\n        hash = md5(verifier).digest()\n        logging.debug([verfiferHash, hash])\n        return hash == verfiferHash\n\n    @staticmethod\n    def decrypt(password, salt, ibuf, blocksize=0x200):\n        r\"\"\"\n        Return decrypted data.\n        \"\"\"\n        obuf = io.BytesIO()\n\n        block = 0\n        key = _makekey(password, salt, block)\n\n        for c, buf in enumerate(iter(functools.partial(ibuf.read, blocksize), b\"\")):\n            cipher = Cipher(ARC4(key), mode=None, backend=default_backend())\n            decryptor = cipher.decryptor()\n\n            dec = decryptor.update(buf) + decryptor.finalize()\n            obuf.write(dec)\n\n            # From wvDecrypt:\n            # at this stage we need to rekey the rc4 algorithm\n            # Dieter Spaar <spaar@mirider.augusta.de> figured out\n            # this rekeying, big kudos to him\n            block += 1\n            key = _makekey(password, salt, block)\n\n        obuf.seek(0)\n        return obuf\n"
  },
  {
    "path": "msoffcrypto/method/rc4_cryptoapi.py",
    "content": "import functools\nimport io\nimport logging\nfrom hashlib import sha1\nfrom struct import pack\n\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.ciphers import Cipher\n\ntry:\n    # NOTE: Avoid DeprecationWarning since cryptography>=43.0\n    # TODO: .algorithm differs from the official documentation\n    from cryptography.hazmat.decrepit.ciphers.algorithms import ARC4\nexcept ImportError:\n    from cryptography.hazmat.primitives.ciphers.algorithms import ARC4\n\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\n\ndef _makekey(password, salt, keyLength, block, algIdHash=0x00008004):\n    r\"\"\"\n    Return a intermediate key.\n    \"\"\"\n    # https://msdn.microsoft.com/en-us/library/dd920677(v=office.12).aspx\n    password = password.encode(\"UTF-16LE\")\n    h0 = sha1(salt + password).digest()\n    blockbytes = pack(\"<I\", block)\n    hfinal = sha1(h0 + blockbytes).digest()\n    if keyLength == 40:\n        key = hfinal[:5] + b\"\\x00\" * 11\n    else:\n        key = hfinal[: keyLength // 8]\n    return key\n\n\nclass DocumentRC4CryptoAPI:\n    def __init__(self):\n        pass\n\n    @staticmethod\n    def verifypw(\n        password,\n        salt,\n        keySize,\n        encryptedVerifier,\n        encryptedVerifierHash,\n        algId=0x00006801,\n        block=0,\n    ):\n        r\"\"\"\n        Return True if the given password is valid.\n        \"\"\"\n        # TODO: For consistency with others, rename method to verify_password or the like\n        # https://msdn.microsoft.com/en-us/library/dd953617(v=office.12).aspx\n        key = _makekey(password, salt, keySize, block)\n        cipher = Cipher(ARC4(key), mode=None, backend=default_backend())\n        decryptor = cipher.decryptor()\n        verifier = decryptor.update(encryptedVerifier)\n        verfiferHash = decryptor.update(encryptedVerifierHash)\n        hash = sha1(verifier).digest()\n        logging.debug([verfiferHash, hash])\n        return hash == verfiferHash\n\n    @staticmethod\n    def decrypt(password, salt, keySize, ibuf, blocksize=0x200, block=0):\n        r\"\"\"\n        Return decrypted data.\n        \"\"\"\n        obuf = io.BytesIO()\n\n        key = _makekey(password, salt, keySize, block)\n\n        for c, buf in enumerate(iter(functools.partial(ibuf.read, blocksize), b\"\")):\n            cipher = Cipher(ARC4(key), mode=None, backend=default_backend())\n            decryptor = cipher.decryptor()\n\n            dec = decryptor.update(buf) + decryptor.finalize()\n            obuf.write(dec)\n\n            # From wvDecrypt:\n            # at this stage we need to rekey the rc4 algorithm\n            # Dieter Spaar <spaar@mirider.augusta.de> figured out\n            # this rekeying, big kudos to him\n            block += 1\n            key = _makekey(password, salt, keySize, block)\n\n        obuf.seek(0)\n        return obuf\n"
  },
  {
    "path": "msoffcrypto/method/xor_obfuscation.py",
    "content": "import io\nimport logging\nfrom hashlib import md5\nfrom struct import pack\n\nlogger = logging.getLogger(__name__)\nlogger.addHandler(logging.NullHandler())\n\n\ndef _makekey(password, salt, block):\n    r\"\"\"\n    Return a intermediate key.\n\n        >>> password = 'password1'\n        >>> salt = b'\\xe8w,\\x1d\\x91\\xc5j7\\x96Ga\\xb2\\x80\\x182\\x17'\n        >>> block = 0\n        >>> expected = b' \\xbf2\\xdd\\xf5@\\x85\\x8cQ7D\\xaf\\x0f$\\xe0<'\n        >>> _makekey(password, salt, block) == expected\n        True\n    \"\"\"\n    # https://msdn.microsoft.com/en-us/library/dd920360(v=office.12).aspx\n    password = password.encode(\"UTF-16LE\")\n    h0 = md5(password).digest()\n    truncatedHash = h0[:5]\n    intermediateBuffer = (truncatedHash + salt) * 16\n    h1 = md5(intermediateBuffer).digest()\n    truncatedHash = h1[:5]\n    blockbytes = pack(\"<I\", block)\n    hfinal = md5(truncatedHash + blockbytes).digest()\n    key = hfinal[: 128 // 8]\n    return key\n\n\nclass DocumentXOR:\n    def __init__(self):\n        pass\n\n    pad_array = [\n        0xBB,\n        0xFF,\n        0xFF,\n        0xBA,\n        0xFF,\n        0xFF,\n        0xB9,\n        0x80,\n        0x00,\n        0xBE,\n        0x0F,\n        0x00,\n        0xBF,\n        0x0F,\n        0x00,\n    ]\n    initial_code = [\n        0xE1F0,\n        0x1D0F,\n        0xCC9C,\n        0x84C0,\n        0x110C,\n        0x0E10,\n        0xF1CE,\n        0x313E,\n        0x1872,\n        0xE139,\n        0xD40F,\n        0x84F9,\n        0x280C,\n        0xA96A,\n        0x4EC3,\n    ]\n\n    xor_matrix = [\n        0xAEFC,\n        0x4DD9,\n        0x9BB2,\n        0x2745,\n        0x4E8A,\n        0x9D14,\n        0x2A09,\n        0x7B61,\n        0xF6C2,\n        0xFDA5,\n        0xEB6B,\n        0xC6F7,\n        0x9DCF,\n        0x2BBF,\n        0x4563,\n        0x8AC6,\n        0x05AD,\n        0x0B5A,\n        0x16B4,\n        0x2D68,\n        0x5AD0,\n        0x0375,\n        0x06EA,\n        0x0DD4,\n        0x1BA8,\n        0x3750,\n        0x6EA0,\n        0xDD40,\n        0xD849,\n        0xA0B3,\n        0x5147,\n        0xA28E,\n        0x553D,\n        0xAA7A,\n        0x44D5,\n        0x6F45,\n        0xDE8A,\n        0xAD35,\n        0x4A4B,\n        0x9496,\n        0x390D,\n        0x721A,\n        0xEB23,\n        0xC667,\n        0x9CEF,\n        0x29FF,\n        0x53FE,\n        0xA7FC,\n        0x5FD9,\n        0x47D3,\n        0x8FA6,\n        0x0F6D,\n        0x1EDA,\n        0x3DB4,\n        0x7B68,\n        0xF6D0,\n        0xB861,\n        0x60E3,\n        0xC1C6,\n        0x93AD,\n        0x377B,\n        0x6EF6,\n        0xDDEC,\n        0x45A0,\n        0x8B40,\n        0x06A1,\n        0x0D42,\n        0x1A84,\n        0x3508,\n        0x6A10,\n        0xAA51,\n        0x4483,\n        0x8906,\n        0x022D,\n        0x045A,\n        0x08B4,\n        0x1168,\n        0x76B4,\n        0xED68,\n        0xCAF1,\n        0x85C3,\n        0x1BA7,\n        0x374E,\n        0x6E9C,\n        0x3730,\n        0x6E60,\n        0xDCC0,\n        0xA9A1,\n        0x4363,\n        0x86C6,\n        0x1DAD,\n        0x3331,\n        0x6662,\n        0xCCC4,\n        0x89A9,\n        0x0373,\n        0x06E6,\n        0x0DCC,\n        0x1021,\n        0x2042,\n        0x4084,\n        0x8108,\n        0x1231,\n        0x2462,\n        0x48C4,\n    ]\n\n    @staticmethod\n    def verifypw(password, verificationBytes):\n        r\"\"\"\n        Return True if the given password is valid.\n\n            >>> from struct import unpack\n            >>> password = 'VelvetSweatshop'\n            >>> (key,) = unpack('<H', b'\\x0A\\x9A')  # 0x9a0a\n            >>> DocumentXOR.verifypw(password, key)\n            True\n        \"\"\"\n        # https://interoperability.blob.core.windows.net/files/MS-OFFCRYPTO/%5bMS-OFFCRYPTO%5d.pdf\n        verifier = 0\n        password_array = []\n        password_array.append(len(password))\n        password_array.extend([ord(ch) for ch in password])\n        password_array.reverse()\n        for password_byte in password_array:\n            if verifier & 0x4000 == 0x0000:\n                intermidiate_1 = 0\n            else:\n                intermidiate_1 = 1\n\n            intermidiate_2 = verifier * 2\n            intermidiate_2 = (\n                intermidiate_2 & 0x7FFF\n            )  # SET most significant bit of Intermediate2 TO 0\n\n            intermidiate_3 = intermidiate_1 ^ intermidiate_2\n\n            verifier = intermidiate_3 ^ password_byte\n\n        return True if (verifier ^ 0xCE4B) == verificationBytes else False\n\n    @staticmethod\n    def xor_ror(byte1, byte2):\n        return DocumentXOR.ror(byte1 ^ byte2, 1, 8)\n\n    @staticmethod\n    def create_xor_key_method1(password):\n        xor_key = DocumentXOR.initial_code[len(password) - 1]\n        current_element = 0x00000068\n\n        data = [ord(ch) for ch in reversed(password)]\n        for ch in data:\n            for i in range(7):\n                if ch & 0x40 != 0:\n                    xor_key = (\n                        xor_key ^ DocumentXOR.xor_matrix[current_element]\n                    ) % 65536\n                ch = (ch << 1) % 256\n                current_element -= 1\n\n        return xor_key\n\n    @staticmethod\n    def create_xor_array_method1(password):\n        xor_key = DocumentXOR.create_xor_key_method1(password)\n\n        index = len(password)\n\n        obfuscation_array = [\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n            0x00,\n        ]\n\n        if index % 2 == 1:\n            temp = (\n                xor_key & 0xFF00\n            ) >> 8  # SET Temp TO most significant byte of XorKey\n            obfuscation_array[index] = DocumentXOR.xor_ror(\n                DocumentXOR.pad_array[0], temp\n            )\n\n            index -= 1\n            temp = xor_key & 0x00FF\n            password_last_char = ord(password[-1])\n            obfuscation_array[index] = DocumentXOR.xor_ror(password_last_char, temp)\n\n        while index > 0:\n            index -= 1\n            temp = (xor_key & 0xFF00) >> 8\n            obfuscation_array[index] = DocumentXOR.xor_ror(ord(password[index]), temp)\n\n            index -= 1\n            temp = xor_key & 0x00FF\n            obfuscation_array[index] = DocumentXOR.xor_ror(ord(password[index]), temp)\n\n        index = 15\n        pad_index = 15 - len(password)\n\n        while pad_index > 0:\n            temp = (xor_key & 0xFF00) >> 8\n            obfuscation_array[index] = DocumentXOR.xor_ror(\n                DocumentXOR.pad_array[pad_index], temp\n            )\n\n            index -= 1\n            pad_index -= 1\n\n            temp = xor_key & 0x00FF\n            obfuscation_array[index] = DocumentXOR.xor_ror(\n                DocumentXOR.pad_array[pad_index], temp\n            )\n\n            index -= 1\n            pad_index -= 1\n\n        return obfuscation_array\n\n    @staticmethod\n    def ror(n, rotations, width):\n        return (2**width - 1) & (n >> rotations | n << (width - rotations))\n\n    @staticmethod\n    def rol(n, rotations, width):\n        return (2**width - 1) & (n << rotations | n >> (width - rotations))\n\n    @staticmethod\n    def decrypt(password, ibuf, plaintext, records, base):\n        r\"\"\"\n        Return decrypted data (DecryptData_Method1)\n        \"\"\"\n        obuf = io.BytesIO()\n\n        xor_array = DocumentXOR.create_xor_array_method1(password)\n\n        data_index = 0\n        record_index = 0\n        while data_index < len(plaintext):\n            count = 1\n            if plaintext[data_index] == -1 or plaintext[data_index] == -2:\n                for j in range(data_index + 1, len(plaintext)):\n                    if plaintext[j] >= 0:\n                        break\n                    count += 1\n\n                if plaintext[data_index] == -2:\n                    xor_array_index = (data_index + count + 4) % 16\n                else:\n                    xor_array_index = (data_index + count) % 16\n                temp_res = 0\n                for item in range(count):\n                    data_byte = ibuf.read(1)\n                    temp_res = data_byte[0] ^ xor_array[xor_array_index]\n                    temp_res = DocumentXOR.ror(temp_res, 5, 8)\n                    obuf.write(temp_res.to_bytes(1, \"little\"))\n\n                    xor_array_index += 1\n\n                    xor_array_index = xor_array_index % 16\n                record_index += 1\n\n            else:\n                obuf.write(ibuf.read(1))\n\n            data_index += count\n\n        obuf.seek(0)\n        return obuf\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"msoffcrypto-tool\"\nversion = \"6.0.0\"\ndescription = \"Python tool and library for decrypting and encrypting MS Office files using a password or other keys\"\nlicense = \"MIT\"\nhomepage = \"https://github.com/nolze/msoffcrypto-tool\"\nauthors = [\"nolze <nolze@int3.net>\"]\nreadme = \"README.md\"\npackages = [{ include = \"msoffcrypto\" }, { include = \"NOTICE.txt\" }]\n\n[tool.poetry.dependencies]\npython = \"^3.10\"\ncryptography = \">=39.0\"\nolefile = \">=0.46\"\n\n[tool.poetry.group.dev.dependencies]\n# pytest = { version = \">=6.2.1\", python = \"^3.7\" }\npytest = \"^9.0.2\"\ncoverage = { extras = [\"toml\"], version = \"^7.5\" }\n\n\n[tool.poetry.group.docs.dependencies]\nsphinx = \"^8\"\nsphinx-autobuild = \"2024.10.02\"\nfuro = \"2025.12.19\"\nmyst-parser = \"^4.0.1\"\nsphinxcontrib-autoprogram = \"^0.1.8\"\n\n[tool.poetry.scripts]\nmsoffcrypto-tool = 'msoffcrypto.__main__:main'\n\n[tool.poetry.requires-plugins]\npoetry-plugin-export = \">=1.8\"\n\n[tool.black]\nline-length = 140\nexclude = '/(\\.git|\\.pytest_cache|\\.venv|\\.vscode|dist|docs)/'\n\n[tool.pytest.ini_options]\naddopts = \"-ra -q --doctest-modules\"\ntestpaths = [\"msoffcrypto\", \"tests\"]\n\n[tool.coverage.run]\nomit = [\".venv/*\", \"tests/*\"]\n\n[build-system]\nrequires = [\"poetry_core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_cli.py",
    "content": "import subprocess\nimport unittest\n\n\nclass CLITest(unittest.TestCase):\n    def test_cli(self):\n        # Python 3:\n        # cp = subprocess.run(\"./tests/test_cli.sh\", shell=True)\n        # self.assertEqual(cp.returncode, 0)\n        # For Python 2 compat:\n        returncode = subprocess.call(\"./tests/test_cli.sh\", shell=True)\n        self.assertEqual(returncode, 0)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_cli.sh",
    "content": "#!/usr/bin/env bash\n\nset -ev\n\ncd \"$(dirname \"$0\")\"\n\nmsoffcrypto-tool () {\n    python ../msoffcrypto \"$@\"\n}\n\n# Decryption\n\nmsoffcrypto-tool --test inputs/example_password.docx && : ; [ $? = 0 ]\nmsoffcrypto-tool --test outputs/example.docx && : ; [ $? = 1 ]\nmsoffcrypto-tool -p Password1234_ inputs/example_password.docx /tmp/example.docx\ndiff /tmp/example.docx outputs/example.docx\n\nmsoffcrypto-tool --test inputs/example_password.xlsx && : ; [ $? = 0 ]\nmsoffcrypto-tool --test outputs/example.xlsx && : ; [ $? = 1 ]\nmsoffcrypto-tool -p Password1234_ inputs/example_password.xlsx /tmp/example.xlsx\ndiff /tmp/example.xlsx outputs/example.xlsx\n\nmsoffcrypto-tool --test inputs/ecma376standard_password.docx && : ; [ $? = 0 ]\nmsoffcrypto-tool --test outputs/ecma376standard_password_plain.docx && : ; [ $? = 1 ]\nmsoffcrypto-tool -p Password1234_ inputs/ecma376standard_password.docx /tmp/ecma376standard_password_plain.docx\ndiff /tmp/ecma376standard_password_plain.docx outputs/ecma376standard_password_plain.docx\n\nmsoffcrypto-tool --test inputs/rc4cryptoapi_password.doc && : ; [ $? = 0 ]\nmsoffcrypto-tool --test outputs/rc4cryptoapi_password_plain.doc && : ; [ $? = 1 ]\nmsoffcrypto-tool -p Password1234_ inputs/rc4cryptoapi_password.doc /tmp/rc4cryptoapi_password_plain.doc\ndiff /tmp/rc4cryptoapi_password_plain.doc outputs/rc4cryptoapi_password_plain.doc\n\nmsoffcrypto-tool --test inputs/rc4cryptoapi_password.xls && : ; [ $? = 0 ]\nmsoffcrypto-tool --test outputs/rc4cryptoapi_password_plain.xls && : ; [ $? = 1 ]\nmsoffcrypto-tool -p Password1234_ inputs/rc4cryptoapi_password.xls /tmp/rc4cryptoapi_password_plain.xls\ndiff /tmp/rc4cryptoapi_password_plain.xls outputs/rc4cryptoapi_password_plain.xls\n\nmsoffcrypto-tool --test inputs/rc4cryptoapi_password.ppt && : ; [ $? = 0 ]\nmsoffcrypto-tool --test outputs/rc4cryptoapi_password_plain.ppt && : ; [ $? = 1 ]\nmsoffcrypto-tool -p Password1234_ inputs/rc4cryptoapi_password.ppt /tmp/rc4cryptoapi_password_plain.ppt\ndiff /tmp/rc4cryptoapi_password_plain.ppt outputs/rc4cryptoapi_password_plain.ppt\n\n# Encryption\n\nmsoffcrypto-tool -e -p Password1234_ outputs/example.docx /tmp/example_password.docx\nmsoffcrypto-tool --test /tmp/example_password.docx && : ; [ $? = 0 ]\nmsoffcrypto-tool -p Password1234_ /tmp/example_password.docx /tmp/example.docx\ndiff /tmp/example.docx outputs/example.docx\n\nmsoffcrypto-tool -e -p Password1234_ outputs/example.xlsx /tmp/example_password.xlsx\nmsoffcrypto-tool --test /tmp/example_password.xlsx && : ; [ $? = 0 ]\nmsoffcrypto-tool -p Password1234_ /tmp/example_password.xlsx /tmp/example.xlsx\ndiff /tmp/example.xlsx outputs/example.xlsx\n"
  },
  {
    "path": "tests/test_compare_known_output.py",
    "content": "#!/usr/bin/env python\n\n\"\"\"Compare output of msoffcrypto-tool for a few input files.\"\"\"\n\nimport os\nimport sys\nimport unittest\nfrom difflib import SequenceMatcher\nfrom os.path import abspath, dirname, isfile\nfrom os.path import join as pjoin\nfrom tempfile import mkstemp\n\ntry:\n    import cryptography\nexcept ImportError:\n    cryptography = None\n\n# add base dir to path so we always import local msoffcrypto\nTEST_BASE_DIR = dirname(abspath(__file__))\nMODULE_BASE_DIR = dirname(TEST_BASE_DIR)\nif sys.path[0] != MODULE_BASE_DIR:\n    sys.path.insert(0, MODULE_BASE_DIR)\nimport msoffcrypto\n\n#: encryption password for files tested here\nPASSWORD = \"Password1234_\"\n\n#: input dir\nINPUT_DIR = \"inputs\"\n\n#: pairs of input/output files\nEXAMPLE_FILES = (\n    (\"example_password.docx\", \"example.docx\", PASSWORD),\n    (\"example_password.xlsx\", \"example.xlsx\", PASSWORD),\n    (\"ecma376standard_password.docx\", \"ecma376standard_password_plain.docx\", PASSWORD),\n    (\"rc4cryptoapi_password.doc\", \"rc4cryptoapi_password_plain.doc\", PASSWORD),\n    (\"rc4cryptoapi_password.xls\", \"rc4cryptoapi_password_plain.xls\", PASSWORD),\n    (\"rc4cryptoapi_password.ppt\", \"rc4cryptoapi_password_plain.ppt\", PASSWORD),\n    (\"xor_password_123456789012345.xls\", \"xor_password_123456789012345_plain.xls\", \"123456789012345\"),\n)\n\n#: output dir:\nOUTPUT_DIR = \"outputs\"\n\n\n@unittest.skipIf(\n    cryptography is None, \"Cryptography module not installed for python{}.{}\".format(sys.version_info.major, sys.version_info.minor)\n)\nclass KnownOutputCompare(unittest.TestCase):\n    \"\"\"See module doc.\"\"\"\n\n    def test_known_output(self):\n        \"\"\"See module doc.\"\"\"\n        for in_name, out_name, password in EXAMPLE_FILES:\n            input_path = pjoin(TEST_BASE_DIR, INPUT_DIR, in_name)\n            expect_path = pjoin(TEST_BASE_DIR, OUTPUT_DIR, out_name)\n\n            # now run the relevant parts of __main__.main:\n            with open(input_path, \"rb\") as input_handle:\n                file = msoffcrypto.OfficeFile(input_handle)\n                if file.format == \"ooxml\" and file.type in [\"standard\", \"agile\"]:\n                    file.load_key(password=password, verify_password=True)\n                else:\n                    file.load_key(password=password)\n\n                out_desc = None\n                out_path = None\n                output = []\n                try:\n                    # create temp file for output of decryption function\n                    out_desc, out_path = mkstemp(prefix=\"msoffcrypto-test-\", suffix=\".txt\", text=True)\n                    with os.fdopen(out_desc, \"wb\") as out_handle:\n                        out_desc = None  # out_handle now owns this\n\n                        # run decryption, capture output\n                        print(\"decrypting {}\".format(in_name))\n                        if file.format == \"ooxml\" and file.type in [\"agile\"]:\n                            file.decrypt(out_handle, verify_integrity=True)\n                        else:\n                            file.decrypt(out_handle)\n\n                    # read extracted output file into memory\n                    with open(expect_path, \"rb\") as reader:\n                        output = reader.read()\n                finally:\n                    # ensure we do not leak temp files. Always close & remove\n                    if out_desc:\n                        os.close(out_desc)\n                    if out_path and isfile(out_path):\n                        os.unlink(out_path)\n\n            # read output file into memory\n            with open(expect_path, \"rb\") as reader:\n                expect = reader.read()\n\n            # compare:\n            print(\"comparing output to {}\".format(out_name))\n            similarity = SequenceMatcher(None, expect, output).ratio()\n            self.assertGreater(similarity, 0.99)\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n"
  },
  {
    "path": "tests/test_file_handle.py",
    "content": "\"\"\"Check that given file handles are not closed.\"\"\"\n\n\nimport unittest\nfrom os.path import dirname, join\n\nfrom msoffcrypto import OfficeFile\n\n#: directory with input\nDATA_DIR = join(dirname(__file__), \"inputs\")\n\n\nclass FileHandleTest(unittest.TestCase):\n    \"\"\"See module doc.\"\"\"\n\n    def test_file_handle_open(self):\n        \"\"\"Check that file handles are open after is_encrypted().\"\"\"\n        for suffix in \"doc\", \"ppt\", \"xls\":\n            path = join(DATA_DIR, \"plain.\" + suffix)\n\n            with open(path, \"rb\") as file_handle:\n                ofile = OfficeFile(file_handle)\n\n                # do something with ofile\n                self.assertEqual(ofile.is_encrypted(), False)\n\n                # check that file handle is still open\n                self.assertFalse(file_handle.closed)\n\n                # destroy OfficeFile, calls destructor\n                del ofile\n\n                # check that file handle is still open\n                self.assertFalse(file_handle.closed)\n\n            # just for completeness:\n            # check that file handle is now closed\n            self.assertTrue(file_handle.closed)\n\n\n# if someone calls this as script, run unittests\nif __name__ == \"__main__\":\n    unittest.main()\n"
  }
]