Skip to content

Commit

Permalink
update: refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
nolze committed May 24, 2024
1 parent 30bc352 commit 13c678c
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 49 deletions.
2 changes: 0 additions & 2 deletions msoffcrypto/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import print_function

import argparse
import getpass
import logging
Expand Down
11 changes: 9 additions & 2 deletions msoffcrypto/format/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,34 @@ def _parse_encryptionheader(blob):


# https://msdn.microsoft.com/en-us/library/dd910568(v=office.12).aspx
def _parse_encryptionverifier(blob, algorithm):
def _parse_encryptionverifier(blob, algorithm: str):
(saltSize,) = unpack("<I", blob.read(4))
salt = blob.read(16)

encryptedVerifier = blob.read(16)

(verifierHashSize,) = unpack("<I", blob.read(4))

if algorithm == "RC4":
encryptedVerifierHash = blob.read(20)
elif algorithm == "AES":
encryptedVerifierHash = blob.read(32)
else:
raise ValueError("Invalid algorithm: {}".format(algorithm))

verifier = {
"saltSize": saltSize,
"salt": salt,
"encryptedVerifier": encryptedVerifier,
"verifierHashSize": verifierHashSize,
"encryptedVerifierHash": encryptedVerifierHash,
}

return verifier


def _parse_header_RC4CryptoAPI(encryptionHeader):
flags = encryptionHeader.read(4)
_flags = encryptionHeader.read(4) # TODO: Support flags
(headerSize,) = unpack("<I", encryptionHeader.read(4))
logger.debug(headerSize)
blob = io.BytesIO(encryptionHeader.read(headerSize))
Expand Down
15 changes: 15 additions & 0 deletions msoffcrypto/format/doc97.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,12 +423,18 @@ def decrypt(self, outfile):
dec1 = DocumentRC4CryptoAPI.decrypt(
self.key, self.salt, self.keySize, worddocument
)
else:
raise exceptions.DecryptionError(
"Unsupported encryption method: {}".format(self.type)
)

dec1.seek(FIB_LENGTH)
obuf1.write(dec1.read())
obuf1.seek(0)

# TODO: Preserve header
obuf2 = io.BytesIO()

if self.type == "rc4":
with self.ole.openstream(self.info.tablename) as stream:
dec2 = DocumentRC4.decrypt(self.key, self.salt, stream)
Expand All @@ -437,6 +443,11 @@ def decrypt(self, outfile):
dec2 = DocumentRC4CryptoAPI.decrypt(
self.key, self.salt, self.keySize, stream
)
else:
raise exceptions.DecryptionError(
"Unsupported encryption method: {}".format(self.type)
)

obuf2.write(dec2.read())
obuf2.seek(0)

Expand All @@ -451,6 +462,10 @@ def decrypt(self, outfile):
dec3 = DocumentRC4CryptoAPI.decrypt(
self.key, self.salt, self.keySize, data_stream
)
else:
raise exceptions.DecryptionError(
"Unsupported encryption method: {}".format(self.type)
)
obuf3.write(dec3.read())
obuf3.seek(0)

Expand Down
108 changes: 76 additions & 32 deletions msoffcrypto/format/ooxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ def _is_ooxml(file):
# Heuristic
if (
xml.documentElement.tagName == "Types"
and xml.documentElement.namespaceURI == "http://schemas.openxmlformats.org/package/2006/content-types"
and xml.documentElement.namespaceURI
== "http://schemas.openxmlformats.org/package/2006/content-types"
):
return True
else:
return False
except:
except Exception:
return False


Expand All @@ -49,7 +50,9 @@ def _parseinfo_standard(ole):
0x0000660F: "AES-192",
0x00006610: "AES-256",
}
verifier = _parse_encryptionverifier(blob, "AES" if header["algId"] & 0xFF00 == 0x6600 else "RC4") # TODO: Fix
verifier = _parse_encryptionverifier(
blob, "AES" if header["algId"] & 0xFF00 == 0x6600 else "RC4"
) # TODO: Fix
info = {
"header": header,
"verifier": verifier,
Expand All @@ -60,16 +63,34 @@ def _parseinfo_standard(ole):
def _parseinfo_agile(ole):
ole.seek(8)
xml = parseString(ole.read())
keyDataSalt = base64.b64decode(xml.getElementsByTagName("keyData")[0].getAttribute("saltValue"))
keyDataHashAlgorithm = xml.getElementsByTagName("keyData")[0].getAttribute("hashAlgorithm")
keyDataBlockSize = int(xml.getElementsByTagName("keyData")[0].getAttribute("blockSize"))
encryptedHmacKey = base64.b64decode(xml.getElementsByTagName("dataIntegrity")[0].getAttribute("encryptedHmacKey"))
encryptedHmacValue = base64.b64decode(xml.getElementsByTagName("dataIntegrity")[0].getAttribute("encryptedHmacValue"))
password_node = xml.getElementsByTagNameNS("http://schemas.microsoft.com/office/2006/keyEncryptor/password", "encryptedKey")[0]
keyDataSalt = base64.b64decode(
xml.getElementsByTagName("keyData")[0].getAttribute("saltValue")
)
keyDataHashAlgorithm = xml.getElementsByTagName("keyData")[0].getAttribute(
"hashAlgorithm"
)
keyDataBlockSize = int(
xml.getElementsByTagName("keyData")[0].getAttribute("blockSize")
)
encryptedHmacKey = base64.b64decode(
xml.getElementsByTagName("dataIntegrity")[0].getAttribute("encryptedHmacKey")
)
encryptedHmacValue = base64.b64decode(
xml.getElementsByTagName("dataIntegrity")[0].getAttribute("encryptedHmacValue")
)
password_node = xml.getElementsByTagNameNS(
"http://schemas.microsoft.com/office/2006/keyEncryptor/password", "encryptedKey"
)[0]
spinValue = int(password_node.getAttribute("spinCount"))
encryptedKeyValue = base64.b64decode(password_node.getAttribute("encryptedKeyValue"))
encryptedVerifierHashInput = base64.b64decode(password_node.getAttribute("encryptedVerifierHashInput"))
encryptedVerifierHashValue = base64.b64decode(password_node.getAttribute("encryptedVerifierHashValue"))
encryptedKeyValue = base64.b64decode(
password_node.getAttribute("encryptedKeyValue")
)
encryptedVerifierHashInput = base64.b64decode(
password_node.getAttribute("encryptedVerifierHashInput")
)
encryptedVerifierHashValue = base64.b64decode(
password_node.getAttribute("encryptedVerifierHashValue")
)
passwordSalt = base64.b64decode(password_node.getAttribute("saltValue"))
passwordHashAlgorithm = password_node.getAttribute("hashAlgorithm")
passwordKeyBits = int(password_node.getAttribute("keyBits"))
Expand Down Expand Up @@ -97,8 +118,12 @@ def _parseinfo(ole):
elif versionMajor in [2, 3, 4] and versionMinor == 2: # Standard
return "standard", _parseinfo_standard(ole)
elif versionMajor in [3, 4] and versionMinor == 3: # Extensible
raise exceptions.DecryptionError("Unsupported EncryptionInfo version (Extensible Encryption)")
raise exceptions.DecryptionError("Unsupported EncryptionInfo version ({}:{})".format(versionMajor, versionMinor))
raise exceptions.DecryptionError(
"Unsupported EncryptionInfo version (Extensible Encryption)"
)
raise exceptions.DecryptionError(
"Unsupported EncryptionInfo version ({}:{})".format(versionMajor, versionMinor)
)


class OOXMLFile(base.BaseOfficeFile):
Expand Down Expand Up @@ -132,7 +157,9 @@ def __init__(self, file):
with self.file.openstream("EncryptionInfo") as stream:
self.type, self.info = _parseinfo(stream)
except IOError:
raise exceptions.FileFormatError("Supposed to be an encrypted OOXML file, but no EncryptionInfo stream found")
raise exceptions.FileFormatError(
"Supposed to be an encrypted OOXML file, but no EncryptionInfo stream found"
)

logger.debug("OOXMLFile.type: {}".format(self.type))

Expand All @@ -151,7 +178,9 @@ def __init__(self, file):
else:
raise exceptions.FileFormatError("Unsupported file format")

def load_key(self, password=None, private_key=None, secret_key=None, verify_password=False):
def load_key(
self, password=None, private_key=None, secret_key=None, verify_password=False
):
"""
>>> with open("tests/outputs/ecma376standard_password_plain.docx", "rb") as f:
... officefile = OOXMLFile(f)
Expand Down Expand Up @@ -191,7 +220,9 @@ def load_key(self, password=None, private_key=None, secret_key=None, verify_pass
)
if verify_password:
verified = ECMA376Standard.verifykey(
self.secret_key, self.info["verifier"]["encryptedVerifier"], self.info["verifier"]["encryptedVerifierHash"]
self.secret_key,
self.info["verifier"]["encryptedVerifier"],
self.info["verifier"]["encryptedVerifierHash"],
)
if not verified:
raise exceptions.InvalidKeyError("Key verification failed")
Expand All @@ -201,22 +232,26 @@ def load_key(self, password=None, private_key=None, secret_key=None, verify_pass
pass
elif private_key:
if self.type == "agile":
self.secret_key = ECMA376Agile.makekey_from_privkey(private_key, self.info["encryptedKeyValue"])
self.secret_key = ECMA376Agile.makekey_from_privkey(
private_key, self.info["encryptedKeyValue"]
)
else:
raise exceptions.DecryptionError("Unsupported key type for the encryption method")
raise exceptions.DecryptionError(
"Unsupported key type for the encryption method"
)
elif secret_key:
self.secret_key = secret_key
else:
raise exceptions.DecryptionError("No key specified")

def decrypt(self, ofile, verify_integrity=False):
def decrypt(self, outfile, verify_integrity=False):
"""
>>> from msoffcrypto import exceptions
>>> from io import BytesIO; ofile = BytesIO()
>>> from io import BytesIO; outfile = BytesIO()
>>> with open("tests/outputs/ecma376standard_password_plain.docx", "rb") as f:
... officefile = OOXMLFile(f)
... officefile.load_key("1234")
... officefile.decrypt(ofile)
... officefile.decrypt(outfile)
Traceback (most recent call last):
msoffcrypto.exceptions.DecryptionError: Unencrypted document
"""
Expand All @@ -233,30 +268,39 @@ def decrypt(self, ofile, verify_integrity=False):
stream,
)
if not verified:
raise exceptions.InvalidKeyError("Payload integrity verification failed")

obuf = ECMA376Agile.decrypt(self.secret_key, self.info["keyDataSalt"], self.info["keyDataHashAlgorithm"], stream)
ofile.write(obuf)
raise exceptions.InvalidKeyError(
"Payload integrity verification failed"
)

obuf = ECMA376Agile.decrypt(
self.secret_key,
self.info["keyDataSalt"],
self.info["keyDataHashAlgorithm"],
stream,
)
outfile.write(obuf)
elif self.type == "standard":
with self.file.openstream("EncryptedPackage") as stream:
obuf = ECMA376Standard.decrypt(self.secret_key, stream)
ofile.write(obuf)
outfile.write(obuf)
elif self.type == "plain":
raise exceptions.DecryptionError("Unencrypted document")
else:
raise exceptions.DecryptionError("Unsupported encryption method")

# If the file is successfully decrypted, there must be a valid OOXML file, i.e. a valid zip file
if not zipfile.is_zipfile(io.BytesIO(obuf)):
raise exceptions.InvalidKeyError("The file could not be decrypted with this password")
raise exceptions.InvalidKeyError(
"The file could not be decrypted with this password"
)

def encrypt(self, password, ofile):
def encrypt(self, password, outfile):
"""
>>> from msoffcrypto.format.ooxml import OOXMLFile
>>> from io import BytesIO; ofile = BytesIO()
>>> from io import BytesIO; outfile = BytesIO()
>>> with open("tests/outputs/example.docx", "rb") as f:
... officefile = OOXMLFile(f)
... officefile.encrypt("1234", ofile)
... officefile.encrypt("1234", outfile)
"""
if self.is_encrypted():
raise exceptions.EncryptionError("File is already encrypted")
Expand All @@ -268,7 +312,7 @@ def encrypt(self, password, ofile):
if not olefile.isOleFile(buf):
raise exceptions.EncryptionError("Unable to encrypt this file")

ofile.write(buf)
outfile.write(buf)

def is_encrypted(self):
"""
Expand Down
4 changes: 4 additions & 0 deletions msoffcrypto/format/xls97.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,10 @@ def decrypt(self, outfile):
dec = DocumentXOR.decrypt(
self.key, encrypted_buf, plain_buf, record_info, 10
)
else:
raise exceptions.DecryptionError(
"Unsupported encryption method: {}".format(self.type)
)

for c in plain_buf:
if c == -1 or c == -2:
Expand Down
Loading

0 comments on commit 13c678c

Please sign in to comment.