Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump to v3 #4

Merged
merged 17 commits into from
May 11, 2018
Next Next commit
Isolate functionalities
  • Loading branch information
nolze committed May 10, 2018
commit c623b608ab7f973c0c686272ffc2998d37d07f07
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ import msoffcrypto
file = msoffcrypto.OfficeFile(open("encrypted.docx", "rb"))

# Use password
file.load_password("Passw0rd")
file.load_key(password="Passw0rd")

# Use private key
# file.load_privkey(open("priv.pem", "rb"))
# file.load_key(private_key=open("priv.pem", "rb"))
# Use intermediate key (secretKey)
# file.load_skey(binascii.unhexlify("AE8C36E68B4BB9EA46E5544A5FDB6693875B2FDE1507CBC65C8BCF99E25C2562"))
# file.load_key(secret_key=binascii.unhexlify("AE8C36E68B4BB9EA46E5544A5FDB6693875B2FDE1507CBC65C8BCF99E25C2562"))

file.decrypt(open("decrypted.docx", "wb"))
```
Expand Down
2 changes: 1 addition & 1 deletion msoffcrypto/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .msoffcrypto import *
from .officefile import OfficeFile
51 changes: 51 additions & 0 deletions msoffcrypto/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import sys
import logging

logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

import olefile

from .officefile import OfficeFile

def ifWIN32SetBinary(io):
if sys.platform == 'win32':
import msvcrt, os
msvcrt.setmode(io.fileno(), os.O_BINARY)

def main():
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-k', dest='secret_key', help='MS-OFFCRYPTO secretKey value (hex)')
group.add_argument('-p', dest='private_key', type=argparse.FileType('rb'), help='RSA private key file')
group.add_argument('-P', dest='password', help='Password ASCII')
parser.add_argument('-v', dest='verbose', action='store_true', help='Print verbose information')
parser.add_argument('infile', nargs='?', type=argparse.FileType('rb'))
parser.add_argument('outfile', nargs='?', type=argparse.FileType('wb'))
args = parser.parse_args()

if not olefile.isOleFile(args.infile):
raise AssertionError("No OLE file")

file = OfficeFile(args.infile)

if args.verbose:
logger.removeHandler(logging.NullHandler())
logging.basicConfig(level=logging.DEBUG, format="%(message)s")

if args.secret_key:
file.load_key(secret_key=binascii.unhexlify(args.secret_key))
elif args.private_key:
file.load_key(private_key=args.private_key)
elif args.password:
file.load_key(password=args.password)

if args.outfile == None:
ifWIN32SetBinary(sys.stdout)
args.outfile = sys.stdout.buffer

file.decrypt(args.outfile)

if __name__ == '__main__':
main()
14 changes: 14 additions & 0 deletions msoffcrypto/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from abc import ABC
from abc import abstractmethod

class BaseOfficeFile(ABC):
def __init__(self):
pass

@abstractmethod
def load_key(self):
pass

@abstractmethod
def decrypt(self):
pass
4 changes: 4 additions & 0 deletions msoffcrypto/officefile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def OfficeFile(file):
# TODO: Conditional on file
from .ooxml import OOXMLFile
return OOXMLFile(file)
92 changes: 27 additions & 65 deletions msoffcrypto/msoffcrypto.py → msoffcrypto/ooxml.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
import sys, hashlib, base64, binascii, functools, os
from struct import pack, unpack
from xml.dom.minidom import parseString
import logging

import olefile
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

import olefile
from xml.dom.minidom import parseString

import logging
from . import base

logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

SEGMENT_LENGTH = 4096

def hashCalc(i, algorithm):
def _hashCalc(i, algorithm):
if algorithm == "SHA512":
return hashlib.sha512(i)
else:
return hashlib.sha1(i)

def decrypt(key, keyDataSalt, hashAlgorithm, ifile, ofile):
def _decrypt(key, keyDataSalt, hashAlgorithm, ifile, ofile):
SEGMENT_LENGTH = 4096
obuf = b''
totalSize = unpack('<I', ifile.read(4))[0]
logger.debug("totalSize: {}".format(totalSize))
ifile.seek(8)
for i, ibuf in enumerate(iter(functools.partial(ifile.read, SEGMENT_LENGTH), b'')):
saltWithBlockKey = keyDataSalt + pack('<I', i)
iv = hashCalc(saltWithBlockKey, hashAlgorithm).digest()
iv = _hashCalc(saltWithBlockKey, hashAlgorithm).digest()
iv = iv[:16]
aes = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = aes.decryptor()
Expand All @@ -38,21 +37,21 @@ def decrypt(key, keyDataSalt, hashAlgorithm, ifile, ofile):
ofile.write(obuf)
return True

def generate_skey_from_privkey(privkey, encryptedKeyValue):
def _generate_skey_from_privkey(privkey, encryptedKeyValue):
privkey = serialization.load_pem_private_key(privkey.read(), password=None, backend=default_backend())
skey = privkey.decrypt(encryptedKeyValue, padding.PKCS1v15())
return skey

def generate_skey_from_password(password, saltValue, hashAlgorithm, encryptedKeyValue, spinValue, keyBits):
def _generate_skey_from_password(password, saltValue, hashAlgorithm, encryptedKeyValue, spinValue, keyBits):
block3 = bytearray([0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6])
# Initial round sha512(salt + password)
h = hashCalc(saltValue + password.encode("UTF-16LE"), hashAlgorithm)
h = _hashCalc(saltValue + password.encode("UTF-16LE"), hashAlgorithm)

# Iteration of 0 -> spincount-1; hash = sha512(iterator + hash)
for i in range(0, spinValue, 1):
h = hashCalc(pack("<I", i) + h.digest(), hashAlgorithm)
h = _hashCalc(pack("<I", i) + h.digest(), hashAlgorithm)

h2 = hashCalc(h.digest() + block3, hashAlgorithm)
h2 = _hashCalc(h.digest() + block3, hashAlgorithm)
# Needed to truncate skey to bitsize
skey3 = h2.digest()[:keyBits//8]

Expand All @@ -62,7 +61,7 @@ def generate_skey_from_password(password, saltValue, hashAlgorithm, encryptedKey
skey = decryptor.update(encryptedKeyValue) + decryptor.finalize()
return skey

def parseinfo(ole):
def _parseinfo(ole):
versionMajor, versionMinor = unpack('<HH', ole.read(4))
if versionMajor != 4 or versionMinor != 4:
raise AssertionError("Unsupported EncryptionInfo version")
Expand All @@ -87,59 +86,22 @@ def parseinfo(ole):
}
return info

class OfficeFile:
class OOXMLFile(base.BaseOfficeFile):
def __init__(self, file):
ole = olefile.OleFileIO(file)
self.file = ole
self.info = parseinfo(ole.openstream('EncryptionInfo'))
self.info = _parseinfo(self.file.openstream('EncryptionInfo'))
self.secret_key = None
def load_skey(self, secret_key):
self.secret_key = secret_key
def load_password(self, password):
self.secret_key = generate_skey_from_password(password, self.info['passwordSalt'], self.info['passwordHashAlgorithm'], self.info['encryptedKeyValue'], self.info['spinValue'], self.info['passwordKeyBits'])
def load_privkey(self, private_key):
self.secret_key = generate_skey_from_privkey(private_key, self.info['encryptedKeyValue'])
def decrypt(self, ofile):
decrypt(self.secret_key, self.info['keyDataSalt'], self.info['keyDataHashAlgorithm'], self.file.openstream('EncryptedPackage'), ofile)

def ifWIN32SetBinary(io):
if sys.platform == 'win32':
import msvcrt
msvcrt.setmode(io.fileno(), os.O_BINARY)

def main():
import argparse
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-k', dest='secret_key', help='MS-OFFCRYPTO secretKey value (hex)')
group.add_argument('-p', dest='private_key', type=argparse.FileType('rb'), help='RSA private key file')
group.add_argument('-P', dest='password', help='Password ASCII')
parser.add_argument('-v', dest='verbose', action='store_true', help='Print verbose information')
parser.add_argument('infile', nargs='?', type=argparse.FileType('rb'))
parser.add_argument('outfile', nargs='?', type=argparse.FileType('wb'))
args = parser.parse_args()

if not olefile.isOleFile(args.infile):
raise AssertionError("No OLE file")
## TODO: Support aliases?
self.keyTypes = ['password', 'private_key', 'secret_key']

file = OfficeFile(args.infile)
def load_key(self, password=None, private_key=None, secret_key=None):
if password:
self.secret_key = _generate_skey_from_password(password, self.info['passwordSalt'], self.info['passwordHashAlgorithm'], self.info['encryptedKeyValue'], self.info['spinValue'], self.info['passwordKeyBits'])
elif private_key:
self.secret_key = _generate_skey_from_privkey(private_key, self.info['encryptedKeyValue'])
elif secret_key:
self.secret_key = secret_key

if args.verbose:
logger.removeHandler(logging.NullHandler())
logging.basicConfig(level=logging.DEBUG, format="%(message)s")

if args.secret_key:
file.load_skey(binascii.unhexlify(args.secret_key))
elif args.private_key:
file.load_privkey(args.private_key)
elif args.password:
file.load_password(args.password)

if args.outfile == None:
ifWIN32SetBinary(sys.stdout)
args.outfile = sys.stdout.buffer

file.decrypt(args.outfile)

if __name__ == '__main__':
main()
def decrypt(self, ofile):
_decrypt(self.secret_key, self.info['keyDataSalt'], self.info['keyDataHashAlgorithm'], self.file.openstream('EncryptedPackage'), ofile)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

setup(
name='msoffcrypto-tool',
version='2.2.0',
description='A Python tool and library for decrypting MS Office files with passwords and other secrets',
version='3.0.0',
description='A Python tool and library for decrypting MS Office files with passwords or other secrets',
url='https://github.com/nolze/msoffcrypto-tool',
author='nolze',
author_email='[email protected]',
Expand Down