From 9a092e79473d865a7aee78d362b4c204f0e4139e Mon Sep 17 00:00:00 2001 From: nolze Date: Tue, 16 Jan 2024 23:10:41 +0900 Subject: [PATCH] Update --- LICENSE.txt | 36 ++- README.md | 69 ++++-- msoffcrypto/__main__.py | 22 +- msoffcrypto/exceptions/__init__.py | 22 +- msoffcrypto/method/ecma376_agile.py | 52 ++-- msoffcrypto/method/ecma376_encrypted.py | 302 +++++++++++++++--------- tests/test_cli.sh | 1 + 7 files changed, 324 insertions(+), 180 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 5bb6b92..95e85c0 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -18,4 +18,38 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. + +---------------------------------------------------------------------- + +This software contains derivative works from https://github.com/herumi/msoffice +which is licensed under the BSD 3-Clause License. + +https://github.com/herumi/msoffice/blob/c3cdb1ea0a5285a2a1718fee2dc893fd884bdad0/COPYRIGHT + +Copyright (c) 2007-2015 Cybozu Labs, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. +Neither the name of the Cybozu Labs, Inc. nor the names of its contributors may +be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 239786f..2dda965 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ pip install msoffcrypto-tool ### As CLI tool (with password) +#### Decryption + +Specify the password with `-p` flag: + ``` msoffcrypto-tool encrypted.docx decrypted.docx -p Passw0rd ``` @@ -41,13 +45,20 @@ $ msoffcrypto-tool encrypted.docx decrypted.docx -p Password: ``` -Test if the file is encrypted or not (exit code 0 or 1 is returned): +To check if the file is encrypted or not, use `-t` flag: ``` msoffcrypto-tool document.doc --test -v ``` -Encrypt an OOXML file: +It returns `1` if the file is encrypted, `0` if not. + +#### Encryption (OOXML only, experimental) + +> [!IMPORTANT] +> Encryption feature is experimental. Please use it at your own risk. + +To password-protect a document, use `-e` flag along with `-p` flag: ``` msoffcrypto-tool -e -p Passw0rd plain.docx encrypted.docx @@ -57,7 +68,9 @@ msoffcrypto-tool -e -p Passw0rd plain.docx encrypted.docx Password and more key types are supported with library functions. -Basic decryption usage: +#### Decryption + +Basic usage: ```python import msoffcrypto @@ -73,7 +86,7 @@ with open("decrypted.docx", "wb") as f: encrypted.close() ``` -Basic decryption usage (in-memory): +In-memory: ```python import msoffcrypto @@ -91,7 +104,31 @@ df = pd.read_excel(decrypted) print(df) ``` -Basic encryption usage (only OOXML is supported): +Advanced usage: + +```python +# Verify password before decryption (default: False) +# The ECMA-376 Agile/Standard crypto system allows one to know whether the supplied password is correct before actually decrypting the file +# Currently, the verify_password option is only meaningful for ECMA-376 Agile/Standard Encryption +file.load_key(password="Passw0rd", verify_password=True) + +# Use private key +file.load_key(private_key=open("priv.pem", "rb")) + +# Use intermediate key (secretKey) +file.load_key(secret_key=binascii.unhexlify("AE8C36E68B4BB9EA46E5544A5FDB6693875B2FDE1507CBC65C8BCF99E25C2562")) + +# Check the HMAC of the data payload before decryption (default: False) +# Currently, the verify_integrity option is only meaningful for ECMA-376 Agile Encryption +file.decrypt(open("decrypted.docx", "wb"), verify_integrity=True) +``` + +#### Encryption (OOXML only, experimental) + +> [!IMPORTANT] +> Encryption feature is experimental. Please use it at your own risk. + +Basic usage: ```python from msoffcrypto.format.ooxml import OOXMLFile @@ -105,7 +142,7 @@ with open("encrypted.docx", "wb") as f: plain.close() ``` -Basic encryption usage (in-memory, only OOXML is supported): +In-memory: ```python from msoffcrypto.format.ooxml import OOXMLFile @@ -120,25 +157,6 @@ with open("plain.xlsx", "rb") as f: # Do stuff with encrypted buffer; it contains an OLE container with an encrypted stream ``` -Advanced usage: - -```python -# Verify password before decryption (default: False) -# The ECMA-376 Agile/Standard crypto system allows one to know whether the supplied password is correct before actually decrypting the file -# Currently, the verify_password option is only meaningful for ECMA-376 Agile/Standard Encryption -file.load_key(password="Passw0rd", verify_password=True) - -# Use private key -file.load_key(private_key=open("priv.pem", "rb")) - -# Use intermediate key (secretKey) -file.load_key(secret_key=binascii.unhexlify("AE8C36E68B4BB9EA46E5544A5FDB6693875B2FDE1507CBC65C8BCF99E25C2562")) - -# Check the HMAC of the data payload before decryption (default: False) -# Currently, the verify_integrity option is only meaningful for ECMA-376 Agile Encryption -file.decrypt(open("decrypted.docx", "wb"), verify_integrity=True) -``` - ## Supported encryption methods ### MS-OFFCRYPTO specs @@ -235,7 +253,6 @@ poetry run coverage run -m pytest -v * * * -* ### In publications diff --git a/msoffcrypto/__main__.py b/msoffcrypto/__main__.py index 50de6af..0e8d63d 100644 --- a/msoffcrypto/__main__.py +++ b/msoffcrypto/__main__.py @@ -54,8 +54,8 @@ def is_encrypted(file): group = parser.add_mutually_exclusive_group(required=True) group.add_argument("-p", "--password", nargs="?", const="", dest="password", help="password text") group.add_argument("-t", "--test", dest="test_encrypted", action="store_true", help="test if the file is encrypted") +parser.add_argument("-e", dest="encrypt", action="store_true", help="encryption mode (default is false)") parser.add_argument("-v", dest="verbose", action="store_true", help="print verbose information") -parser.add_argument("-e", dest="encrypt", action="store_true", help="encryption mode (default is to decrypt)") parser.add_argument("infile", nargs="?", type=argparse.FileType("rb"), help="input file") parser.add_argument("outfile", nargs="?", type=argparse.FileType("wb"), help="output file (if blank, stdout is used)") @@ -82,16 +82,6 @@ def main(): else: password = getpass.getpass() - if args.encrypt: - # The only format we support for encryption - file = OOXMLFile(args.infile) - else: - if not olefile.isOleFile(args.infile): - raise exceptions.FileFormatError("Not OLE file") - - file = OfficeFile(args.infile) - file.load_key(password=password) - if args.outfile is None: ifWIN32SetBinary(sys.stdout) if hasattr(sys.stdout, "buffer"): # For Python 2 @@ -100,9 +90,19 @@ def main(): args.outfile = sys.stdout if args.encrypt: + # OOXML is the only format we support for encryption + file = OOXMLFile(args.infile) + file.encrypt(password, args.outfile) else: + if not olefile.isOleFile(args.infile): + raise exceptions.FileFormatError("Not OLE file") + + file = OfficeFile(args.infile) + file.load_key(password=password) + file.decrypt(args.outfile) + if __name__ == "__main__": main() diff --git a/msoffcrypto/exceptions/__init__.py b/msoffcrypto/exceptions/__init__.py index 3a118b8..c2b7fbb 100644 --- a/msoffcrypto/exceptions/__init__.py +++ b/msoffcrypto/exceptions/__init__.py @@ -1,26 +1,28 @@ class FileFormatError(Exception): - """Raised when the format of given file is unsupported or unrecognized. - """ + """Raised when the format of given file is unsupported or unrecognized.""" + pass class ParseError(Exception): - """Raised when the file cannot be parsed correctly. - """ + """Raised when the file cannot be parsed correctly.""" + pass class DecryptionError(Exception): - """Raised when the file cannot be decrypted. - """ + """Raised when the file cannot be decrypted.""" + pass + class EncryptionError(Exception): - """Raised when the file cannot be encrypted. - """ + """Raised when the file cannot be encrypted.""" + pass + class InvalidKeyError(DecryptionError): - """Raised when the given password or key is incorrect or cannot be verified. - """ + """Raised when the given password or key is incorrect or cannot be verified.""" + pass diff --git a/msoffcrypto/method/ecma376_agile.py b/msoffcrypto/method/ecma376_agile.py index 8f26970..3c31b26 100644 --- a/msoffcrypto/method/ecma376_agile.py +++ b/msoffcrypto/method/ecma376_agile.py @@ -1,9 +1,9 @@ +import base64 import functools import hmac import io -import secrets -import base64 import logging +import secrets from hashlib import sha1, sha256, sha384, sha512 from struct import pack, unpack @@ -31,23 +31,29 @@ blkKey_dataIntegrity1 = bytearray([0x5F, 0xB2, 0xAD, 0x01, 0x0C, 0xB9, 0xE1, 0xF6]) blkKey_dataIntegrity2 = bytearray([0xA0, 0x67, 0x7F, 0x02, 0xB2, 0x2C, 0x84, 0x33]) + def _random_buffer(sz): return secrets.token_bytes(sz) + def _get_num_blocks(sz, block): return (sz + block - 1) // block + def _round_up(sz, block): return _get_num_blocks(sz, block) * block -def _resize_buffer(buf, n, c = b'\0'): - if len(buf) >= n : + +def _resize_buffer(buf, n, c=b"\0"): + if len(buf) >= n: return buf[:n] return buf + c * (n - len(buf)) + def _normalize_key(key, n): - return _resize_buffer(key, n, b'\x36') + return _resize_buffer(key, n, b"\x36") + def _get_hash_func(algorithm): return ALGORITHM_HASH.get(algorithm, sha1) @@ -59,6 +65,7 @@ def _decrypt_aes_cbc(data, key, iv): decrypted = decryptor.update(data) + decryptor.finalize() return decrypted + def _encrypt_aes_cbc(data, key, iv): aes = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) @@ -67,6 +74,7 @@ def _encrypt_aes_cbc(data, key, iv): return encrypted + def _encrypt_aes_cbc_padded(data, key, iv, blockSize): buf = data @@ -75,6 +83,7 @@ def _encrypt_aes_cbc_padded(data, key, iv, blockSize): return _encrypt_aes_cbc(buf, key, iv) + def _get_salt(salt_value=None, salt_size=16): if not salt_value is None: if len(salt_value) != salt_size: @@ -84,6 +93,7 @@ def _get_salt(salt_value=None, salt_size=16): return _random_buffer(salt_size) + # Hardcoded to AES256 + SHA512 for OOXML. class ECMA376AgileCipherParams: def __init__(self): @@ -95,8 +105,10 @@ def __init__(self): self.hashSize = 64 self.saltValue = None + def _enc64(b): - return base64.b64encode(b).decode('UTF-8') + return base64.b64encode(b).decode("UTF-8") + class ECMA376AgileEncryptionInfo: def __init__(self): @@ -118,7 +130,7 @@ def toEncryptionDescriptor(self): """ Returns an XML description of the encryption information. """ - return f''' + return f""" @@ -132,7 +144,8 @@ def toEncryptionDescriptor(self): -''' +""" + def _generate_iv(params: ECMA376AgileCipherParams, blkKey, salt_value): if not blkKey: @@ -142,6 +155,7 @@ def _generate_iv(params: ECMA376AgileCipherParams, blkKey, salt_value): return _normalize_key(hashCalc(salt_value + blkKey).digest(), params.blockSize) + class ECMA376Agile: def __init__(self): pass @@ -225,17 +239,15 @@ def encrypt(key, ibuf, salt_value=None, spin_count=100000): When salt_value is not specified (the default), we generate a random one. """ - # - # Encryption ported from C++ (https://github.com/herumi/msoffice) - # - # However, all bugs are my own fault. - # + + # Encryption ported from C++ (https://github.com/herumi/msoffice, BSD-3) + info, secret_key = ECMA376Agile.generate_encryption_parameters(key, salt_value, spin_count) encrypted_data = ECMA376Agile.encrypt_payload(ibuf, info.encryptedKey, secret_key, info.keyData.saltValue) encryption_info = ECMA376Agile.get_encryption_information(info, encrypted_data, secret_key) obuf = io.BytesIO() - ECMA376Encrypted(encrypted_data, encryption_info).writeTo(obuf) + ECMA376Encrypted(encrypted_data, encryption_info).write_to(obuf) return obuf.getvalue() @@ -268,10 +280,14 @@ def generate_encryption_parameters(key, salt_value=None, spin_count=100000): info.encryptedKey.saltValue = _get_salt(salt_value, info.encryptedKey.saltSize) - h = ECMA376Agile._derive_iterated_hash_from_password(key, info.encryptedKey.saltValue, info.encryptedKey.hashName, info.spinCount).digest() + h = ECMA376Agile._derive_iterated_hash_from_password( + key, info.encryptedKey.saltValue, info.encryptedKey.hashName, info.spinCount + ).digest() key1 = ECMA376Agile._derive_encryption_key(h, blkKey_VerifierHashInput, info.encryptedKey.hashName, info.encryptedKey.keyBits) - key2 = ECMA376Agile._derive_encryption_key(h, blkKey_encryptedVerifierHashValue, info.encryptedKey.hashName, info.encryptedKey.keyBits) + key2 = ECMA376Agile._derive_encryption_key( + h, blkKey_encryptedVerifierHashValue, info.encryptedKey.hashName, info.encryptedKey.keyBits + ) key3 = ECMA376Agile._derive_encryption_key(h, blkKey_encryptedKeyValue, info.encryptedKey.hashName, info.encryptedKey.keyBits) verifierHashInput = _random_buffer(info.encryptedKey.saltSize) @@ -287,7 +303,7 @@ def generate_encryption_parameters(key, salt_value=None, spin_count=100000): secret_key = _random_buffer(info.encryptedKey.saltSize) secret_key = _normalize_key(secret_key, info.encryptedKey.keyBits // 8) - info.encryptedKeyValue =_encrypt_aes_cbc(secret_key, key3, info.encryptedKey.saltValue) + info.encryptedKeyValue = _encrypt_aes_cbc(secret_key, key3, info.encryptedKey.saltValue) info.keyData.saltValue = _get_salt(salt_size=info.keyData.saltSize) @@ -337,7 +353,7 @@ def encrypt_payload(ibuf, params: ECMA376AgileCipherParams, secret_key, salt_val @staticmethod def generate_integrity_parameter(encrypted_data, params: ECMA376AgileCipherParams, secret_key, salt_value): """ - Returns the encrypted HmacKey and HmacValue + Returns the encrypted HmacKey and HmacValue. """ salt = _random_buffer(params.hashSize) diff --git a/msoffcrypto/method/ecma376_encrypted.py b/msoffcrypto/method/ecma376_encrypted.py index b753608..b363d75 100644 --- a/msoffcrypto/method/ecma376_encrypted.py +++ b/msoffcrypto/method/ecma376_encrypted.py @@ -1,9 +1,9 @@ -from datetime import datetime -import olefile import io +from datetime import datetime from struct import pack -# +import olefile + # An encrypted ECMA376 file is stored as an OLE container. # # At this point, creating an Ole file is somewhat of a chore, since @@ -22,10 +22,8 @@ # # https://github.com/libyal/libolecf/blob/main/documentation/OLE%20Compound%20File%20format.asciidoc # -# Initial C++ code from https://github.com/herumi/msoffice -# -# However, all bugs are my own fault. -# +# Initial C++ code from https://github.com/herumi/msoffice (BSD-3) + def datetime2filetime(dt): """ @@ -39,10 +37,12 @@ def datetime2filetime(dt): _FILETIME_NULL_DATE = datetime(1601, 1, 1, 0, 0, 0) return int((dt - _FILETIME_NULL_DATE).total_seconds() * 10000000) + class RedBlack: - RED = 0 # Note that this is per-spec; olefile.py shows the opposite + RED = 0 # Note that this is per-spec; olefile.py shows the opposite BLACK = 1 + class DirectoryEntryType: EMPTY = 0 STORAGE = 1 @@ -51,45 +51,49 @@ class DirectoryEntryType: PROPERTY = 4 ROOT_STORAGE = 5 + class SectorTypes: - MAXREGSECT = 0xfffffffa - DIFSECT = 0xfffffffc - FATSECT = 0xfffffffd - ENDOFCHAIN = 0xfffffffe - FREESECT = 0xffffffff - NOSTREAM = 0xffffffff - -# Order in the directories array; must be in sync with getDirectoryEntries() + MAXREGSECT = 0xFFFFFFFA + DIFSECT = 0xFFFFFFFC + FATSECT = 0xFFFFFFFD + ENDOFCHAIN = 0xFFFFFFFE + FREESECT = 0xFFFFFFFF + NOSTREAM = 0xFFFFFFFF + + class DSPos: - iRoot = 0 - iEncryptionPackage = 1 - iDataSpaces = 2 - iVersion = 3 - iDataSpaceMap = 4 - iDataSpaceInfo = 5 - iStongEncryptionDataSpace = 6 - iTransformInfo = 7 - iStrongEncryptionTransform = 8 - iPrimary = 9 - iEncryptionInfo = 10 - dirNum = 11 + # Order in the directories array; must be in sync with getDirectoryEntries() + + iRoot = 0 + iEncryptionPackage = 1 + iDataSpaces = 2 + iVersion = 3 + iDataSpaceMap = 4 + iDataSpaceInfo = 5 + iStongEncryptionDataSpace = 6 + iTransformInfo = 7 + iStrongEncryptionTransform = 8 + iPrimary = 9 + iEncryptionInfo = 10 + dirNum = 11 + -# -# Lifted off of Herumi/msoffice (C++ package) -# https://github.com/herumi/msoffice/blob/master/include/resource.hpp -# class DefaultContent: + # Lifted off of Herumi/msoffice (C++ package) + # https://github.com/herumi/msoffice/blob/master/include/resource.hpp + 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" 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" 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" 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" + class Header: FIRSTNUMDIFAT = 109 - BUFFER_SIZE = 512 # Size taken when writing out to disk/buffer + BUFFER_SIZE = 512 # Size taken when writing out to disk/buffer def __init__(self): - self.minorVersion = 0x003e + self.minorVersion = 0x003E self.majorVersion = 3 self.sectorShift = 9 self.numDirectorySectors = 0 @@ -103,18 +107,37 @@ def __init__(self): self.sectorSize = 1 << self.sectorShift self.difat = [] - def writeTo(self, obuf): + def write_to(self, obuf): obuf.write(olefile.MAGIC) - obuf.write(b"\0" * 16) # CLSID + obuf.write(b"\0" * 16) # CLSID - byteOrder = 0xfffe # Little-Endian + byteOrder = 0xFFFE # Little-Endian miniSectorShift = 6 miniStreamCutoffSize = 0x1000 reserved = 0 - obuf.write(pack(" 2 else 0)) obuf.write(pack("> 32)) + obuf.write(pack("> 32)) @property def Name(self): @@ -186,7 +222,7 @@ def Name(self, n): @property def CLSID(self): - return self._CLSID + return self._CLSID @CLSID.setter def CLSID(self, c): @@ -201,7 +237,7 @@ def LeftSiblingId(self): @LeftSiblingId.setter def LeftSiblingId(self, id): - self._validId(id) + self._valid_id(id) self._LeftSiblingId = id @property @@ -210,7 +246,7 @@ def RightSiblingId(self): @RightSiblingId.setter def RightSiblingId(self, id): - self._validId(id) + self._valid_id(id) self._RightSiblingId = id @property @@ -219,13 +255,14 @@ def ChildId(self): @ChildId.setter def ChildId(self, id): - self._validId(id) + self._valid_id(id) self._ChildId = id - def _validId(self, id): + def _valid_id(self, id): if not ((id <= SectorTypes.MAXREGSECT) or (id == SectorTypes.NOSTREAM)): raise ValueError("Invalid id received") + class ECMA376EncryptedLayout: def __init__(self, sectorSize): self.sectorSize = sectorSize @@ -300,58 +337,93 @@ def offsetData(self, startingSectorLocation): def offsetMiniData(self, startingSectorLocation): return self.offsetMiniFatData + startingSectorLocation * 64 + class ECMA376Encrypted: - def __init__(self, encryptedPackage=b"", encryptionInfo = b""): - self._header = self._getDefaultHeader() - self._dirs = self._getDirectoryEntries() + def __init__(self, encryptedPackage=b"", encryptionInfo=b""): + self._header = self._get_default_header() + self._dirs = self._get_directory_entries() - self.setPayload(encryptedPackage, encryptionInfo) + self.set_payload(encryptedPackage, encryptionInfo) - def writeTo(self, obuf): + def write_to(self, obuf): """ - Writes the encrypted data to obuf + Writes the encrypted data to obuf """ # Create a temporary buffer with seek/tell capabilities, we do not want to assume the passed-in buffer has such # capabilities (ie: piping to stdout). _obuf = io.BytesIO() - self._writeTo(_obuf) + self._write_to(_obuf) # Finalize and write to client buffer. obuf.write(_obuf.getvalue()) - def setPayload(self, encryptedPackage, encryptionInfo): + def set_payload(self, encryptedPackage, encryptionInfo): self._dirs[DSPos.iEncryptionPackage].Content = encryptedPackage self._dirs[DSPos.iEncryptionInfo].Content = encryptionInfo - def _getDefaultHeader(self): + def _get_default_header(self): return Header() - def _getDirectoryEntries(self): + def _get_directory_entries(self): ft = datetime2filetime(datetime.now()) - directories = [ # Must follow DSPos ordering - DirectoryEntry("Root Entry", DirectoryEntryType.ROOT_STORAGE, RedBlack.RED, ct=ft, mt=ft, childId=DSPos.iEncryptionInfo), - DirectoryEntry("EncryptedPackage", DirectoryEntryType.STREAM, RedBlack.RED, ct=ft, mt=ft), - DirectoryEntry("\x06DataSpaces", DirectoryEntryType.STORAGE, RedBlack.RED, ct=ft, mt=ft, childId=DSPos.iDataSpaceMap), - DirectoryEntry("Version", DirectoryEntryType.STREAM, RedBlack.BLACK, ct=ft, mt=ft, content=DefaultContent.Version), - DirectoryEntry("DataSpaceMap", DirectoryEntryType.STREAM, RedBlack.BLACK, ct=ft, mt=ft, leftId=DSPos.iVersion, rightId=DSPos.iDataSpaceInfo, content=DefaultContent.DataSpaceMap), - DirectoryEntry("DataSpaceInfo", DirectoryEntryType.STORAGE, RedBlack.BLACK, ct=ft, mt=ft, rightId=DSPos.iTransformInfo, childId=DSPos.iStongEncryptionDataSpace), - DirectoryEntry("StrongEncryptionDataSpace", DirectoryEntryType.STREAM, RedBlack.BLACK, ct=ft, mt=ft, content=DefaultContent.StrongEncryptionDataSpace), - DirectoryEntry("TransformInfo", DirectoryEntryType.STORAGE, RedBlack.RED, ct=ft, mt=ft, childId=DSPos.iStrongEncryptionTransform), - DirectoryEntry("StrongEncryptionTransform", DirectoryEntryType.STORAGE, RedBlack.BLACK, ct=ft, mt=ft, childId=DSPos.iPrimary), - DirectoryEntry("\x06Primary", DirectoryEntryType.STREAM, RedBlack.BLACK, ct=ft, mt=ft, content=DefaultContent.Primary), - DirectoryEntry("EncryptionInfo", DirectoryEntryType.STREAM, RedBlack.BLACK, ct=ft, mt=ft, leftId=DSPos.iDataSpaces, rightId=DSPos.iEncryptionPackage) + directories = [ # Must follow DSPos ordering + DirectoryEntry("Root Entry", DirectoryEntryType.ROOT_STORAGE, RedBlack.RED, ct=ft, mt=ft, childId=DSPos.iEncryptionInfo), + DirectoryEntry("EncryptedPackage", DirectoryEntryType.STREAM, RedBlack.RED, ct=ft, mt=ft), + DirectoryEntry("\x06DataSpaces", DirectoryEntryType.STORAGE, RedBlack.RED, ct=ft, mt=ft, childId=DSPos.iDataSpaceMap), + DirectoryEntry("Version", DirectoryEntryType.STREAM, RedBlack.BLACK, ct=ft, mt=ft, content=DefaultContent.Version), + DirectoryEntry( + "DataSpaceMap", + DirectoryEntryType.STREAM, + RedBlack.BLACK, + ct=ft, + mt=ft, + leftId=DSPos.iVersion, + rightId=DSPos.iDataSpaceInfo, + content=DefaultContent.DataSpaceMap, + ), + DirectoryEntry( + "DataSpaceInfo", + DirectoryEntryType.STORAGE, + RedBlack.BLACK, + ct=ft, + mt=ft, + rightId=DSPos.iTransformInfo, + childId=DSPos.iStongEncryptionDataSpace, + ), + DirectoryEntry( + "StrongEncryptionDataSpace", + DirectoryEntryType.STREAM, + RedBlack.BLACK, + ct=ft, + mt=ft, + content=DefaultContent.StrongEncryptionDataSpace, + ), + DirectoryEntry( + "TransformInfo", DirectoryEntryType.STORAGE, RedBlack.RED, ct=ft, mt=ft, childId=DSPos.iStrongEncryptionTransform + ), + DirectoryEntry("StrongEncryptionTransform", DirectoryEntryType.STORAGE, RedBlack.BLACK, ct=ft, mt=ft, childId=DSPos.iPrimary), + DirectoryEntry("\x06Primary", DirectoryEntryType.STREAM, RedBlack.BLACK, ct=ft, mt=ft, content=DefaultContent.Primary), + DirectoryEntry( + "EncryptionInfo", + DirectoryEntryType.STREAM, + RedBlack.BLACK, + ct=ft, + mt=ft, + leftId=DSPos.iDataSpaces, + rightId=DSPos.iEncryptionPackage, + ), ] return directories - def _writeTo(self, obuf): + def _write_to(self, obuf): layout = ECMA376EncryptedLayout(self._header.sectorSize) - self._setSectorLocationsOfStreams(layout) - self._detectSectorNum(layout) + self._set_sector_locations_of_streams(layout) + self._detect_sector_num(layout) self._header.firstDirectorySectorLocation = layout.directoryEntryPos self._header.firstMiniFatSectorLocation = layout.miniFatPos @@ -375,53 +447,55 @@ def _writeTo(self, obuf): obuf.write(b"\0" * layout.totalSize) obuf.seek(0) - self._header.writeTo(obuf) + self._header.write_to(obuf) - self._writeDifat(obuf, layout) - self._writeFatStart(obuf, layout) - self._writeMiniFat(obuf, layout) + self._write_DIFAT(obuf, layout) + self._write_FAT_start(obuf, layout) + self._write_MiniFAT(obuf, layout) - self._writeDirectoryEntries(obuf, layout) - self._writeContent(obuf, layout) + self._write_directory_entries(obuf, layout) + self._write_Content(obuf, layout) - def _writeDirectoryEntries(self, obuf, layout: ECMA376EncryptedLayout): + def _write_directory_entries(self, obuf, layout: ECMA376EncryptedLayout): obuf.seek(layout.offsetDirectoryEntries) for d in self._dirs: - d.writeHeaderTo(obuf) # This must write 128 bytes, no more, no less. + d.write_header_to(obuf) # This must write 128 bytes, no more, no less. if obuf.tell() != (layout.offsetDirectoryEntries + len(self._dirs) * 128): + # TODO: Use appropriate custom exception raise Exception("Buffer did not advance as expected when writing out directory entries") - def _writeContent(self, obuf, layout: ECMA376EncryptedLayout): + def _write_Content(self, obuf, layout: ECMA376EncryptedLayout): for d in self._dirs: size = len(d.Content) if size: - if size <= 4096: # Small content goes in the minifat section + if size <= 4096: # Small content goes in the minifat section obuf.seek(layout.offsetMiniData(d.StartingSectorLocation)) obuf.write(d.Content) else: obuf.seek(layout.offsetData(d.StartingSectorLocation)) obuf.write(d.Content) - def _writeFatStart(self, obuf, layout: ECMA376EncryptedLayout): + def _write_FAT_start(self, obuf, layout: ECMA376EncryptedLayout): v = ([SectorTypes.DIFSECT] * layout.difatSectorNum) + ([SectorTypes.FATSECT] * layout.fatSectorNum) v += [layout.numMiniFatSectors, layout.directoryEntrySectorNum, layout.miniFatDataSectorNum, layout.encryptionPackageSectorNum] obuf.seek(layout.offsetFat) - self._writeFat(obuf, v, layout.fatSectorNum * layout.sectorSize) + self._write_FAT(obuf, v, layout.fatSectorNum * layout.sectorSize) - def _writeMiniFat(self, obuf, layout: ECMA376EncryptedLayout): + def _write_MiniFAT(self, obuf, layout: ECMA376EncryptedLayout): obuf.seek(layout.offsetMiniFat) - self._writeFat(obuf, layout.miniFatSectors, layout.numMiniFatSectors * layout.sectorSize) + self._write_FAT(obuf, layout.miniFatSectors, layout.numMiniFatSectors * layout.sectorSize) - def _writeFat(self, obuf, entries, blockSize): + def _write_FAT(self, obuf, entries, blockSize): v = 0 startPos = obuf.tell() - max_n = blockSize // 4 # 4 bytes per entry with 1: + if self._get_block_num(miniFatDataSectorNum, 128) > 1: raise ValueError("Unexpected layout size; too large") layout.miniFatNum = miniFatNum layout.miniFatDataSectorNum = miniFatDataSectorNum layout.miniFatSectors = miniFatSectors - layout.directoryEntrySectorNum = self._getBlockNum(len(self._dirs), 4) - layout.encryptionPackageSectorNum = self._getBlockNum(len(self._dirs[DSPos.iEncryptionPackage].Content), layout.sectorSize) + layout.directoryEntrySectorNum = self._get_block_num(len(self._dirs), 4) + layout.encryptionPackageSectorNum = self._get_block_num(len(self._dirs[DSPos.iEncryptionPackage].Content), layout.sectorSize) - def _getMiniFatSectorNumber(self, size): - return self._getBlockNum(size, 64) + def _get_MiniFAT_sector_number(self, size): + return self._get_block_num(size, 64) - def _getBlockNum(self, x, block): + def _get_block_num(self, x, block): return (x + block - 1) // block - diff --git a/tests/test_cli.sh b/tests/test_cli.sh index 1f08f5e..0881407 100755 --- a/tests/test_cli.sh +++ b/tests/test_cli.sh @@ -41,6 +41,7 @@ msoffcrypto-tool -p Password1234_ inputs/rc4cryptoapi_password.ppt /tmp/rc4crypt diff /tmp/rc4cryptoapi_password_plain.ppt outputs/rc4cryptoapi_password_plain.ppt # Encryption + msoffcrypto-tool -e -p Password1234_ outputs/example.docx /tmp/example_password.docx msoffcrypto-tool --test /tmp/example_password.docx && : ; [ $? = 0 ] msoffcrypto-tool -p Password1234_ /tmp/example_password.docx /tmp/example.docx