Module refinery.lib.ole.crypto

Decryption support for encrypted Microsoft Office documents.

Supports ECMA-376 Agile (OOXML), ECMA-376 Standard, RC4, RC4 CryptoAPI, and XOR obfuscation methods across DOC, XLS, PPT, and OOXML container formats.

Ported from the msoffcrypto-tool project; uses Cryptodome instead of the cryptography package.

Expand source code Browse git
"""
Decryption support for encrypted Microsoft Office documents.

Supports ECMA-376 Agile (OOXML), ECMA-376 Standard, RC4, RC4 CryptoAPI, and XOR obfuscation
methods across DOC, XLS, PPT, and OOXML container formats.

Ported from the msoffcrypto-tool project; uses Cryptodome instead of the cryptography package.
"""
from __future__ import annotations

import base64
import codecs
import enum
import functools
import io
import logging
import zipfile

from hashlib import md5, sha1, sha256, sha384, sha512
from struct import pack, unpack, unpack_from
from typing import NamedTuple
from xml.dom.minidom import parseString

from Cryptodome.Cipher import AES, ARC4

from refinery.lib.ole.file import (
    OleFile,
    is_ole_file,
)
from refinery.lib.structures import MemoryFile

logger = logging.getLogger(__name__)


class FileFormatError(Exception):
    pass


class DecryptionError(Exception):
    pass


class InvalidKeyError(DecryptionError):
    pass


class EncryptionType(enum.Enum):
    PLAIN         = 'plain'          # noqa: E221
    AGILE         = 'agile'          # noqa: E221
    STANDARD      = 'standard'       # noqa: E221
    RC4           = 'rc4'            # noqa: E221
    RC4_CRYPTOAPI = 'rc4_cryptoapi'
    XOR           = 'xor'            # noqa: E221


class EncryptionHeader(NamedTuple):
    flags: int
    size_extra: int
    alg_id: int
    alg_id_hash: int
    key_size: int
    provider_type: int
    csp_name: str


class EncryptionVerifier(NamedTuple):
    salt_size: int
    salt: bytes
    encrypted_verifier: bytes
    verifier_hash_size: int
    encrypted_verifier_hash: bytes


class RC4CryptoAPIInfo(NamedTuple):
    salt: bytes
    key_size: int
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes


class RC4Info(NamedTuple):
    salt: bytes
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes


class StandardEncryptionInfo(NamedTuple):
    header: EncryptionHeader
    verifier: EncryptionVerifier


class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int


class FibBase(NamedTuple):
    w_ident: int
    n_fib: int
    f_encrypted: int
    f_which_tbl_stm: int
    f_obfuscation: int
    i_key: int


class RecordHeader(NamedTuple):
    rec_ver: int
    rec_instance: int
    rec_type: int
    rec_len: int


class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes


class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None


class PersistDirectoryEntry(NamedTuple):
    persist_id: int
    c_persist: int
    rg_persist_offset: list[int]


class PersistDirectoryAtom(NamedTuple):
    rh: RecordHeader
    rg_persist_dir_entry: list[PersistDirectoryEntry]


def _rc4_makekey(password: str, salt: bytes, block: int) -> bytes:
    pw = password.encode('UTF-16LE')
    h0 = md5(pw).digest()[:5]
    h1 = md5((h0 + salt) * 16).digest()[:5]
    return md5(h1 + pack('<I', block)).digest()[:16]


class DocumentRC4:

    @staticmethod
    def verifypw(
        password: str,
        salt: bytes,
        encrypted_verifier: bytes,
        encrypted_verifier_hash: bytes,
    ) -> bool:
        key = _rc4_makekey(password, salt, 0)
        dec = ARC4.new(key)
        verifier = dec.decrypt(encrypted_verifier)
        verifier_hash = dec.decrypt(encrypted_verifier_hash)
        return md5(verifier).digest() == verifier_hash

    @staticmethod
    def decrypt(
        password: str,
        salt: bytes,
        ibuf: io.IOBase,
        blocksize: int = 0x200,
    ) -> MemoryFile[bytearray]:
        obuf = MemoryFile()
        block = 0
        key = _rc4_makekey(password, salt, block)
        for buf in iter(functools.partial(ibuf.read, blocksize), b''):
            obuf.write(ARC4.new(key).decrypt(buf))
            block += 1
            key = _rc4_makekey(password, salt, block)
        obuf.seek(0)
        return obuf


def _rc4api_makekey(
    password: str,
    salt: bytes,
    key_length: int,
    block: int,
) -> bytes:
    pw = password.encode('UTF-16LE')
    hfinal = sha1(sha1(salt + pw).digest() + pack('<I', block)).digest()
    if key_length == 40:
        return hfinal[:5] + b'\x00' * 11
    return hfinal[:key_length // 8]


class DocumentRC4CryptoAPI:

    @staticmethod
    def verifypw(
        password: str,
        salt: bytes,
        key_size: int,
        encrypted_verifier: bytes,
        encrypted_verifier_hash: bytes,
    ) -> bool:
        key = _rc4api_makekey(password, salt, key_size, 0)
        dec = ARC4.new(key)
        verifier = dec.decrypt(encrypted_verifier)
        verifier_hash = dec.decrypt(encrypted_verifier_hash)
        return sha1(verifier).digest() == verifier_hash

    @staticmethod
    def decrypt(
        password: str,
        salt: bytes,
        key_size: int,
        ibuf: io.IOBase,
        blocksize: int = 0x200,
        block: int = 0,
    ) -> MemoryFile[bytearray]:
        obuf = MemoryFile()
        key = _rc4api_makekey(password, salt, key_size, block)
        for buf in iter(functools.partial(ibuf.read, blocksize), b''):
            obuf.write(ARC4.new(key).decrypt(buf))
            block += 1
            key = _rc4api_makekey(password, salt, key_size, block)
        obuf.seek(0)
        return obuf


class DocumentXOR:

    _PAD = [
        0xBB, 0xFF, 0xFF, 0xBA, 0xFF, 0xFF, 0xB9, 0x80,
        0x00, 0xBE, 0x0F, 0x00, 0xBF, 0x0F, 0x00,
    ]
    _INITIAL_CODE = [
        0xE1F0, 0x1D0F, 0xCC9C, 0x84C0, 0x110C, 0x0E10, 0xF1CE, 0x313E,
        0x1872, 0xE139, 0xD40F, 0x84F9, 0x280C, 0xA96A, 0x4EC3,
    ]
    _XOR_MATRIX = [
        0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09,
        0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF,
        0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0,
        0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40,
        0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5,
        0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A,
        0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9,
        0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0,
        0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC,
        0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10,
        0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168,
        0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C,
        0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD,
        0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC,
        0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4,
    ]

    @staticmethod
    def _ror(n: int, rotations: int, width: int) -> int:
        return (2 ** width - 1) & (n >> rotations | n << (width - rotations))

    @staticmethod
    def _xor_ror(byte1: int, byte2: int) -> int:
        return DocumentXOR._ror(byte1 ^ byte2, 1, 8)

    @staticmethod
    def verifypw(password: str, verification_bytes: int) -> bool:
        verifier = 0
        pw_arr = [len(password)] + [ord(ch) for ch in password]
        pw_arr.reverse()
        for b in pw_arr:
            if verifier & 0x4000:
                intermediate = 1
            else:
                intermediate = 0
            verifier = (intermediate ^ ((verifier * 2) & 0x7FFF)) ^ b
        return (verifier ^ 0xCE4B) == verification_bytes

    @staticmethod
    def _create_xor_key(password: str) -> int:
        xor_key = DocumentXOR._INITIAL_CODE[len(password) - 1]
        element = 0x68
        for ch in reversed(password):
            c = ord(ch)
            for _ in range(7):
                if c & 0x40:
                    xor_key = (xor_key ^ DocumentXOR._XOR_MATRIX[element]) % 65536
                c = (c << 1) % 256
                element -= 1
        return xor_key

    @staticmethod
    def _create_xor_array(password: str) -> list[int]:
        xor_key = DocumentXOR._create_xor_key(password)
        index = len(password)
        obfuscation = [0] * 16

        if index % 2 == 1:
            temp = (xor_key & 0xFF00) >> 8
            obfuscation[index] = DocumentXOR._xor_ror(DocumentXOR._PAD[0], temp)
            index -= 1
            temp = xor_key & 0x00FF
            obfuscation[index] = DocumentXOR._xor_ror(ord(password[-1]), temp)

        while index > 0:
            index -= 1
            temp = (xor_key & 0xFF00) >> 8
            obfuscation[index] = DocumentXOR._xor_ror(ord(password[index]), temp)
            index -= 1
            temp = xor_key & 0x00FF
            obfuscation[index] = DocumentXOR._xor_ror(ord(password[index]), temp)

        idx = 15
        pad_idx = 15 - len(password)
        while pad_idx > 0:
            temp = (xor_key & 0xFF00) >> 8
            obfuscation[idx] = DocumentXOR._xor_ror(DocumentXOR._PAD[pad_idx], temp)
            idx -= 1
            pad_idx -= 1
            temp = xor_key & 0x00FF
            obfuscation[idx] = DocumentXOR._xor_ror(DocumentXOR._PAD[pad_idx], temp)
            idx -= 1
            pad_idx -= 1

        return obfuscation

    @staticmethod
    def decrypt(
        password: str,
        ibuf: io.IOBase,
        plaintext: list[int],
        records: list,
        base: int,
    ) -> MemoryFile[bytearray]:
        obuf = MemoryFile()
        xor_array = DocumentXOR._create_xor_array(password)
        data_index = 0
        while data_index < len(plaintext):
            count = 1
            if plaintext[data_index] in (-1, -2):
                for j in range(data_index + 1, len(plaintext)):
                    if plaintext[j] >= 0:
                        break
                    count += 1
                if plaintext[data_index] == -2:
                    xor_idx = (data_index + count + 4) % 16
                else:
                    xor_idx = (data_index + count) % 16
                for _ in range(count):
                    b = ibuf.read(1)
                    dec = DocumentXOR._ror(b[0] ^ xor_array[xor_idx], 5, 8)
                    obuf.write_byte(dec)
                    xor_idx = (xor_idx + 1) % 16
            else:
                obuf.write(ibuf.read(1))
            data_index += count
        obuf.seek(0)
        return obuf


_ALGORITHM_HASH = {
    'SHA1'  : sha1,
    'SHA256': sha256,
    'SHA384': sha384,
    'SHA512': sha512,
}

_BLK_KEY_ENCRYPTED_KEY_VALUE = bytearray(
    [0x14, 0x6E, 0x0B, 0xE7, 0xAB, 0xAC, 0xD0, 0xD6])


def _get_hash_func(algorithm: str):
    return _ALGORITHM_HASH.get(algorithm, sha1)


def _normalize_key(key: bytes, n: int) -> bytes:
    if len(key) >= n:
        return key[:n]
    return key + b'\x36' * (n - len(key))


def _decrypt_aes_cbc(data: bytes, key: bytes, iv: bytes) -> bytes:
    cipher = AES.new(key, AES.MODE_CBC, iv[:16])
    return cipher.decrypt(data)


class ECMA376Agile:

    @staticmethod
    def _derive_iterated_hash(
        password: str,
        salt: bytes,
        algorithm: str,
        spin: int,
    ):
        h = _get_hash_func(algorithm)
        hobj = h(salt + password.encode('UTF-16LE'))
        for i in range(spin):
            hobj = h(pack('<I', i) + hobj.digest())
        return hobj

    @staticmethod
    def _derive_encryption_key(
        h: bytes,
        block_key: bytes | bytearray,
        algorithm: str,
        key_bits: int,
    ) -> bytes:
        hfinal = _get_hash_func(algorithm)(h + block_key)
        return hfinal.digest()[:key_bits // 8]

    @staticmethod
    def decrypt(
        key: bytes,
        key_data_salt: bytes,
        hash_algorithm: str,
        ibuf: io.IOBase,
    ) -> bytearray:
        SEGMENT_LENGTH = 4096
        hashCalc = _get_hash_func(hash_algorithm)
        obuf = MemoryFile()
        ibuf.seek(0)
        total_size = unpack('<Q', ibuf.read(8))[0]
        remaining = total_size
        for i, buf in enumerate(
            iter(functools.partial(ibuf.read, SEGMENT_LENGTH), b'')
        ):
            iv = hashCalc(key_data_salt + pack('<I', i)).digest()[:16]
            dec = _decrypt_aes_cbc(buf, key, iv)
            if remaining < len(dec):
                dec = dec[:remaining]
            obuf.write(dec)
            remaining -= len(dec)
            if remaining <= 0:
                break
        return obuf.getvalue()

    @staticmethod
    def makekey_from_password(
        password: str,
        salt: bytes,
        hash_algorithm: str,
        encrypted_key_value: bytes,
        spin_value: int,
        key_bits: int,
    ) -> bytes:
        h = ECMA376Agile._derive_iterated_hash(password, salt, hash_algorithm, spin_value)
        enc_key = ECMA376Agile._derive_encryption_key(
            h.digest(), _BLK_KEY_ENCRYPTED_KEY_VALUE, hash_algorithm, key_bits)
        return _decrypt_aes_cbc(encrypted_key_value, enc_key, salt)


class ECMA376Standard:

    @staticmethod
    def decrypt(key: bytes, ibuf: io.IOBase) -> bytearray:
        obuf = MemoryFile()
        total_size = unpack('<I', ibuf.read(4))[0]
        ibuf.seek(8)
        data = ibuf.read()
        pad = (AES.block_size - len(data) % AES.block_size) % AES.block_size
        if pad:
            data = data + b'\x00' * pad
        cipher = AES.new(key, AES.MODE_ECB)
        dec = cipher.decrypt(data)
        obuf.write(dec[:total_size])
        return obuf.getvalue()

    @staticmethod
    def verifykey(
        key: bytes,
        encrypted_verifier: bytes,
        encrypted_verifier_hash: bytes,
    ) -> bool:
        cipher = AES.new(key, AES.MODE_ECB)
        verifier = cipher.decrypt(encrypted_verifier)
        expected = sha1(verifier).digest()
        cipher = AES.new(key, AES.MODE_ECB)
        verifier_hash = cipher.decrypt(encrypted_verifier_hash)[:sha1().digest_size]
        return expected == verifier_hash

    @staticmethod
    def makekey_from_password(
        password: str,
        alg_id: int,
        alg_id_hash: int,
        provider_type: int,
        key_size: int,
        salt_size: int,
        salt: bytes,
    ) -> bytes:
        ITER_COUNT = 50000
        cb_hash = sha1().digest_size
        cb_key = key_size // 8

        pw = password.encode('UTF-16LE')
        h = sha1(salt + pw).digest()
        for i in range(ITER_COUNT):
            h = sha1(pack('<I', i) + h).digest()
        hfinal = sha1(h + pack('<I', 0)).digest()

        buf1 = bytearray(b'\x36' * 64)
        for i in range(cb_hash):
            buf1[i] ^= hfinal[i]
        x1 = sha1(buf1).digest()
        buf2 = bytearray(b'\x5c' * 64)
        for i in range(cb_hash):
            buf2[i] ^= hfinal[i]
        x2 = sha1(buf2).digest()
        x3 = x1 + x2
        return x3[:cb_key]


def _parse_encryption_header(blob: io.IOBase) -> EncryptionHeader:
    flags = unpack('<I', blob.read(4))[0]
    size_extra = unpack('<I', blob.read(4))[0]
    alg_id = unpack('<I', blob.read(4))[0]
    alg_id_hash = unpack('<I', blob.read(4))[0]
    key_size = unpack('<I', blob.read(4))[0]
    provider_type = unpack('<I', blob.read(4))[0]
    blob.read(4)
    blob.read(4)
    csp_name = codecs.decode(blob.read(), 'utf-16le')
    return EncryptionHeader(
        flags=flags,
        size_extra=size_extra,
        alg_id=alg_id,
        alg_id_hash=alg_id_hash,
        key_size=key_size,
        provider_type=provider_type,
        csp_name=csp_name,
    )


def _parse_encryption_verifier(blob: io.IOBase, algorithm: str) -> EncryptionVerifier:
    salt_size = unpack('<I', blob.read(4))[0]
    salt = bytes(blob.read(16))
    encrypted_verifier = bytes(blob.read(16))
    verifier_hash_size = unpack('<I', blob.read(4))[0]
    if algorithm == 'RC4':
        encrypted_verifier_hash = bytes(blob.read(20))
    elif algorithm == 'AES':
        encrypted_verifier_hash = bytes(blob.read(32))
    else:
        raise ValueError(F'Invalid algorithm: {algorithm}')
    return EncryptionVerifier(
        salt_size=salt_size,
        salt=salt,
        encrypted_verifier=encrypted_verifier,
        verifier_hash_size=verifier_hash_size,
        encrypted_verifier_hash=encrypted_verifier_hash,
    )


def _parse_header_rc4_cryptoapi(stream: io.IOBase) -> RC4CryptoAPIInfo:
    stream.read(4)
    header_size = unpack('<I', stream.read(4))[0]
    blob = MemoryFile(stream.read(header_size))
    header = _parse_encryption_header(blob)
    key_size = 0x28 if header.key_size == 0 else header.key_size
    blob = MemoryFile(stream.read())
    verifier = _parse_encryption_verifier(blob, 'RC4')
    return RC4CryptoAPIInfo(
        salt=verifier.salt,
        key_size=key_size,
        encrypted_verifier=verifier.encrypted_verifier,
        encrypted_verifier_hash=verifier.encrypted_verifier_hash,
    )


def _parse_header_rc4(stream: io.IOBase) -> RC4Info:
    return RC4Info(
        salt=bytes(stream.read(16)),
        encrypted_verifier=bytes(stream.read(16)),
        encrypted_verifier_hash=bytes(stream.read(16)),
    )


def _parseinfo_standard(ole_stream: io.IOBase) -> StandardEncryptionInfo:
    ole_stream.read(4)
    enc_header_size = unpack('<I', ole_stream.read(4))[0]
    block = ole_stream.read(enc_header_size)
    header = _parse_encryption_header(MemoryFile(block))
    block = ole_stream.read()
    algo = 'AES' if header.alg_id & 0xFF00 == 0x6600 else 'RC4'
    verifier = _parse_encryption_verifier(MemoryFile(block), algo)
    return StandardEncryptionInfo(header=header, verifier=verifier)


def _parseinfo_agile(ole_stream: io.IOBase) -> AgileEncryptionInfo:
    ole_stream.seek(8)
    xml = parseString(ole_stream.read())
    kd = xml.getElementsByTagName('keyData')[0]
    di = xml.getElementsByTagName('dataIntegrity')[0]
    pw = xml.getElementsByTagNameNS(
        'http://schemas.microsoft.com/office/2006/keyEncryptor/password',
        'encryptedKey')[0]
    return AgileEncryptionInfo(
        key_data_salt=base64.b64decode(kd.getAttribute('saltValue')),
        key_data_hash_algorithm=kd.getAttribute('hashAlgorithm'),
        key_data_block_size=int(kd.getAttribute('blockSize')),
        encrypted_hmac_key=base64.b64decode(di.getAttribute('encryptedHmacKey')),
        encrypted_hmac_value=base64.b64decode(di.getAttribute('encryptedHmacValue')),
        encrypted_verifier_hash_input=base64.b64decode(
            pw.getAttribute('encryptedVerifierHashInput')),
        encrypted_verifier_hash_value=base64.b64decode(
            pw.getAttribute('encryptedVerifierHashValue')),
        encrypted_key_value=base64.b64decode(pw.getAttribute('encryptedKeyValue')),
        spin_value=int(pw.getAttribute('spinCount')),
        password_salt=base64.b64decode(pw.getAttribute('saltValue')),
        password_hash_algorithm=pw.getAttribute('hashAlgorithm'),
        password_key_bits=int(pw.getAttribute('keyBits')),
    )


def _parseinfo(
    ole_stream: io.IOBase,
) -> tuple[EncryptionType, StandardEncryptionInfo | AgileEncryptionInfo]:
    v_major, v_minor = unpack('<HH', ole_stream.read(4))
    if v_major == 4 and v_minor == 4:
        return EncryptionType.AGILE, _parseinfo_agile(ole_stream)
    elif v_major in (2, 3, 4) and v_minor == 2:
        return EncryptionType.STANDARD, _parseinfo_standard(ole_stream)
    elif v_major in (3, 4) and v_minor == 3:
        raise DecryptionError('Unsupported EncryptionInfo version (Extensible Encryption)')
    raise DecryptionError(F'Unsupported EncryptionInfo version ({v_major}:{v_minor})')


class OOXMLFile:

    def __init__(self, file: io.IOBase):
        file.seek(0)
        data = file.read()
        file.seek(0)
        self._data = data
        self._type = EncryptionType.PLAIN
        self._info: StandardEncryptionInfo | AgileEncryptionInfo | None = None
        self._key: bytes | None = None

        if is_ole_file(data):
            ole = OleFile(data)
            self._ole = ole
            if not ole.exists('EncryptionInfo'):
                raise FileFormatError('No EncryptionInfo stream found')
            with ole.openstream('EncryptionInfo') as stream:
                self._type, self._info = _parseinfo(stream)
        elif zipfile.is_zipfile(file):
            self._type = EncryptionType.PLAIN
        else:
            raise FileFormatError('Unsupported file format')

    def _require_key(self) -> bytes:
        if self._key is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        return self._key

    def load_key(self, password: str | None = None):
        if not password:
            raise DecryptionError('No password specified')
        if self._type == EncryptionType.AGILE:
            if not isinstance(self._info, AgileEncryptionInfo):
                raise DecryptionError('Encryption info mismatch')
            self._key = ECMA376Agile.makekey_from_password(
                password,
                self._info.password_salt,
                self._info.password_hash_algorithm,
                self._info.encrypted_key_value,
                self._info.spin_value,
                self._info.password_key_bits,
            )
        elif self._type == EncryptionType.STANDARD:
            if not isinstance(self._info, StandardEncryptionInfo):
                raise DecryptionError('Encryption info mismatch')
            self._key = ECMA376Standard.makekey_from_password(
                password,
                self._info.header.alg_id,
                self._info.header.alg_id_hash,
                self._info.header.provider_type,
                self._info.header.key_size,
                self._info.verifier.salt_size,
                self._info.verifier.salt,
            )

    def _decrypt_package(self) -> bytearray:
        key = self._require_key()
        if self._type == EncryptionType.AGILE:
            if not isinstance(self._info, AgileEncryptionInfo):
                raise DecryptionError('Encryption info mismatch')
            with self._ole.openstream('EncryptedPackage') as stream:
                return ECMA376Agile.decrypt(
                    key,
                    self._info.key_data_salt,
                    self._info.key_data_hash_algorithm,
                    stream,
                )
        if self._type == EncryptionType.STANDARD:
            with self._ole.openstream('EncryptedPackage') as stream:
                return ECMA376Standard.decrypt(key, stream)
        raise DecryptionError(F'Unsupported encryption type: {self._type}')

    def decrypt(self, outfile: io.IOBase):
        if self._type == EncryptionType.PLAIN:
            raise DecryptionError('Document is not encrypted')
        obuf = self._decrypt_package()
        outfile.write(obuf)
        if not zipfile.is_zipfile(io.BytesIO(obuf)):
            raise InvalidKeyError('The file could not be decrypted with this password')

    def is_encrypted(self) -> bool:
        return self._type != EncryptionType.PLAIN


def _parse_fib_base(data: bytes | bytearray | memoryview) -> FibBase:
    mv = memoryview(data)
    w_ident = unpack_from('<H', mv, 0)[0]
    n_fib = unpack_from('<H', mv, 2)[0]
    bits = unpack_from('<H', mv, 10)[0]
    f_encrypted = (bits >> 8) & 1
    f_which_tbl = (bits >> 9) & 1
    f_obfuscation = (bits >> 15) & 1
    i_key = unpack_from('<I', mv, 14)[0]
    return FibBase(
        w_ident=w_ident,
        n_fib=n_fib,
        f_encrypted=f_encrypted,
        f_which_tbl_stm=f_which_tbl,
        f_obfuscation=f_obfuscation,
        i_key=i_key,
    )


def _patch_fib_encryption(data: bytearray) -> bytearray:
    """
    Return a copy of the first 32 bytes of the FIB with encryption cleared.
    """
    patched = bytearray(data[:32])
    bits = unpack_from('<H', patched, 10)[0]
    bits &= ~(1 << 8)
    bits &= ~(1 << 15)
    patched[10:12] = pack('<H', bits)
    patched[14:18] = pack('<I', 0)
    return patched


class Doc97File:

    def __init__(self, file: io.IOBase):
        file.seek(0)
        self._data = file.read()
        self._ole = OleFile(self._data)
        self._key: str | None = None
        self._salt: bytes | None = None
        self._key_size: int = 0
        self._type: EncryptionType | None = None

        with self._ole.openstream('wordDocument') as stream:
            fib_raw = stream.read(32)
            self._fib = _parse_fib_base(fib_raw)
        self._tablename = '1Table' if self._fib.f_which_tbl_stm else '0Table'

    def load_key(self, password: str | None = None):
        if not password:
            raise DecryptionError('No password specified')
        fib = self._fib
        if not fib.f_encrypted:
            raise DecryptionError('File is not encrypted')
        if fib.f_obfuscation:
            raise DecryptionError('XOR obfuscation in DOC is not supported')

        with self._ole.openstream(self._tablename) as table:
            v_major, v_minor = unpack('<HH', table.read(4))
            if v_major == 1 and v_minor == 1:
                rc4_info = _parse_header_rc4(table)
                if DocumentRC4.verifypw(
                    password, rc4_info.salt,
                    rc4_info.encrypted_verifier,
                    rc4_info.encrypted_verifier_hash,
                ):
                    self._type = EncryptionType.RC4
                    self._key = password
                    self._salt = rc4_info.salt
                else:
                    raise InvalidKeyError('Failed to verify password')
            elif v_major in (2, 3, 4) and v_minor == 2:
                cryptoapi_info = _parse_header_rc4_cryptoapi(table)
                if DocumentRC4CryptoAPI.verifypw(
                    password, cryptoapi_info.salt, cryptoapi_info.key_size,
                    cryptoapi_info.encrypted_verifier,
                    cryptoapi_info.encrypted_verifier_hash,
                ):
                    self._type = EncryptionType.RC4_CRYPTOAPI
                    self._key = password
                    self._salt = cryptoapi_info.salt
                    self._key_size = cryptoapi_info.key_size
                else:
                    raise InvalidKeyError('Failed to verify password')
            else:
                raise DecryptionError('Unsupported encryption method')

    def _require_key(self) -> tuple[str, bytes]:
        if self._key is None or self._salt is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        return self._key, self._salt

    def _decrypt_stream(self, name: str) -> MemoryFile[bytearray]:
        key, salt = self._require_key()
        stream = self._ole.openstream(name)
        if self._type == EncryptionType.RC4:
            return DocumentRC4.decrypt(key, salt, stream)
        return DocumentRC4CryptoAPI.decrypt(key, salt, self._key_size, stream)

    def decrypt(self, outfile: io.IOBase):
        FIB_LENGTH = 0x44
        fib_raw = bytearray(self._ole.openstream('wordDocument').read(FIB_LENGTH))
        patched_header = _patch_fib_encryption(fib_raw)
        fib_prefix = bytearray(patched_header)
        fib_prefix.extend(fib_raw[len(patched_header):FIB_LENGTH])

        dec_wd = self._decrypt_stream('wordDocument')
        dec_wd.seek(FIB_LENGTH)
        fib_prefix += dec_wd.read()
        word_doc_buf = fib_prefix

        table_buf = self._decrypt_stream(self._tablename).read()

        data_buf: bytes | bytearray | None = None
        if self._ole.exists('Data'):
            data_buf = self._decrypt_stream('Data').read()

        out = bytearray(self._data)
        ole = OleFile(out)
        ole.write_stream('wordDocument', word_doc_buf)
        ole.write_stream(self._tablename, table_buf)
        if data_buf is not None:
            ole.write_stream('Data', data_buf)
        outfile.write(out)

    def is_encrypted(self) -> bool:
        return self._fib.f_encrypted == 1


_BIFF_BOF         = 2057  # noqa
_BIFF_FILEPASS    = 47    # noqa
_BIFF_USREXCL     = 404   # noqa
_BIFF_FILELOCK     = 405   # noqa
_BIFF_INTERFACEHDR = 225   # noqa
_BIFF_RRDINFO      = 406   # noqa
_BIFF_RRDHEAD      = 312   # noqa
_BIFF_BOUNDSHEET8  = 133   # noqa


class _BIFFStream:

    def __init__(self, data: io.IOBase):
        self._data = data

    def has_record(self, target: int) -> bool:
        pos = self._data.tell()
        while True:
            h = self._data.read(4)
            if not h or len(h) < 4:
                self._data.seek(pos)
                return False
            num, size = unpack('<HH', h)
            if num == target:
                self._data.seek(pos)
                return True
            self._data.read(size)

    def skip_to(self, target: int) -> tuple[int, int]:
        while True:
            h = self._data.read(4)
            if not h or len(h) < 4:
                raise DecryptionError('Record not found')
            num, size = unpack('<HH', h)
            if num == target:
                return num, size
            self._data.read(size)

    def iter_record(self):
        while True:
            h = self._data.read(4)
            if not h or len(h) < 4:
                break
            num, size = unpack('<HH', h)
            record = MemoryFile(self._data.read(size))
            yield num, size, record


_NOT_OBFUSCATED = frozenset((
    _BIFF_BOF,
    _BIFF_FILEPASS,
    _BIFF_USREXCL,
    _BIFF_FILELOCK,
    _BIFF_INTERFACEHDR,
    _BIFF_RRDINFO,
    _BIFF_RRDHEAD,
))


class Xls97File:

    def __init__(self, file: io.IOBase):
        file.seek(0)
        self._data = file.read()
        self._ole = OleFile(self._data)
        self._key: str | None = None
        self._salt: bytes | None = None
        self._key_size: int = 0
        self._type: EncryptionType | None = None

    def load_key(self, password: str | None = None):
        if not password:
            raise DecryptionError('No password specified')
        with self._ole.openstream('Workbook') as wb:
            workbook = _BIFFStream(wb)
            num = unpack('<H', workbook._data.read(2))[0]
            if num != _BIFF_BOF:
                raise FileFormatError('Invalid Workbook stream')
            size = unpack('<H', workbook._data.read(2))[0]
            workbook._data.read(size)
            _num, size = workbook.skip_to(_BIFF_FILEPASS)

            enc_type = unpack('<H', workbook._data.read(2))[0]
            enc_info = MemoryFile(workbook._data.read(size - 2))

            if enc_type == 0x0000:
                key, verification_bytes = unpack('<HH', enc_info.read(4))
                if DocumentXOR.verifypw(password, verification_bytes):
                    self._type = EncryptionType.XOR
                    self._key = password
                else:
                    raise InvalidKeyError('Failed to verify password')
            elif enc_type == 0x0001:
                v_major, v_minor = unpack('<HH', enc_info.read(4))
                if v_major == 1 and v_minor == 1:
                    rc4_info = _parse_header_rc4(enc_info)
                    if DocumentRC4.verifypw(
                        password, rc4_info.salt,
                        rc4_info.encrypted_verifier,
                        rc4_info.encrypted_verifier_hash,
                    ):
                        self._type = EncryptionType.RC4
                        self._key = password
                        self._salt = rc4_info.salt
                    else:
                        raise InvalidKeyError('Failed to verify password')
                elif v_major in (2, 3, 4) and v_minor == 2:
                    cryptoapi_info = _parse_header_rc4_cryptoapi(enc_info)
                    if DocumentRC4CryptoAPI.verifypw(
                        password, cryptoapi_info.salt, cryptoapi_info.key_size,
                        cryptoapi_info.encrypted_verifier,
                        cryptoapi_info.encrypted_verifier_hash,
                    ):
                        self._type = EncryptionType.RC4_CRYPTOAPI
                        self._key = password
                        self._salt = cryptoapi_info.salt
                        self._key_size = cryptoapi_info.key_size
                    else:
                        raise InvalidKeyError('Failed to verify password')
                else:
                    raise DecryptionError('Unsupported encryption method')

    def _read_workbook_records(self) -> tuple[list[int], MemoryFile]:
        plain_buf: list[int] = []
        encrypted_buf = MemoryFile()
        with self._ole.openstream('Workbook') as wb:
            workbook = _BIFFStream(wb)
            for num, size, record in workbook.iter_record():
                if num == _BIFF_FILEPASS:
                    plain_buf.extend((0, 0))
                    plain_buf.extend(pack('<H', size))
                    plain_buf.extend(0 for _ in range(size))
                    encrypted_buf.write(b'\x00' * (4 + size))
                elif num in _NOT_OBFUSCATED:
                    header = pack('<HH', num, size)
                    plain_buf.extend(header)
                    plain_buf.extend(record.read())
                    encrypted_buf.write(b'\x00' * (4 + size))
                elif num == _BIFF_BOUNDSHEET8:
                    header = pack('<HH', num, size)
                    plain_buf.extend(header)
                    plain_buf.extend(record.read(4))
                    plain_buf.extend(-2 for _ in range(size - 4))
                    encrypted_buf.write(b'\x00' * 4 + b'\x00' * 4 + record.read())
                else:
                    header = pack('<HH', num, size)
                    plain_buf.extend(header)
                    plain_buf.extend(-1 for _ in range(size))
                    encrypted_buf.write(b'\x00' * 4 + record.read())
        encrypted_buf.seek(0)
        return plain_buf, encrypted_buf

    def decrypt(self, outfile: io.IOBase):
        if self._key is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        plain_buf, encrypted_buf = self._read_workbook_records()

        if self._type == EncryptionType.RC4:
            if self._salt is None:
                raise DecryptionError('load_key() must be called before decrypt()')
            dec = DocumentRC4.decrypt(self._key, self._salt, encrypted_buf, blocksize=1024)
        elif self._type == EncryptionType.RC4_CRYPTOAPI:
            if self._salt is None:
                raise DecryptionError('load_key() must be called before decrypt()')
            dec = DocumentRC4CryptoAPI.decrypt(
                self._key, self._salt, self._key_size, encrypted_buf, blocksize=1024)
        elif self._type == EncryptionType.XOR:
            dec = DocumentXOR.decrypt(self._key, encrypted_buf, plain_buf, [], 10)
        else:
            raise DecryptionError(F'Unsupported encryption type: {self._type}')

        for c in plain_buf:
            if c in (-1, -2):
                dec.seek(1, 1)
            else:
                dec.write_byte(c)
        dec.seek(0)

        out = bytearray(self._data)
        ole = OleFile(out)
        ole.write_stream('Workbook', dec.read())
        outfile.write(out)

    def is_encrypted(self) -> bool:
        stream = _BIFFStream(self._ole.openstream('Workbook'))
        num = unpack('<H', stream._data.read(2))[0]
        if num != _BIFF_BOF:
            return False
        size = unpack('<H', stream._data.read(2))[0]
        stream._data.read(size)
        return stream.has_record(_BIFF_FILEPASS)


def _parse_record_header(data: bytes | bytearray | memoryview) -> RecordHeader:
    buf = unpack_from('<H', data, 0)[0]
    return RecordHeader(
        rec_ver=buf & 0xF,
        rec_instance=(buf >> 4) & 0xFFF,
        rec_type=unpack_from('<H', data, 2)[0],
        rec_len=unpack_from('<I', data, 4)[0],
    )


def _pack_record_header(rh: RecordHeader) -> bytearray:
    buf = (rh.rec_ver & 0xF) | ((rh.rec_instance & 0xFFF) << 4)
    return bytearray(pack('<HHI', buf, rh.rec_type, rh.rec_len))


def _parse_current_user_atom(blob: io.IOBase) -> CurrentUserAtom:
    rh = _parse_record_header(blob.read(8))
    size = unpack('<I', blob.read(4))[0]
    header_token = unpack('<I', blob.read(4))[0]
    offset_to_current_edit = unpack('<I', blob.read(4))[0]
    len_user_name = unpack('<H', blob.read(2))[0]
    doc_file_version = unpack('<H', blob.read(2))[0]
    major_version, minor_version = unpack('<BB', blob.read(2))
    unused = blob.read(2)
    ansi_user_name = blob.read(len_user_name)
    rel_version_raw = blob.read(4)
    rel_version = unpack('<I', rel_version_raw)[0] if len(rel_version_raw) == 4 else 0
    unicode_user_name = blob.read(2 * len_user_name)
    return CurrentUserAtom(
        rh=rh,
        size=size,
        header_token=header_token,
        offset_to_current_edit=offset_to_current_edit,
        len_user_name=len_user_name,
        doc_file_version=doc_file_version,
        major_version=major_version,
        minor_version=minor_version,
        unused=unused,
        ansi_user_name=ansi_user_name,
        rel_version=rel_version,
        unicode_user_name=unicode_user_name,
    )


def _pack_current_user_atom(cu: CurrentUserAtom) -> bytearray:
    out = bytearray()
    out += _pack_record_header(cu.rh)
    out += pack('<I', cu.size)
    out += pack('<I', cu.header_token)
    out += pack('<I', cu.offset_to_current_edit)
    out += pack('<H', cu.len_user_name)
    out += pack('<H', cu.doc_file_version)
    out += pack('<BB', cu.major_version, cu.minor_version)
    out += cu.unused
    out += cu.ansi_user_name
    out += pack('<I', cu.rel_version)
    out += cu.unicode_user_name
    return out


def _parse_user_edit_atom(blob: io.IOBase) -> UserEditAtom:
    rh = _parse_record_header(blob.read(8))
    last_slide_id = unpack('<I', blob.read(4))[0]
    version = unpack('<H', blob.read(2))[0]
    minor_ver, major_ver = unpack('<BB', blob.read(2))
    offset_last_edit = unpack('<I', blob.read(4))[0]
    offset_persist_dir = unpack('<I', blob.read(4))[0]
    doc_persist_id = unpack('<I', blob.read(4))[0]
    persist_id_seed = unpack('<I', blob.read(4))[0]
    last_view = unpack('<H', blob.read(2))[0]
    unused = blob.read(2)
    buf = blob.read(4)
    encrypt_session_ref = unpack('<I', buf)[0] if len(buf) == 4 else None
    return UserEditAtom(
        rh=rh,
        last_slide_id_ref=last_slide_id,
        version=version,
        minor_version=minor_ver,
        major_version=major_ver,
        offset_last_edit=offset_last_edit,
        offset_persist_directory=offset_persist_dir,
        doc_persist_id_ref=doc_persist_id,
        persist_id_seed=persist_id_seed,
        last_view=last_view,
        unused=unused,
        encrypt_session_persist_id_ref=encrypt_session_ref,
    )


def _pack_user_edit_atom(uea: UserEditAtom) -> bytearray:
    out = bytearray()
    out += _pack_record_header(uea.rh)
    out += pack('<I', uea.last_slide_id_ref)
    out += pack('<H', uea.version)
    out += pack('<BB', uea.minor_version, uea.major_version)
    out += pack('<I', uea.offset_last_edit)
    out += pack('<I', uea.offset_persist_directory)
    out += pack('<I', uea.doc_persist_id_ref)
    out += pack('<I', uea.persist_id_seed)
    out += pack('<H', uea.last_view)
    out += uea.unused
    if uea.encrypt_session_persist_id_ref is not None:
        out += pack('<I', uea.encrypt_session_persist_id_ref)
    return out


def _parse_persist_directory_entry(blob: io.IOBase) -> PersistDirectoryEntry:
    buf = unpack('<I', blob.read(4))[0]
    persist_id = buf & 0xFFFFF
    c_persist = (buf >> 20) & 0xFFF
    offsets = [unpack('<I', blob.read(4))[0] for _ in range(c_persist)]
    return PersistDirectoryEntry(
        persist_id=persist_id, c_persist=c_persist, rg_persist_offset=offsets)


def _pack_persist_directory_entry(entry: PersistDirectoryEntry) -> bytearray:
    buf = (entry.persist_id & 0xFFFFF) | ((entry.c_persist & 0xFFF) << 20)
    out = bytearray(pack('<I', buf))
    for v in entry.rg_persist_offset:
        out += pack('<I', v)
    return out


def _parse_persist_directory_atom(blob: io.IOBase) -> PersistDirectoryAtom:
    rh = _parse_record_header(blob.read(8))
    raw = MemoryFile(blob.read(rh.rec_len))
    entries = []
    pos = 0
    while pos < rh.rec_len:
        entry = _parse_persist_directory_entry(raw)
        entry_size = 4 + 4 * len(entry.rg_persist_offset)
        entries.append(entry)
        pos += entry_size
    return PersistDirectoryAtom(rh=rh, rg_persist_dir_entry=entries)


def _pack_persist_directory_atom(pda: PersistDirectoryAtom) -> bytearray:
    out = bytearray(_pack_record_header(pda.rh))
    for entry in pda.rg_persist_dir_entry:
        out += _pack_persist_directory_entry(entry)
    return out


def _construct_persist_object_directory(
    cu_stream: io.IOBase, ppt_stream: io.IOBase
) -> dict[int, int]:
    cu_stream.seek(0)
    cu = _parse_current_user_atom(cu_stream)
    ppt_stream.seek(cu.offset_to_current_edit)

    pda_stack = []
    for _ in range(1):
        uea = _parse_user_edit_atom(ppt_stream)
        ppt_stream.seek(uea.offset_persist_directory)
        pda = _parse_persist_directory_atom(ppt_stream)
        pda_stack.append(pda)
        if uea.offset_last_edit == 0:
            break
        ppt_stream.seek(uea.offset_last_edit)

    directory: dict[int, int] = {}
    while pda_stack:
        pda = pda_stack.pop()
        for entry in pda.rg_persist_dir_entry:
            for i, offset in enumerate(entry.rg_persist_offset):
                directory[entry.persist_id + i] = offset
    return directory


class Ppt97File:

    def __init__(self, file: io.IOBase):
        file.seek(0)
        self._data = file.read()
        self._ole = OleFile(self._data)
        self._key: str | None = None
        self._salt: bytes | None = None
        self._key_size: int = 0
        self._type: EncryptionType | None = None

    def load_key(self, password: str | None = None):
        if not password:
            raise DecryptionError('No password specified')
        cu_stream = self._ole.openstream('Current User')
        ppt_stream = self._ole.openstream('PowerPoint Document')

        pod = _construct_persist_object_directory(cu_stream, ppt_stream)

        cu_stream.seek(0)
        cu = _parse_current_user_atom(cu_stream)
        ppt_stream.seek(cu.offset_to_current_edit)
        uea = _parse_user_edit_atom(ppt_stream)

        if uea.encrypt_session_persist_id_ref is None:
            raise DecryptionError('PowerPoint file has no encryption session reference')
        crypt_offset = pod[uea.encrypt_session_persist_id_ref]
        ppt_stream.seek(crypt_offset)
        rh = _parse_record_header(ppt_stream.read(8))
        crypt_data = ppt_stream.read(rh.rec_len)

        enc_info = MemoryFile(crypt_data)
        v_major, v_minor = unpack('<HH', enc_info.read(4))
        if not (v_major in (2, 3, 4) and v_minor == 2):
            raise DecryptionError('Unsupported PPT encryption version')

        info = _parse_header_rc4_cryptoapi(enc_info)
        if DocumentRC4CryptoAPI.verifypw(
            password, info.salt, info.key_size,
            info.encrypted_verifier, info.encrypted_verifier_hash,
        ):
            self._type = EncryptionType.RC4_CRYPTOAPI
            self._key = password
            self._salt = info.salt
            self._key_size = info.key_size
        else:
            raise InvalidKeyError('Failed to verify password')

    def _require_key(self) -> tuple[str, bytes]:
        if self._key is None or self._salt is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        return self._key, self._salt

    def decrypt(self, outfile: io.IOBase):
        key, salt = self._require_key()
        cu_stream = self._ole.openstream('Current User')
        ppt_stream = self._ole.openstream('PowerPoint Document')

        cu_stream.seek(0)
        cu_atom = _parse_current_user_atom(cu_stream)
        cu_atom_new = cu_atom._replace(header_token=0xE391C05F)
        cu_buf = _pack_current_user_atom(cu_atom_new)

        ppt_stream.seek(0)
        dec_ba = bytearray(ppt_stream.read())

        ppt_stream.seek(cu_atom.offset_to_current_edit)
        uea = _parse_user_edit_atom(ppt_stream)

        rh_new = uea.rh._replace(rec_len=uea.rh.rec_len - 4)
        uea_new = uea._replace(rh=rh_new, encrypt_session_persist_id_ref=0x00000000)
        uea_bytes = bytearray(_pack_user_edit_atom(uea_new))
        offset = cu_atom.offset_to_current_edit
        dec_ba[offset:offset + len(uea_bytes)] = uea_bytes

        ppt_stream.seek(cu_atom.offset_to_current_edit)
        uea = _parse_user_edit_atom(ppt_stream)
        ppt_stream.seek(uea.offset_persist_directory)
        pda = _parse_persist_directory_atom(ppt_stream)

        first = pda.rg_persist_dir_entry[0]._replace(
            c_persist=pda.rg_persist_dir_entry[0].c_persist - 1)
        pda_new = pda._replace(rg_persist_dir_entry=[first])
        pda_bytes = bytearray(_pack_persist_directory_atom(pda_new))
        offset = uea.offset_persist_directory
        dec_ba[offset:offset + len(pda_bytes)] = pda_bytes

        ppt_stream.seek(0)
        pod = _construct_persist_object_directory(
            MemoryFile(cu_buf), MemoryFile(bytearray(ppt_stream.read())))
        directory_items = list(pod.items())

        for i, (persist_id, offset) in enumerate(directory_items):
            mv = memoryview(dec_ba)
            rh = _parse_record_header(mv[offset:offset + 8])
            if rh.rec_type == 0x2F14:
                dec_ba[offset:offset + 8 + rh.rec_len] = b'\x00' * (8 + rh.rec_len)
                continue
            if rh.rec_type in (0x0FF5, 0x1772):
                continue
            rec_len = directory_items[i + 1][1] - offset - 8
            enc_buf = MemoryFile(mv[offset:offset + 8 + rec_len])
            blocksize = self._key_size * ((8 + rec_len) // self._key_size + 1)
            dec = DocumentRC4CryptoAPI.decrypt(
                key, salt, self._key_size,
                enc_buf, blocksize=blocksize, block=persist_id)
            dec_bytes = bytearray(dec.read())
            dec_ba[offset:offset + len(dec_bytes)] = dec_bytes

        out = bytearray(self._data)
        ole = OleFile(out)
        cu_stream_size = ole.get_size('Current User')
        cu_padded = bytearray(cu_buf)
        if len(cu_padded) < cu_stream_size:
            cu_padded += b'\x00' * (cu_stream_size - len(cu_padded))
        ole.write_stream('Current User', cu_padded)
        ole.write_stream('PowerPoint Document', bytes(dec_ba))
        outfile.write(out)

    def is_encrypted(self) -> bool:
        cu_stream = self._ole.openstream('Current User')
        cu_stream.seek(0)
        cu = _parse_current_user_atom(cu_stream)
        ppt_stream = self._ole.openstream('PowerPoint Document')
        ppt_stream.seek(cu.offset_to_current_edit)
        uea = _parse_user_edit_atom(ppt_stream)
        return uea.rh.rec_len == 0x20


def OfficeFile(file: io.IOBase):
    """
    Detect the format of an Office file and return the appropriate handler.
    """
    file.seek(0)
    data = file.read()
    file.seek(0)

    if is_ole_file(data):
        ole = OleFile(data)
        if ole.exists('EncryptionInfo'):
            return OOXMLFile(file)
        if ole.exists('wordDocument'):
            return Doc97File(file)
        if ole.exists('Workbook'):
            return Xls97File(file)
        if ole.exists('PowerPoint Document'):
            return Ppt97File(file)
        raise FileFormatError('Unrecognized OLE file format')
    elif zipfile.is_zipfile(file):
        return OOXMLFile(file)
    else:
        raise FileFormatError('Unsupported file format')

Functions

def OfficeFile(file)

Detect the format of an Office file and return the appropriate handler.

Expand source code Browse git
def OfficeFile(file: io.IOBase):
    """
    Detect the format of an Office file and return the appropriate handler.
    """
    file.seek(0)
    data = file.read()
    file.seek(0)

    if is_ole_file(data):
        ole = OleFile(data)
        if ole.exists('EncryptionInfo'):
            return OOXMLFile(file)
        if ole.exists('wordDocument'):
            return Doc97File(file)
        if ole.exists('Workbook'):
            return Xls97File(file)
        if ole.exists('PowerPoint Document'):
            return Ppt97File(file)
        raise FileFormatError('Unrecognized OLE file format')
    elif zipfile.is_zipfile(file):
        return OOXMLFile(file)
    else:
        raise FileFormatError('Unsupported file format')

Classes

class FileFormatError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code Browse git
class FileFormatError(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException
class DecryptionError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code Browse git
class DecryptionError(Exception):
    pass

Ancestors

  • builtins.Exception
  • builtins.BaseException

Subclasses

class InvalidKeyError (*args, **kwargs)

Common base class for all non-exit exceptions.

Expand source code Browse git
class InvalidKeyError(DecryptionError):
    pass

Ancestors

class EncryptionType (*args, **kwds)

Create a collection of name/value pairs.

Example enumeration:

>>> class Color(Enum):
...     RED = 1
...     BLUE = 2
...     GREEN = 3

Access them by:

  • attribute access:

Color.RED

  • value lookup:

Color(1)

  • name lookup:

Color['RED']

Enumerations can be iterated over, and know how many members they have:

>>> len(Color)
3
>>> list(Color)
[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]

Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.

Expand source code Browse git
class EncryptionType(enum.Enum):
    PLAIN         = 'plain'          # noqa: E221
    AGILE         = 'agile'          # noqa: E221
    STANDARD      = 'standard'       # noqa: E221
    RC4           = 'rc4'            # noqa: E221
    RC4_CRYPTOAPI = 'rc4_cryptoapi'
    XOR           = 'xor'            # noqa: E221

Ancestors

  • enum.Enum

Class variables

var PLAIN

The type of the None singleton.

var AGILE

The type of the None singleton.

var STANDARD

The type of the None singleton.

var RC4

The type of the None singleton.

var RC4_CRYPTOAPI

The type of the None singleton.

var XOR

The type of the None singleton.

class EncryptionHeader (flags, size_extra, alg_id, alg_id_hash, key_size, provider_type, csp_name)

EncryptionHeader(flags, size_extra, alg_id, alg_id_hash, key_size, provider_type, csp_name)

Expand source code Browse git
class EncryptionHeader(NamedTuple):
    flags: int
    size_extra: int
    alg_id: int
    alg_id_hash: int
    key_size: int
    provider_type: int
    csp_name: str

Ancestors

  • builtins.tuple

Instance variables

var flags

Alias for field number 0

Expand source code Browse git
class EncryptionHeader(NamedTuple):
    flags: int
    size_extra: int
    alg_id: int
    alg_id_hash: int
    key_size: int
    provider_type: int
    csp_name: str
var size_extra

Alias for field number 1

Expand source code Browse git
class EncryptionHeader(NamedTuple):
    flags: int
    size_extra: int
    alg_id: int
    alg_id_hash: int
    key_size: int
    provider_type: int
    csp_name: str
var alg_id

Alias for field number 2

Expand source code Browse git
class EncryptionHeader(NamedTuple):
    flags: int
    size_extra: int
    alg_id: int
    alg_id_hash: int
    key_size: int
    provider_type: int
    csp_name: str
var alg_id_hash

Alias for field number 3

Expand source code Browse git
class EncryptionHeader(NamedTuple):
    flags: int
    size_extra: int
    alg_id: int
    alg_id_hash: int
    key_size: int
    provider_type: int
    csp_name: str
var key_size

Alias for field number 4

Expand source code Browse git
class EncryptionHeader(NamedTuple):
    flags: int
    size_extra: int
    alg_id: int
    alg_id_hash: int
    key_size: int
    provider_type: int
    csp_name: str
var provider_type

Alias for field number 5

Expand source code Browse git
class EncryptionHeader(NamedTuple):
    flags: int
    size_extra: int
    alg_id: int
    alg_id_hash: int
    key_size: int
    provider_type: int
    csp_name: str
var csp_name

Alias for field number 6

Expand source code Browse git
class EncryptionHeader(NamedTuple):
    flags: int
    size_extra: int
    alg_id: int
    alg_id_hash: int
    key_size: int
    provider_type: int
    csp_name: str
class EncryptionVerifier (salt_size, salt, encrypted_verifier, verifier_hash_size, encrypted_verifier_hash)

EncryptionVerifier(salt_size, salt, encrypted_verifier, verifier_hash_size, encrypted_verifier_hash)

Expand source code Browse git
class EncryptionVerifier(NamedTuple):
    salt_size: int
    salt: bytes
    encrypted_verifier: bytes
    verifier_hash_size: int
    encrypted_verifier_hash: bytes

Ancestors

  • builtins.tuple

Instance variables

var salt_size

Alias for field number 0

Expand source code Browse git
class EncryptionVerifier(NamedTuple):
    salt_size: int
    salt: bytes
    encrypted_verifier: bytes
    verifier_hash_size: int
    encrypted_verifier_hash: bytes
var salt

Alias for field number 1

Expand source code Browse git
class EncryptionVerifier(NamedTuple):
    salt_size: int
    salt: bytes
    encrypted_verifier: bytes
    verifier_hash_size: int
    encrypted_verifier_hash: bytes
var encrypted_verifier

Alias for field number 2

Expand source code Browse git
class EncryptionVerifier(NamedTuple):
    salt_size: int
    salt: bytes
    encrypted_verifier: bytes
    verifier_hash_size: int
    encrypted_verifier_hash: bytes
var verifier_hash_size

Alias for field number 3

Expand source code Browse git
class EncryptionVerifier(NamedTuple):
    salt_size: int
    salt: bytes
    encrypted_verifier: bytes
    verifier_hash_size: int
    encrypted_verifier_hash: bytes
var encrypted_verifier_hash

Alias for field number 4

Expand source code Browse git
class EncryptionVerifier(NamedTuple):
    salt_size: int
    salt: bytes
    encrypted_verifier: bytes
    verifier_hash_size: int
    encrypted_verifier_hash: bytes
class RC4CryptoAPIInfo (salt, key_size, encrypted_verifier, encrypted_verifier_hash)

RC4CryptoAPIInfo(salt, key_size, encrypted_verifier, encrypted_verifier_hash)

Expand source code Browse git
class RC4CryptoAPIInfo(NamedTuple):
    salt: bytes
    key_size: int
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes

Ancestors

  • builtins.tuple

Instance variables

var salt

Alias for field number 0

Expand source code Browse git
class RC4CryptoAPIInfo(NamedTuple):
    salt: bytes
    key_size: int
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes
var key_size

Alias for field number 1

Expand source code Browse git
class RC4CryptoAPIInfo(NamedTuple):
    salt: bytes
    key_size: int
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes
var encrypted_verifier

Alias for field number 2

Expand source code Browse git
class RC4CryptoAPIInfo(NamedTuple):
    salt: bytes
    key_size: int
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes
var encrypted_verifier_hash

Alias for field number 3

Expand source code Browse git
class RC4CryptoAPIInfo(NamedTuple):
    salt: bytes
    key_size: int
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes
class RC4Info (salt, encrypted_verifier, encrypted_verifier_hash)

RC4Info(salt, encrypted_verifier, encrypted_verifier_hash)

Expand source code Browse git
class RC4Info(NamedTuple):
    salt: bytes
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes

Ancestors

  • builtins.tuple

Instance variables

var salt

Alias for field number 0

Expand source code Browse git
class RC4Info(NamedTuple):
    salt: bytes
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes
var encrypted_verifier

Alias for field number 1

Expand source code Browse git
class RC4Info(NamedTuple):
    salt: bytes
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes
var encrypted_verifier_hash

Alias for field number 2

Expand source code Browse git
class RC4Info(NamedTuple):
    salt: bytes
    encrypted_verifier: bytes
    encrypted_verifier_hash: bytes
class StandardEncryptionInfo (header, verifier)

StandardEncryptionInfo(header, verifier)

Expand source code Browse git
class StandardEncryptionInfo(NamedTuple):
    header: EncryptionHeader
    verifier: EncryptionVerifier

Ancestors

  • builtins.tuple

Instance variables

var header

Alias for field number 0

Expand source code Browse git
class StandardEncryptionInfo(NamedTuple):
    header: EncryptionHeader
    verifier: EncryptionVerifier
var verifier

Alias for field number 1

Expand source code Browse git
class StandardEncryptionInfo(NamedTuple):
    header: EncryptionHeader
    verifier: EncryptionVerifier
class AgileEncryptionInfo (key_data_salt, key_data_hash_algorithm, key_data_block_size, encrypted_hmac_key, encrypted_hmac_value, encrypted_verifier_hash_input, encrypted_verifier_hash_value, encrypted_key_value, spin_value, password_salt, password_hash_algorithm, password_key_bits)

AgileEncryptionInfo(key_data_salt, key_data_hash_algorithm, key_data_block_size, encrypted_hmac_key, encrypted_hmac_value, encrypted_verifier_hash_input, encrypted_verifier_hash_value, encrypted_key_value, spin_value, password_salt, password_hash_algorithm, password_key_bits)

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int

Ancestors

  • builtins.tuple

Instance variables

var key_data_salt

Alias for field number 0

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var key_data_hash_algorithm

Alias for field number 1

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var key_data_block_size

Alias for field number 2

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var encrypted_hmac_key

Alias for field number 3

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var encrypted_hmac_value

Alias for field number 4

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var encrypted_verifier_hash_input

Alias for field number 5

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var encrypted_verifier_hash_value

Alias for field number 6

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var encrypted_key_value

Alias for field number 7

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var spin_value

Alias for field number 8

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var password_salt

Alias for field number 9

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var password_hash_algorithm

Alias for field number 10

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
var password_key_bits

Alias for field number 11

Expand source code Browse git
class AgileEncryptionInfo(NamedTuple):
    key_data_salt: bytes
    key_data_hash_algorithm: str
    key_data_block_size: int
    encrypted_hmac_key: bytes
    encrypted_hmac_value: bytes
    encrypted_verifier_hash_input: bytes
    encrypted_verifier_hash_value: bytes
    encrypted_key_value: bytes
    spin_value: int
    password_salt: bytes
    password_hash_algorithm: str
    password_key_bits: int
class FibBase (w_ident, n_fib, f_encrypted, f_which_tbl_stm, f_obfuscation, i_key)

FibBase(w_ident, n_fib, f_encrypted, f_which_tbl_stm, f_obfuscation, i_key)

Expand source code Browse git
class FibBase(NamedTuple):
    w_ident: int
    n_fib: int
    f_encrypted: int
    f_which_tbl_stm: int
    f_obfuscation: int
    i_key: int

Ancestors

  • builtins.tuple

Instance variables

var w_ident

Alias for field number 0

Expand source code Browse git
class FibBase(NamedTuple):
    w_ident: int
    n_fib: int
    f_encrypted: int
    f_which_tbl_stm: int
    f_obfuscation: int
    i_key: int
var n_fib

Alias for field number 1

Expand source code Browse git
class FibBase(NamedTuple):
    w_ident: int
    n_fib: int
    f_encrypted: int
    f_which_tbl_stm: int
    f_obfuscation: int
    i_key: int
var f_encrypted

Alias for field number 2

Expand source code Browse git
class FibBase(NamedTuple):
    w_ident: int
    n_fib: int
    f_encrypted: int
    f_which_tbl_stm: int
    f_obfuscation: int
    i_key: int
var f_which_tbl_stm

Alias for field number 3

Expand source code Browse git
class FibBase(NamedTuple):
    w_ident: int
    n_fib: int
    f_encrypted: int
    f_which_tbl_stm: int
    f_obfuscation: int
    i_key: int
var f_obfuscation

Alias for field number 4

Expand source code Browse git
class FibBase(NamedTuple):
    w_ident: int
    n_fib: int
    f_encrypted: int
    f_which_tbl_stm: int
    f_obfuscation: int
    i_key: int
var i_key

Alias for field number 5

Expand source code Browse git
class FibBase(NamedTuple):
    w_ident: int
    n_fib: int
    f_encrypted: int
    f_which_tbl_stm: int
    f_obfuscation: int
    i_key: int
class RecordHeader (rec_ver, rec_instance, rec_type, rec_len)

RecordHeader(rec_ver, rec_instance, rec_type, rec_len)

Expand source code Browse git
class RecordHeader(NamedTuple):
    rec_ver: int
    rec_instance: int
    rec_type: int
    rec_len: int

Ancestors

  • builtins.tuple

Instance variables

var rec_ver

Alias for field number 0

Expand source code Browse git
class RecordHeader(NamedTuple):
    rec_ver: int
    rec_instance: int
    rec_type: int
    rec_len: int
var rec_instance

Alias for field number 1

Expand source code Browse git
class RecordHeader(NamedTuple):
    rec_ver: int
    rec_instance: int
    rec_type: int
    rec_len: int
var rec_type

Alias for field number 2

Expand source code Browse git
class RecordHeader(NamedTuple):
    rec_ver: int
    rec_instance: int
    rec_type: int
    rec_len: int
var rec_len

Alias for field number 3

Expand source code Browse git
class RecordHeader(NamedTuple):
    rec_ver: int
    rec_instance: int
    rec_type: int
    rec_len: int
class CurrentUserAtom (rh, size, header_token, offset_to_current_edit, len_user_name, doc_file_version, major_version, minor_version, unused, ansi_user_name, rel_version, unicode_user_name)

CurrentUserAtom(rh, size, header_token, offset_to_current_edit, len_user_name, doc_file_version, major_version, minor_version, unused, ansi_user_name, rel_version, unicode_user_name)

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes

Ancestors

  • builtins.tuple

Instance variables

var rh

Alias for field number 0

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var size

Alias for field number 1

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var header_token

Alias for field number 2

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var offset_to_current_edit

Alias for field number 3

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var len_user_name

Alias for field number 4

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var doc_file_version

Alias for field number 5

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var major_version

Alias for field number 6

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var minor_version

Alias for field number 7

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var unused

Alias for field number 8

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var ansi_user_name

Alias for field number 9

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var rel_version

Alias for field number 10

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
var unicode_user_name

Alias for field number 11

Expand source code Browse git
class CurrentUserAtom(NamedTuple):
    rh: RecordHeader
    size: int
    header_token: int
    offset_to_current_edit: int
    len_user_name: int
    doc_file_version: int
    major_version: int
    minor_version: int
    unused: bytes
    ansi_user_name: bytes
    rel_version: int
    unicode_user_name: bytes
class UserEditAtom (rh, last_slide_id_ref, version, minor_version, major_version, offset_last_edit, offset_persist_directory, doc_persist_id_ref, persist_id_seed, last_view, unused, encrypt_session_persist_id_ref)

UserEditAtom(rh, last_slide_id_ref, version, minor_version, major_version, offset_last_edit, offset_persist_directory, doc_persist_id_ref, persist_id_seed, last_view, unused, encrypt_session_persist_id_ref)

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None

Ancestors

  • builtins.tuple

Instance variables

var rh

Alias for field number 0

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var last_slide_id_ref

Alias for field number 1

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var version

Alias for field number 2

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var minor_version

Alias for field number 3

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var major_version

Alias for field number 4

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var offset_last_edit

Alias for field number 5

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var offset_persist_directory

Alias for field number 6

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var doc_persist_id_ref

Alias for field number 7

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var persist_id_seed

Alias for field number 8

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var last_view

Alias for field number 9

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var unused

Alias for field number 10

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
var encrypt_session_persist_id_ref

Alias for field number 11

Expand source code Browse git
class UserEditAtom(NamedTuple):
    rh: RecordHeader
    last_slide_id_ref: int
    version: int
    minor_version: int
    major_version: int
    offset_last_edit: int
    offset_persist_directory: int
    doc_persist_id_ref: int
    persist_id_seed: int
    last_view: int
    unused: bytes
    encrypt_session_persist_id_ref: int | None
class PersistDirectoryEntry (persist_id, c_persist, rg_persist_offset)

PersistDirectoryEntry(persist_id, c_persist, rg_persist_offset)

Expand source code Browse git
class PersistDirectoryEntry(NamedTuple):
    persist_id: int
    c_persist: int
    rg_persist_offset: list[int]

Ancestors

  • builtins.tuple

Instance variables

var persist_id

Alias for field number 0

Expand source code Browse git
class PersistDirectoryEntry(NamedTuple):
    persist_id: int
    c_persist: int
    rg_persist_offset: list[int]
var c_persist

Alias for field number 1

Expand source code Browse git
class PersistDirectoryEntry(NamedTuple):
    persist_id: int
    c_persist: int
    rg_persist_offset: list[int]
var rg_persist_offset

Alias for field number 2

Expand source code Browse git
class PersistDirectoryEntry(NamedTuple):
    persist_id: int
    c_persist: int
    rg_persist_offset: list[int]
class PersistDirectoryAtom (rh, rg_persist_dir_entry)

PersistDirectoryAtom(rh, rg_persist_dir_entry)

Expand source code Browse git
class PersistDirectoryAtom(NamedTuple):
    rh: RecordHeader
    rg_persist_dir_entry: list[PersistDirectoryEntry]

Ancestors

  • builtins.tuple

Instance variables

var rh

Alias for field number 0

Expand source code Browse git
class PersistDirectoryAtom(NamedTuple):
    rh: RecordHeader
    rg_persist_dir_entry: list[PersistDirectoryEntry]
var rg_persist_dir_entry

Alias for field number 1

Expand source code Browse git
class PersistDirectoryAtom(NamedTuple):
    rh: RecordHeader
    rg_persist_dir_entry: list[PersistDirectoryEntry]
class DocumentRC4
Expand source code Browse git
class DocumentRC4:

    @staticmethod
    def verifypw(
        password: str,
        salt: bytes,
        encrypted_verifier: bytes,
        encrypted_verifier_hash: bytes,
    ) -> bool:
        key = _rc4_makekey(password, salt, 0)
        dec = ARC4.new(key)
        verifier = dec.decrypt(encrypted_verifier)
        verifier_hash = dec.decrypt(encrypted_verifier_hash)
        return md5(verifier).digest() == verifier_hash

    @staticmethod
    def decrypt(
        password: str,
        salt: bytes,
        ibuf: io.IOBase,
        blocksize: int = 0x200,
    ) -> MemoryFile[bytearray]:
        obuf = MemoryFile()
        block = 0
        key = _rc4_makekey(password, salt, block)
        for buf in iter(functools.partial(ibuf.read, blocksize), b''):
            obuf.write(ARC4.new(key).decrypt(buf))
            block += 1
            key = _rc4_makekey(password, salt, block)
        obuf.seek(0)
        return obuf

Static methods

def verifypw(password, salt, encrypted_verifier, encrypted_verifier_hash)
Expand source code Browse git
@staticmethod
def verifypw(
    password: str,
    salt: bytes,
    encrypted_verifier: bytes,
    encrypted_verifier_hash: bytes,
) -> bool:
    key = _rc4_makekey(password, salt, 0)
    dec = ARC4.new(key)
    verifier = dec.decrypt(encrypted_verifier)
    verifier_hash = dec.decrypt(encrypted_verifier_hash)
    return md5(verifier).digest() == verifier_hash
def decrypt(password, salt, ibuf, blocksize=512)
Expand source code Browse git
@staticmethod
def decrypt(
    password: str,
    salt: bytes,
    ibuf: io.IOBase,
    blocksize: int = 0x200,
) -> MemoryFile[bytearray]:
    obuf = MemoryFile()
    block = 0
    key = _rc4_makekey(password, salt, block)
    for buf in iter(functools.partial(ibuf.read, blocksize), b''):
        obuf.write(ARC4.new(key).decrypt(buf))
        block += 1
        key = _rc4_makekey(password, salt, block)
    obuf.seek(0)
    return obuf
class DocumentRC4CryptoAPI
Expand source code Browse git
class DocumentRC4CryptoAPI:

    @staticmethod
    def verifypw(
        password: str,
        salt: bytes,
        key_size: int,
        encrypted_verifier: bytes,
        encrypted_verifier_hash: bytes,
    ) -> bool:
        key = _rc4api_makekey(password, salt, key_size, 0)
        dec = ARC4.new(key)
        verifier = dec.decrypt(encrypted_verifier)
        verifier_hash = dec.decrypt(encrypted_verifier_hash)
        return sha1(verifier).digest() == verifier_hash

    @staticmethod
    def decrypt(
        password: str,
        salt: bytes,
        key_size: int,
        ibuf: io.IOBase,
        blocksize: int = 0x200,
        block: int = 0,
    ) -> MemoryFile[bytearray]:
        obuf = MemoryFile()
        key = _rc4api_makekey(password, salt, key_size, block)
        for buf in iter(functools.partial(ibuf.read, blocksize), b''):
            obuf.write(ARC4.new(key).decrypt(buf))
            block += 1
            key = _rc4api_makekey(password, salt, key_size, block)
        obuf.seek(0)
        return obuf

Static methods

def verifypw(password, salt, key_size, encrypted_verifier, encrypted_verifier_hash)
Expand source code Browse git
@staticmethod
def verifypw(
    password: str,
    salt: bytes,
    key_size: int,
    encrypted_verifier: bytes,
    encrypted_verifier_hash: bytes,
) -> bool:
    key = _rc4api_makekey(password, salt, key_size, 0)
    dec = ARC4.new(key)
    verifier = dec.decrypt(encrypted_verifier)
    verifier_hash = dec.decrypt(encrypted_verifier_hash)
    return sha1(verifier).digest() == verifier_hash
def decrypt(password, salt, key_size, ibuf, blocksize=512, block=0)
Expand source code Browse git
@staticmethod
def decrypt(
    password: str,
    salt: bytes,
    key_size: int,
    ibuf: io.IOBase,
    blocksize: int = 0x200,
    block: int = 0,
) -> MemoryFile[bytearray]:
    obuf = MemoryFile()
    key = _rc4api_makekey(password, salt, key_size, block)
    for buf in iter(functools.partial(ibuf.read, blocksize), b''):
        obuf.write(ARC4.new(key).decrypt(buf))
        block += 1
        key = _rc4api_makekey(password, salt, key_size, block)
    obuf.seek(0)
    return obuf
class DocumentXOR
Expand source code Browse git
class DocumentXOR:

    _PAD = [
        0xBB, 0xFF, 0xFF, 0xBA, 0xFF, 0xFF, 0xB9, 0x80,
        0x00, 0xBE, 0x0F, 0x00, 0xBF, 0x0F, 0x00,
    ]
    _INITIAL_CODE = [
        0xE1F0, 0x1D0F, 0xCC9C, 0x84C0, 0x110C, 0x0E10, 0xF1CE, 0x313E,
        0x1872, 0xE139, 0xD40F, 0x84F9, 0x280C, 0xA96A, 0x4EC3,
    ]
    _XOR_MATRIX = [
        0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09,
        0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF,
        0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0,
        0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40,
        0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5,
        0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A,
        0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9,
        0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0,
        0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC,
        0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10,
        0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168,
        0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C,
        0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD,
        0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC,
        0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4,
    ]

    @staticmethod
    def _ror(n: int, rotations: int, width: int) -> int:
        return (2 ** width - 1) & (n >> rotations | n << (width - rotations))

    @staticmethod
    def _xor_ror(byte1: int, byte2: int) -> int:
        return DocumentXOR._ror(byte1 ^ byte2, 1, 8)

    @staticmethod
    def verifypw(password: str, verification_bytes: int) -> bool:
        verifier = 0
        pw_arr = [len(password)] + [ord(ch) for ch in password]
        pw_arr.reverse()
        for b in pw_arr:
            if verifier & 0x4000:
                intermediate = 1
            else:
                intermediate = 0
            verifier = (intermediate ^ ((verifier * 2) & 0x7FFF)) ^ b
        return (verifier ^ 0xCE4B) == verification_bytes

    @staticmethod
    def _create_xor_key(password: str) -> int:
        xor_key = DocumentXOR._INITIAL_CODE[len(password) - 1]
        element = 0x68
        for ch in reversed(password):
            c = ord(ch)
            for _ in range(7):
                if c & 0x40:
                    xor_key = (xor_key ^ DocumentXOR._XOR_MATRIX[element]) % 65536
                c = (c << 1) % 256
                element -= 1
        return xor_key

    @staticmethod
    def _create_xor_array(password: str) -> list[int]:
        xor_key = DocumentXOR._create_xor_key(password)
        index = len(password)
        obfuscation = [0] * 16

        if index % 2 == 1:
            temp = (xor_key & 0xFF00) >> 8
            obfuscation[index] = DocumentXOR._xor_ror(DocumentXOR._PAD[0], temp)
            index -= 1
            temp = xor_key & 0x00FF
            obfuscation[index] = DocumentXOR._xor_ror(ord(password[-1]), temp)

        while index > 0:
            index -= 1
            temp = (xor_key & 0xFF00) >> 8
            obfuscation[index] = DocumentXOR._xor_ror(ord(password[index]), temp)
            index -= 1
            temp = xor_key & 0x00FF
            obfuscation[index] = DocumentXOR._xor_ror(ord(password[index]), temp)

        idx = 15
        pad_idx = 15 - len(password)
        while pad_idx > 0:
            temp = (xor_key & 0xFF00) >> 8
            obfuscation[idx] = DocumentXOR._xor_ror(DocumentXOR._PAD[pad_idx], temp)
            idx -= 1
            pad_idx -= 1
            temp = xor_key & 0x00FF
            obfuscation[idx] = DocumentXOR._xor_ror(DocumentXOR._PAD[pad_idx], temp)
            idx -= 1
            pad_idx -= 1

        return obfuscation

    @staticmethod
    def decrypt(
        password: str,
        ibuf: io.IOBase,
        plaintext: list[int],
        records: list,
        base: int,
    ) -> MemoryFile[bytearray]:
        obuf = MemoryFile()
        xor_array = DocumentXOR._create_xor_array(password)
        data_index = 0
        while data_index < len(plaintext):
            count = 1
            if plaintext[data_index] in (-1, -2):
                for j in range(data_index + 1, len(plaintext)):
                    if plaintext[j] >= 0:
                        break
                    count += 1
                if plaintext[data_index] == -2:
                    xor_idx = (data_index + count + 4) % 16
                else:
                    xor_idx = (data_index + count) % 16
                for _ in range(count):
                    b = ibuf.read(1)
                    dec = DocumentXOR._ror(b[0] ^ xor_array[xor_idx], 5, 8)
                    obuf.write_byte(dec)
                    xor_idx = (xor_idx + 1) % 16
            else:
                obuf.write(ibuf.read(1))
            data_index += count
        obuf.seek(0)
        return obuf

Static methods

def verifypw(password, verification_bytes)
Expand source code Browse git
@staticmethod
def verifypw(password: str, verification_bytes: int) -> bool:
    verifier = 0
    pw_arr = [len(password)] + [ord(ch) for ch in password]
    pw_arr.reverse()
    for b in pw_arr:
        if verifier & 0x4000:
            intermediate = 1
        else:
            intermediate = 0
        verifier = (intermediate ^ ((verifier * 2) & 0x7FFF)) ^ b
    return (verifier ^ 0xCE4B) == verification_bytes
def decrypt(password, ibuf, plaintext, records, base)
Expand source code Browse git
@staticmethod
def decrypt(
    password: str,
    ibuf: io.IOBase,
    plaintext: list[int],
    records: list,
    base: int,
) -> MemoryFile[bytearray]:
    obuf = MemoryFile()
    xor_array = DocumentXOR._create_xor_array(password)
    data_index = 0
    while data_index < len(plaintext):
        count = 1
        if plaintext[data_index] in (-1, -2):
            for j in range(data_index + 1, len(plaintext)):
                if plaintext[j] >= 0:
                    break
                count += 1
            if plaintext[data_index] == -2:
                xor_idx = (data_index + count + 4) % 16
            else:
                xor_idx = (data_index + count) % 16
            for _ in range(count):
                b = ibuf.read(1)
                dec = DocumentXOR._ror(b[0] ^ xor_array[xor_idx], 5, 8)
                obuf.write_byte(dec)
                xor_idx = (xor_idx + 1) % 16
        else:
            obuf.write(ibuf.read(1))
        data_index += count
    obuf.seek(0)
    return obuf
class ECMA376Agile
Expand source code Browse git
class ECMA376Agile:

    @staticmethod
    def _derive_iterated_hash(
        password: str,
        salt: bytes,
        algorithm: str,
        spin: int,
    ):
        h = _get_hash_func(algorithm)
        hobj = h(salt + password.encode('UTF-16LE'))
        for i in range(spin):
            hobj = h(pack('<I', i) + hobj.digest())
        return hobj

    @staticmethod
    def _derive_encryption_key(
        h: bytes,
        block_key: bytes | bytearray,
        algorithm: str,
        key_bits: int,
    ) -> bytes:
        hfinal = _get_hash_func(algorithm)(h + block_key)
        return hfinal.digest()[:key_bits // 8]

    @staticmethod
    def decrypt(
        key: bytes,
        key_data_salt: bytes,
        hash_algorithm: str,
        ibuf: io.IOBase,
    ) -> bytearray:
        SEGMENT_LENGTH = 4096
        hashCalc = _get_hash_func(hash_algorithm)
        obuf = MemoryFile()
        ibuf.seek(0)
        total_size = unpack('<Q', ibuf.read(8))[0]
        remaining = total_size
        for i, buf in enumerate(
            iter(functools.partial(ibuf.read, SEGMENT_LENGTH), b'')
        ):
            iv = hashCalc(key_data_salt + pack('<I', i)).digest()[:16]
            dec = _decrypt_aes_cbc(buf, key, iv)
            if remaining < len(dec):
                dec = dec[:remaining]
            obuf.write(dec)
            remaining -= len(dec)
            if remaining <= 0:
                break
        return obuf.getvalue()

    @staticmethod
    def makekey_from_password(
        password: str,
        salt: bytes,
        hash_algorithm: str,
        encrypted_key_value: bytes,
        spin_value: int,
        key_bits: int,
    ) -> bytes:
        h = ECMA376Agile._derive_iterated_hash(password, salt, hash_algorithm, spin_value)
        enc_key = ECMA376Agile._derive_encryption_key(
            h.digest(), _BLK_KEY_ENCRYPTED_KEY_VALUE, hash_algorithm, key_bits)
        return _decrypt_aes_cbc(encrypted_key_value, enc_key, salt)

Static methods

def decrypt(key, key_data_salt, hash_algorithm, ibuf)
Expand source code Browse git
@staticmethod
def decrypt(
    key: bytes,
    key_data_salt: bytes,
    hash_algorithm: str,
    ibuf: io.IOBase,
) -> bytearray:
    SEGMENT_LENGTH = 4096
    hashCalc = _get_hash_func(hash_algorithm)
    obuf = MemoryFile()
    ibuf.seek(0)
    total_size = unpack('<Q', ibuf.read(8))[0]
    remaining = total_size
    for i, buf in enumerate(
        iter(functools.partial(ibuf.read, SEGMENT_LENGTH), b'')
    ):
        iv = hashCalc(key_data_salt + pack('<I', i)).digest()[:16]
        dec = _decrypt_aes_cbc(buf, key, iv)
        if remaining < len(dec):
            dec = dec[:remaining]
        obuf.write(dec)
        remaining -= len(dec)
        if remaining <= 0:
            break
    return obuf.getvalue()
def makekey_from_password(password, salt, hash_algorithm, encrypted_key_value, spin_value, key_bits)
Expand source code Browse git
@staticmethod
def makekey_from_password(
    password: str,
    salt: bytes,
    hash_algorithm: str,
    encrypted_key_value: bytes,
    spin_value: int,
    key_bits: int,
) -> bytes:
    h = ECMA376Agile._derive_iterated_hash(password, salt, hash_algorithm, spin_value)
    enc_key = ECMA376Agile._derive_encryption_key(
        h.digest(), _BLK_KEY_ENCRYPTED_KEY_VALUE, hash_algorithm, key_bits)
    return _decrypt_aes_cbc(encrypted_key_value, enc_key, salt)
class ECMA376Standard
Expand source code Browse git
class ECMA376Standard:

    @staticmethod
    def decrypt(key: bytes, ibuf: io.IOBase) -> bytearray:
        obuf = MemoryFile()
        total_size = unpack('<I', ibuf.read(4))[0]
        ibuf.seek(8)
        data = ibuf.read()
        pad = (AES.block_size - len(data) % AES.block_size) % AES.block_size
        if pad:
            data = data + b'\x00' * pad
        cipher = AES.new(key, AES.MODE_ECB)
        dec = cipher.decrypt(data)
        obuf.write(dec[:total_size])
        return obuf.getvalue()

    @staticmethod
    def verifykey(
        key: bytes,
        encrypted_verifier: bytes,
        encrypted_verifier_hash: bytes,
    ) -> bool:
        cipher = AES.new(key, AES.MODE_ECB)
        verifier = cipher.decrypt(encrypted_verifier)
        expected = sha1(verifier).digest()
        cipher = AES.new(key, AES.MODE_ECB)
        verifier_hash = cipher.decrypt(encrypted_verifier_hash)[:sha1().digest_size]
        return expected == verifier_hash

    @staticmethod
    def makekey_from_password(
        password: str,
        alg_id: int,
        alg_id_hash: int,
        provider_type: int,
        key_size: int,
        salt_size: int,
        salt: bytes,
    ) -> bytes:
        ITER_COUNT = 50000
        cb_hash = sha1().digest_size
        cb_key = key_size // 8

        pw = password.encode('UTF-16LE')
        h = sha1(salt + pw).digest()
        for i in range(ITER_COUNT):
            h = sha1(pack('<I', i) + h).digest()
        hfinal = sha1(h + pack('<I', 0)).digest()

        buf1 = bytearray(b'\x36' * 64)
        for i in range(cb_hash):
            buf1[i] ^= hfinal[i]
        x1 = sha1(buf1).digest()
        buf2 = bytearray(b'\x5c' * 64)
        for i in range(cb_hash):
            buf2[i] ^= hfinal[i]
        x2 = sha1(buf2).digest()
        x3 = x1 + x2
        return x3[:cb_key]

Static methods

def decrypt(key, ibuf)
Expand source code Browse git
@staticmethod
def decrypt(key: bytes, ibuf: io.IOBase) -> bytearray:
    obuf = MemoryFile()
    total_size = unpack('<I', ibuf.read(4))[0]
    ibuf.seek(8)
    data = ibuf.read()
    pad = (AES.block_size - len(data) % AES.block_size) % AES.block_size
    if pad:
        data = data + b'\x00' * pad
    cipher = AES.new(key, AES.MODE_ECB)
    dec = cipher.decrypt(data)
    obuf.write(dec[:total_size])
    return obuf.getvalue()
def verifykey(key, encrypted_verifier, encrypted_verifier_hash)
Expand source code Browse git
@staticmethod
def verifykey(
    key: bytes,
    encrypted_verifier: bytes,
    encrypted_verifier_hash: bytes,
) -> bool:
    cipher = AES.new(key, AES.MODE_ECB)
    verifier = cipher.decrypt(encrypted_verifier)
    expected = sha1(verifier).digest()
    cipher = AES.new(key, AES.MODE_ECB)
    verifier_hash = cipher.decrypt(encrypted_verifier_hash)[:sha1().digest_size]
    return expected == verifier_hash
def makekey_from_password(password, alg_id, alg_id_hash, provider_type, key_size, salt_size, salt)
Expand source code Browse git
@staticmethod
def makekey_from_password(
    password: str,
    alg_id: int,
    alg_id_hash: int,
    provider_type: int,
    key_size: int,
    salt_size: int,
    salt: bytes,
) -> bytes:
    ITER_COUNT = 50000
    cb_hash = sha1().digest_size
    cb_key = key_size // 8

    pw = password.encode('UTF-16LE')
    h = sha1(salt + pw).digest()
    for i in range(ITER_COUNT):
        h = sha1(pack('<I', i) + h).digest()
    hfinal = sha1(h + pack('<I', 0)).digest()

    buf1 = bytearray(b'\x36' * 64)
    for i in range(cb_hash):
        buf1[i] ^= hfinal[i]
    x1 = sha1(buf1).digest()
    buf2 = bytearray(b'\x5c' * 64)
    for i in range(cb_hash):
        buf2[i] ^= hfinal[i]
    x2 = sha1(buf2).digest()
    x3 = x1 + x2
    return x3[:cb_key]
class OOXMLFile (file)
Expand source code Browse git
class OOXMLFile:

    def __init__(self, file: io.IOBase):
        file.seek(0)
        data = file.read()
        file.seek(0)
        self._data = data
        self._type = EncryptionType.PLAIN
        self._info: StandardEncryptionInfo | AgileEncryptionInfo | None = None
        self._key: bytes | None = None

        if is_ole_file(data):
            ole = OleFile(data)
            self._ole = ole
            if not ole.exists('EncryptionInfo'):
                raise FileFormatError('No EncryptionInfo stream found')
            with ole.openstream('EncryptionInfo') as stream:
                self._type, self._info = _parseinfo(stream)
        elif zipfile.is_zipfile(file):
            self._type = EncryptionType.PLAIN
        else:
            raise FileFormatError('Unsupported file format')

    def _require_key(self) -> bytes:
        if self._key is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        return self._key

    def load_key(self, password: str | None = None):
        if not password:
            raise DecryptionError('No password specified')
        if self._type == EncryptionType.AGILE:
            if not isinstance(self._info, AgileEncryptionInfo):
                raise DecryptionError('Encryption info mismatch')
            self._key = ECMA376Agile.makekey_from_password(
                password,
                self._info.password_salt,
                self._info.password_hash_algorithm,
                self._info.encrypted_key_value,
                self._info.spin_value,
                self._info.password_key_bits,
            )
        elif self._type == EncryptionType.STANDARD:
            if not isinstance(self._info, StandardEncryptionInfo):
                raise DecryptionError('Encryption info mismatch')
            self._key = ECMA376Standard.makekey_from_password(
                password,
                self._info.header.alg_id,
                self._info.header.alg_id_hash,
                self._info.header.provider_type,
                self._info.header.key_size,
                self._info.verifier.salt_size,
                self._info.verifier.salt,
            )

    def _decrypt_package(self) -> bytearray:
        key = self._require_key()
        if self._type == EncryptionType.AGILE:
            if not isinstance(self._info, AgileEncryptionInfo):
                raise DecryptionError('Encryption info mismatch')
            with self._ole.openstream('EncryptedPackage') as stream:
                return ECMA376Agile.decrypt(
                    key,
                    self._info.key_data_salt,
                    self._info.key_data_hash_algorithm,
                    stream,
                )
        if self._type == EncryptionType.STANDARD:
            with self._ole.openstream('EncryptedPackage') as stream:
                return ECMA376Standard.decrypt(key, stream)
        raise DecryptionError(F'Unsupported encryption type: {self._type}')

    def decrypt(self, outfile: io.IOBase):
        if self._type == EncryptionType.PLAIN:
            raise DecryptionError('Document is not encrypted')
        obuf = self._decrypt_package()
        outfile.write(obuf)
        if not zipfile.is_zipfile(io.BytesIO(obuf)):
            raise InvalidKeyError('The file could not be decrypted with this password')

    def is_encrypted(self) -> bool:
        return self._type != EncryptionType.PLAIN

Methods

def load_key(self, password=None)
Expand source code Browse git
def load_key(self, password: str | None = None):
    if not password:
        raise DecryptionError('No password specified')
    if self._type == EncryptionType.AGILE:
        if not isinstance(self._info, AgileEncryptionInfo):
            raise DecryptionError('Encryption info mismatch')
        self._key = ECMA376Agile.makekey_from_password(
            password,
            self._info.password_salt,
            self._info.password_hash_algorithm,
            self._info.encrypted_key_value,
            self._info.spin_value,
            self._info.password_key_bits,
        )
    elif self._type == EncryptionType.STANDARD:
        if not isinstance(self._info, StandardEncryptionInfo):
            raise DecryptionError('Encryption info mismatch')
        self._key = ECMA376Standard.makekey_from_password(
            password,
            self._info.header.alg_id,
            self._info.header.alg_id_hash,
            self._info.header.provider_type,
            self._info.header.key_size,
            self._info.verifier.salt_size,
            self._info.verifier.salt,
        )
def decrypt(self, outfile)
Expand source code Browse git
def decrypt(self, outfile: io.IOBase):
    if self._type == EncryptionType.PLAIN:
        raise DecryptionError('Document is not encrypted')
    obuf = self._decrypt_package()
    outfile.write(obuf)
    if not zipfile.is_zipfile(io.BytesIO(obuf)):
        raise InvalidKeyError('The file could not be decrypted with this password')
def is_encrypted(self)
Expand source code Browse git
def is_encrypted(self) -> bool:
    return self._type != EncryptionType.PLAIN
class Doc97File (file)
Expand source code Browse git
class Doc97File:

    def __init__(self, file: io.IOBase):
        file.seek(0)
        self._data = file.read()
        self._ole = OleFile(self._data)
        self._key: str | None = None
        self._salt: bytes | None = None
        self._key_size: int = 0
        self._type: EncryptionType | None = None

        with self._ole.openstream('wordDocument') as stream:
            fib_raw = stream.read(32)
            self._fib = _parse_fib_base(fib_raw)
        self._tablename = '1Table' if self._fib.f_which_tbl_stm else '0Table'

    def load_key(self, password: str | None = None):
        if not password:
            raise DecryptionError('No password specified')
        fib = self._fib
        if not fib.f_encrypted:
            raise DecryptionError('File is not encrypted')
        if fib.f_obfuscation:
            raise DecryptionError('XOR obfuscation in DOC is not supported')

        with self._ole.openstream(self._tablename) as table:
            v_major, v_minor = unpack('<HH', table.read(4))
            if v_major == 1 and v_minor == 1:
                rc4_info = _parse_header_rc4(table)
                if DocumentRC4.verifypw(
                    password, rc4_info.salt,
                    rc4_info.encrypted_verifier,
                    rc4_info.encrypted_verifier_hash,
                ):
                    self._type = EncryptionType.RC4
                    self._key = password
                    self._salt = rc4_info.salt
                else:
                    raise InvalidKeyError('Failed to verify password')
            elif v_major in (2, 3, 4) and v_minor == 2:
                cryptoapi_info = _parse_header_rc4_cryptoapi(table)
                if DocumentRC4CryptoAPI.verifypw(
                    password, cryptoapi_info.salt, cryptoapi_info.key_size,
                    cryptoapi_info.encrypted_verifier,
                    cryptoapi_info.encrypted_verifier_hash,
                ):
                    self._type = EncryptionType.RC4_CRYPTOAPI
                    self._key = password
                    self._salt = cryptoapi_info.salt
                    self._key_size = cryptoapi_info.key_size
                else:
                    raise InvalidKeyError('Failed to verify password')
            else:
                raise DecryptionError('Unsupported encryption method')

    def _require_key(self) -> tuple[str, bytes]:
        if self._key is None or self._salt is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        return self._key, self._salt

    def _decrypt_stream(self, name: str) -> MemoryFile[bytearray]:
        key, salt = self._require_key()
        stream = self._ole.openstream(name)
        if self._type == EncryptionType.RC4:
            return DocumentRC4.decrypt(key, salt, stream)
        return DocumentRC4CryptoAPI.decrypt(key, salt, self._key_size, stream)

    def decrypt(self, outfile: io.IOBase):
        FIB_LENGTH = 0x44
        fib_raw = bytearray(self._ole.openstream('wordDocument').read(FIB_LENGTH))
        patched_header = _patch_fib_encryption(fib_raw)
        fib_prefix = bytearray(patched_header)
        fib_prefix.extend(fib_raw[len(patched_header):FIB_LENGTH])

        dec_wd = self._decrypt_stream('wordDocument')
        dec_wd.seek(FIB_LENGTH)
        fib_prefix += dec_wd.read()
        word_doc_buf = fib_prefix

        table_buf = self._decrypt_stream(self._tablename).read()

        data_buf: bytes | bytearray | None = None
        if self._ole.exists('Data'):
            data_buf = self._decrypt_stream('Data').read()

        out = bytearray(self._data)
        ole = OleFile(out)
        ole.write_stream('wordDocument', word_doc_buf)
        ole.write_stream(self._tablename, table_buf)
        if data_buf is not None:
            ole.write_stream('Data', data_buf)
        outfile.write(out)

    def is_encrypted(self) -> bool:
        return self._fib.f_encrypted == 1

Methods

def load_key(self, password=None)
Expand source code Browse git
def load_key(self, password: str | None = None):
    if not password:
        raise DecryptionError('No password specified')
    fib = self._fib
    if not fib.f_encrypted:
        raise DecryptionError('File is not encrypted')
    if fib.f_obfuscation:
        raise DecryptionError('XOR obfuscation in DOC is not supported')

    with self._ole.openstream(self._tablename) as table:
        v_major, v_minor = unpack('<HH', table.read(4))
        if v_major == 1 and v_minor == 1:
            rc4_info = _parse_header_rc4(table)
            if DocumentRC4.verifypw(
                password, rc4_info.salt,
                rc4_info.encrypted_verifier,
                rc4_info.encrypted_verifier_hash,
            ):
                self._type = EncryptionType.RC4
                self._key = password
                self._salt = rc4_info.salt
            else:
                raise InvalidKeyError('Failed to verify password')
        elif v_major in (2, 3, 4) and v_minor == 2:
            cryptoapi_info = _parse_header_rc4_cryptoapi(table)
            if DocumentRC4CryptoAPI.verifypw(
                password, cryptoapi_info.salt, cryptoapi_info.key_size,
                cryptoapi_info.encrypted_verifier,
                cryptoapi_info.encrypted_verifier_hash,
            ):
                self._type = EncryptionType.RC4_CRYPTOAPI
                self._key = password
                self._salt = cryptoapi_info.salt
                self._key_size = cryptoapi_info.key_size
            else:
                raise InvalidKeyError('Failed to verify password')
        else:
            raise DecryptionError('Unsupported encryption method')
def decrypt(self, outfile)
Expand source code Browse git
def decrypt(self, outfile: io.IOBase):
    FIB_LENGTH = 0x44
    fib_raw = bytearray(self._ole.openstream('wordDocument').read(FIB_LENGTH))
    patched_header = _patch_fib_encryption(fib_raw)
    fib_prefix = bytearray(patched_header)
    fib_prefix.extend(fib_raw[len(patched_header):FIB_LENGTH])

    dec_wd = self._decrypt_stream('wordDocument')
    dec_wd.seek(FIB_LENGTH)
    fib_prefix += dec_wd.read()
    word_doc_buf = fib_prefix

    table_buf = self._decrypt_stream(self._tablename).read()

    data_buf: bytes | bytearray | None = None
    if self._ole.exists('Data'):
        data_buf = self._decrypt_stream('Data').read()

    out = bytearray(self._data)
    ole = OleFile(out)
    ole.write_stream('wordDocument', word_doc_buf)
    ole.write_stream(self._tablename, table_buf)
    if data_buf is not None:
        ole.write_stream('Data', data_buf)
    outfile.write(out)
def is_encrypted(self)
Expand source code Browse git
def is_encrypted(self) -> bool:
    return self._fib.f_encrypted == 1
class Xls97File (file)
Expand source code Browse git
class Xls97File:

    def __init__(self, file: io.IOBase):
        file.seek(0)
        self._data = file.read()
        self._ole = OleFile(self._data)
        self._key: str | None = None
        self._salt: bytes | None = None
        self._key_size: int = 0
        self._type: EncryptionType | None = None

    def load_key(self, password: str | None = None):
        if not password:
            raise DecryptionError('No password specified')
        with self._ole.openstream('Workbook') as wb:
            workbook = _BIFFStream(wb)
            num = unpack('<H', workbook._data.read(2))[0]
            if num != _BIFF_BOF:
                raise FileFormatError('Invalid Workbook stream')
            size = unpack('<H', workbook._data.read(2))[0]
            workbook._data.read(size)
            _num, size = workbook.skip_to(_BIFF_FILEPASS)

            enc_type = unpack('<H', workbook._data.read(2))[0]
            enc_info = MemoryFile(workbook._data.read(size - 2))

            if enc_type == 0x0000:
                key, verification_bytes = unpack('<HH', enc_info.read(4))
                if DocumentXOR.verifypw(password, verification_bytes):
                    self._type = EncryptionType.XOR
                    self._key = password
                else:
                    raise InvalidKeyError('Failed to verify password')
            elif enc_type == 0x0001:
                v_major, v_minor = unpack('<HH', enc_info.read(4))
                if v_major == 1 and v_minor == 1:
                    rc4_info = _parse_header_rc4(enc_info)
                    if DocumentRC4.verifypw(
                        password, rc4_info.salt,
                        rc4_info.encrypted_verifier,
                        rc4_info.encrypted_verifier_hash,
                    ):
                        self._type = EncryptionType.RC4
                        self._key = password
                        self._salt = rc4_info.salt
                    else:
                        raise InvalidKeyError('Failed to verify password')
                elif v_major in (2, 3, 4) and v_minor == 2:
                    cryptoapi_info = _parse_header_rc4_cryptoapi(enc_info)
                    if DocumentRC4CryptoAPI.verifypw(
                        password, cryptoapi_info.salt, cryptoapi_info.key_size,
                        cryptoapi_info.encrypted_verifier,
                        cryptoapi_info.encrypted_verifier_hash,
                    ):
                        self._type = EncryptionType.RC4_CRYPTOAPI
                        self._key = password
                        self._salt = cryptoapi_info.salt
                        self._key_size = cryptoapi_info.key_size
                    else:
                        raise InvalidKeyError('Failed to verify password')
                else:
                    raise DecryptionError('Unsupported encryption method')

    def _read_workbook_records(self) -> tuple[list[int], MemoryFile]:
        plain_buf: list[int] = []
        encrypted_buf = MemoryFile()
        with self._ole.openstream('Workbook') as wb:
            workbook = _BIFFStream(wb)
            for num, size, record in workbook.iter_record():
                if num == _BIFF_FILEPASS:
                    plain_buf.extend((0, 0))
                    plain_buf.extend(pack('<H', size))
                    plain_buf.extend(0 for _ in range(size))
                    encrypted_buf.write(b'\x00' * (4 + size))
                elif num in _NOT_OBFUSCATED:
                    header = pack('<HH', num, size)
                    plain_buf.extend(header)
                    plain_buf.extend(record.read())
                    encrypted_buf.write(b'\x00' * (4 + size))
                elif num == _BIFF_BOUNDSHEET8:
                    header = pack('<HH', num, size)
                    plain_buf.extend(header)
                    plain_buf.extend(record.read(4))
                    plain_buf.extend(-2 for _ in range(size - 4))
                    encrypted_buf.write(b'\x00' * 4 + b'\x00' * 4 + record.read())
                else:
                    header = pack('<HH', num, size)
                    plain_buf.extend(header)
                    plain_buf.extend(-1 for _ in range(size))
                    encrypted_buf.write(b'\x00' * 4 + record.read())
        encrypted_buf.seek(0)
        return plain_buf, encrypted_buf

    def decrypt(self, outfile: io.IOBase):
        if self._key is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        plain_buf, encrypted_buf = self._read_workbook_records()

        if self._type == EncryptionType.RC4:
            if self._salt is None:
                raise DecryptionError('load_key() must be called before decrypt()')
            dec = DocumentRC4.decrypt(self._key, self._salt, encrypted_buf, blocksize=1024)
        elif self._type == EncryptionType.RC4_CRYPTOAPI:
            if self._salt is None:
                raise DecryptionError('load_key() must be called before decrypt()')
            dec = DocumentRC4CryptoAPI.decrypt(
                self._key, self._salt, self._key_size, encrypted_buf, blocksize=1024)
        elif self._type == EncryptionType.XOR:
            dec = DocumentXOR.decrypt(self._key, encrypted_buf, plain_buf, [], 10)
        else:
            raise DecryptionError(F'Unsupported encryption type: {self._type}')

        for c in plain_buf:
            if c in (-1, -2):
                dec.seek(1, 1)
            else:
                dec.write_byte(c)
        dec.seek(0)

        out = bytearray(self._data)
        ole = OleFile(out)
        ole.write_stream('Workbook', dec.read())
        outfile.write(out)

    def is_encrypted(self) -> bool:
        stream = _BIFFStream(self._ole.openstream('Workbook'))
        num = unpack('<H', stream._data.read(2))[0]
        if num != _BIFF_BOF:
            return False
        size = unpack('<H', stream._data.read(2))[0]
        stream._data.read(size)
        return stream.has_record(_BIFF_FILEPASS)

Methods

def load_key(self, password=None)
Expand source code Browse git
def load_key(self, password: str | None = None):
    if not password:
        raise DecryptionError('No password specified')
    with self._ole.openstream('Workbook') as wb:
        workbook = _BIFFStream(wb)
        num = unpack('<H', workbook._data.read(2))[0]
        if num != _BIFF_BOF:
            raise FileFormatError('Invalid Workbook stream')
        size = unpack('<H', workbook._data.read(2))[0]
        workbook._data.read(size)
        _num, size = workbook.skip_to(_BIFF_FILEPASS)

        enc_type = unpack('<H', workbook._data.read(2))[0]
        enc_info = MemoryFile(workbook._data.read(size - 2))

        if enc_type == 0x0000:
            key, verification_bytes = unpack('<HH', enc_info.read(4))
            if DocumentXOR.verifypw(password, verification_bytes):
                self._type = EncryptionType.XOR
                self._key = password
            else:
                raise InvalidKeyError('Failed to verify password')
        elif enc_type == 0x0001:
            v_major, v_minor = unpack('<HH', enc_info.read(4))
            if v_major == 1 and v_minor == 1:
                rc4_info = _parse_header_rc4(enc_info)
                if DocumentRC4.verifypw(
                    password, rc4_info.salt,
                    rc4_info.encrypted_verifier,
                    rc4_info.encrypted_verifier_hash,
                ):
                    self._type = EncryptionType.RC4
                    self._key = password
                    self._salt = rc4_info.salt
                else:
                    raise InvalidKeyError('Failed to verify password')
            elif v_major in (2, 3, 4) and v_minor == 2:
                cryptoapi_info = _parse_header_rc4_cryptoapi(enc_info)
                if DocumentRC4CryptoAPI.verifypw(
                    password, cryptoapi_info.salt, cryptoapi_info.key_size,
                    cryptoapi_info.encrypted_verifier,
                    cryptoapi_info.encrypted_verifier_hash,
                ):
                    self._type = EncryptionType.RC4_CRYPTOAPI
                    self._key = password
                    self._salt = cryptoapi_info.salt
                    self._key_size = cryptoapi_info.key_size
                else:
                    raise InvalidKeyError('Failed to verify password')
            else:
                raise DecryptionError('Unsupported encryption method')
def decrypt(self, outfile)
Expand source code Browse git
def decrypt(self, outfile: io.IOBase):
    if self._key is None:
        raise DecryptionError('load_key() must be called before decrypt()')
    plain_buf, encrypted_buf = self._read_workbook_records()

    if self._type == EncryptionType.RC4:
        if self._salt is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        dec = DocumentRC4.decrypt(self._key, self._salt, encrypted_buf, blocksize=1024)
    elif self._type == EncryptionType.RC4_CRYPTOAPI:
        if self._salt is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        dec = DocumentRC4CryptoAPI.decrypt(
            self._key, self._salt, self._key_size, encrypted_buf, blocksize=1024)
    elif self._type == EncryptionType.XOR:
        dec = DocumentXOR.decrypt(self._key, encrypted_buf, plain_buf, [], 10)
    else:
        raise DecryptionError(F'Unsupported encryption type: {self._type}')

    for c in plain_buf:
        if c in (-1, -2):
            dec.seek(1, 1)
        else:
            dec.write_byte(c)
    dec.seek(0)

    out = bytearray(self._data)
    ole = OleFile(out)
    ole.write_stream('Workbook', dec.read())
    outfile.write(out)
def is_encrypted(self)
Expand source code Browse git
def is_encrypted(self) -> bool:
    stream = _BIFFStream(self._ole.openstream('Workbook'))
    num = unpack('<H', stream._data.read(2))[0]
    if num != _BIFF_BOF:
        return False
    size = unpack('<H', stream._data.read(2))[0]
    stream._data.read(size)
    return stream.has_record(_BIFF_FILEPASS)
class Ppt97File (file)
Expand source code Browse git
class Ppt97File:

    def __init__(self, file: io.IOBase):
        file.seek(0)
        self._data = file.read()
        self._ole = OleFile(self._data)
        self._key: str | None = None
        self._salt: bytes | None = None
        self._key_size: int = 0
        self._type: EncryptionType | None = None

    def load_key(self, password: str | None = None):
        if not password:
            raise DecryptionError('No password specified')
        cu_stream = self._ole.openstream('Current User')
        ppt_stream = self._ole.openstream('PowerPoint Document')

        pod = _construct_persist_object_directory(cu_stream, ppt_stream)

        cu_stream.seek(0)
        cu = _parse_current_user_atom(cu_stream)
        ppt_stream.seek(cu.offset_to_current_edit)
        uea = _parse_user_edit_atom(ppt_stream)

        if uea.encrypt_session_persist_id_ref is None:
            raise DecryptionError('PowerPoint file has no encryption session reference')
        crypt_offset = pod[uea.encrypt_session_persist_id_ref]
        ppt_stream.seek(crypt_offset)
        rh = _parse_record_header(ppt_stream.read(8))
        crypt_data = ppt_stream.read(rh.rec_len)

        enc_info = MemoryFile(crypt_data)
        v_major, v_minor = unpack('<HH', enc_info.read(4))
        if not (v_major in (2, 3, 4) and v_minor == 2):
            raise DecryptionError('Unsupported PPT encryption version')

        info = _parse_header_rc4_cryptoapi(enc_info)
        if DocumentRC4CryptoAPI.verifypw(
            password, info.salt, info.key_size,
            info.encrypted_verifier, info.encrypted_verifier_hash,
        ):
            self._type = EncryptionType.RC4_CRYPTOAPI
            self._key = password
            self._salt = info.salt
            self._key_size = info.key_size
        else:
            raise InvalidKeyError('Failed to verify password')

    def _require_key(self) -> tuple[str, bytes]:
        if self._key is None or self._salt is None:
            raise DecryptionError('load_key() must be called before decrypt()')
        return self._key, self._salt

    def decrypt(self, outfile: io.IOBase):
        key, salt = self._require_key()
        cu_stream = self._ole.openstream('Current User')
        ppt_stream = self._ole.openstream('PowerPoint Document')

        cu_stream.seek(0)
        cu_atom = _parse_current_user_atom(cu_stream)
        cu_atom_new = cu_atom._replace(header_token=0xE391C05F)
        cu_buf = _pack_current_user_atom(cu_atom_new)

        ppt_stream.seek(0)
        dec_ba = bytearray(ppt_stream.read())

        ppt_stream.seek(cu_atom.offset_to_current_edit)
        uea = _parse_user_edit_atom(ppt_stream)

        rh_new = uea.rh._replace(rec_len=uea.rh.rec_len - 4)
        uea_new = uea._replace(rh=rh_new, encrypt_session_persist_id_ref=0x00000000)
        uea_bytes = bytearray(_pack_user_edit_atom(uea_new))
        offset = cu_atom.offset_to_current_edit
        dec_ba[offset:offset + len(uea_bytes)] = uea_bytes

        ppt_stream.seek(cu_atom.offset_to_current_edit)
        uea = _parse_user_edit_atom(ppt_stream)
        ppt_stream.seek(uea.offset_persist_directory)
        pda = _parse_persist_directory_atom(ppt_stream)

        first = pda.rg_persist_dir_entry[0]._replace(
            c_persist=pda.rg_persist_dir_entry[0].c_persist - 1)
        pda_new = pda._replace(rg_persist_dir_entry=[first])
        pda_bytes = bytearray(_pack_persist_directory_atom(pda_new))
        offset = uea.offset_persist_directory
        dec_ba[offset:offset + len(pda_bytes)] = pda_bytes

        ppt_stream.seek(0)
        pod = _construct_persist_object_directory(
            MemoryFile(cu_buf), MemoryFile(bytearray(ppt_stream.read())))
        directory_items = list(pod.items())

        for i, (persist_id, offset) in enumerate(directory_items):
            mv = memoryview(dec_ba)
            rh = _parse_record_header(mv[offset:offset + 8])
            if rh.rec_type == 0x2F14:
                dec_ba[offset:offset + 8 + rh.rec_len] = b'\x00' * (8 + rh.rec_len)
                continue
            if rh.rec_type in (0x0FF5, 0x1772):
                continue
            rec_len = directory_items[i + 1][1] - offset - 8
            enc_buf = MemoryFile(mv[offset:offset + 8 + rec_len])
            blocksize = self._key_size * ((8 + rec_len) // self._key_size + 1)
            dec = DocumentRC4CryptoAPI.decrypt(
                key, salt, self._key_size,
                enc_buf, blocksize=blocksize, block=persist_id)
            dec_bytes = bytearray(dec.read())
            dec_ba[offset:offset + len(dec_bytes)] = dec_bytes

        out = bytearray(self._data)
        ole = OleFile(out)
        cu_stream_size = ole.get_size('Current User')
        cu_padded = bytearray(cu_buf)
        if len(cu_padded) < cu_stream_size:
            cu_padded += b'\x00' * (cu_stream_size - len(cu_padded))
        ole.write_stream('Current User', cu_padded)
        ole.write_stream('PowerPoint Document', bytes(dec_ba))
        outfile.write(out)

    def is_encrypted(self) -> bool:
        cu_stream = self._ole.openstream('Current User')
        cu_stream.seek(0)
        cu = _parse_current_user_atom(cu_stream)
        ppt_stream = self._ole.openstream('PowerPoint Document')
        ppt_stream.seek(cu.offset_to_current_edit)
        uea = _parse_user_edit_atom(ppt_stream)
        return uea.rh.rec_len == 0x20

Methods

def load_key(self, password=None)
Expand source code Browse git
def load_key(self, password: str | None = None):
    if not password:
        raise DecryptionError('No password specified')
    cu_stream = self._ole.openstream('Current User')
    ppt_stream = self._ole.openstream('PowerPoint Document')

    pod = _construct_persist_object_directory(cu_stream, ppt_stream)

    cu_stream.seek(0)
    cu = _parse_current_user_atom(cu_stream)
    ppt_stream.seek(cu.offset_to_current_edit)
    uea = _parse_user_edit_atom(ppt_stream)

    if uea.encrypt_session_persist_id_ref is None:
        raise DecryptionError('PowerPoint file has no encryption session reference')
    crypt_offset = pod[uea.encrypt_session_persist_id_ref]
    ppt_stream.seek(crypt_offset)
    rh = _parse_record_header(ppt_stream.read(8))
    crypt_data = ppt_stream.read(rh.rec_len)

    enc_info = MemoryFile(crypt_data)
    v_major, v_minor = unpack('<HH', enc_info.read(4))
    if not (v_major in (2, 3, 4) and v_minor == 2):
        raise DecryptionError('Unsupported PPT encryption version')

    info = _parse_header_rc4_cryptoapi(enc_info)
    if DocumentRC4CryptoAPI.verifypw(
        password, info.salt, info.key_size,
        info.encrypted_verifier, info.encrypted_verifier_hash,
    ):
        self._type = EncryptionType.RC4_CRYPTOAPI
        self._key = password
        self._salt = info.salt
        self._key_size = info.key_size
    else:
        raise InvalidKeyError('Failed to verify password')
def decrypt(self, outfile)
Expand source code Browse git
def decrypt(self, outfile: io.IOBase):
    key, salt = self._require_key()
    cu_stream = self._ole.openstream('Current User')
    ppt_stream = self._ole.openstream('PowerPoint Document')

    cu_stream.seek(0)
    cu_atom = _parse_current_user_atom(cu_stream)
    cu_atom_new = cu_atom._replace(header_token=0xE391C05F)
    cu_buf = _pack_current_user_atom(cu_atom_new)

    ppt_stream.seek(0)
    dec_ba = bytearray(ppt_stream.read())

    ppt_stream.seek(cu_atom.offset_to_current_edit)
    uea = _parse_user_edit_atom(ppt_stream)

    rh_new = uea.rh._replace(rec_len=uea.rh.rec_len - 4)
    uea_new = uea._replace(rh=rh_new, encrypt_session_persist_id_ref=0x00000000)
    uea_bytes = bytearray(_pack_user_edit_atom(uea_new))
    offset = cu_atom.offset_to_current_edit
    dec_ba[offset:offset + len(uea_bytes)] = uea_bytes

    ppt_stream.seek(cu_atom.offset_to_current_edit)
    uea = _parse_user_edit_atom(ppt_stream)
    ppt_stream.seek(uea.offset_persist_directory)
    pda = _parse_persist_directory_atom(ppt_stream)

    first = pda.rg_persist_dir_entry[0]._replace(
        c_persist=pda.rg_persist_dir_entry[0].c_persist - 1)
    pda_new = pda._replace(rg_persist_dir_entry=[first])
    pda_bytes = bytearray(_pack_persist_directory_atom(pda_new))
    offset = uea.offset_persist_directory
    dec_ba[offset:offset + len(pda_bytes)] = pda_bytes

    ppt_stream.seek(0)
    pod = _construct_persist_object_directory(
        MemoryFile(cu_buf), MemoryFile(bytearray(ppt_stream.read())))
    directory_items = list(pod.items())

    for i, (persist_id, offset) in enumerate(directory_items):
        mv = memoryview(dec_ba)
        rh = _parse_record_header(mv[offset:offset + 8])
        if rh.rec_type == 0x2F14:
            dec_ba[offset:offset + 8 + rh.rec_len] = b'\x00' * (8 + rh.rec_len)
            continue
        if rh.rec_type in (0x0FF5, 0x1772):
            continue
        rec_len = directory_items[i + 1][1] - offset - 8
        enc_buf = MemoryFile(mv[offset:offset + 8 + rec_len])
        blocksize = self._key_size * ((8 + rec_len) // self._key_size + 1)
        dec = DocumentRC4CryptoAPI.decrypt(
            key, salt, self._key_size,
            enc_buf, blocksize=blocksize, block=persist_id)
        dec_bytes = bytearray(dec.read())
        dec_ba[offset:offset + len(dec_bytes)] = dec_bytes

    out = bytearray(self._data)
    ole = OleFile(out)
    cu_stream_size = ole.get_size('Current User')
    cu_padded = bytearray(cu_buf)
    if len(cu_padded) < cu_stream_size:
        cu_padded += b'\x00' * (cu_stream_size - len(cu_padded))
    ole.write_stream('Current User', cu_padded)
    ole.write_stream('PowerPoint Document', bytes(dec_ba))
    outfile.write(out)
def is_encrypted(self)
Expand source code Browse git
def is_encrypted(self) -> bool:
    cu_stream = self._ole.openstream('Current User')
    cu_stream.seek(0)
    cu = _parse_current_user_atom(cu_stream)
    ppt_stream = self._ole.openstream('PowerPoint Document')
    ppt_stream.seek(cu.offset_to_current_edit)
    uea = _parse_user_edit_atom(ppt_stream)
    return uea.rh.rec_len == 0x20