Skip to content

Commit 3f6a96a

Browse files
committed
wip on microsoft office document decryption support
Other than the raw implementation this hasn't been integrated into sflock further yet, but that's yet to come. Thanks to nolze [1] and Mitsunari Shigeo [2] for their research, on which this is fully based. [1]: https://github.com/nolze/ms-offcrypto-tool [2]: https://github.com/herumi/msoffice
1 parent 775121d commit 3f6a96a

File tree

6 files changed

+143
-0
lines changed

6 files changed

+143
-0
lines changed

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
install_requires=[
2828
"click==6.6",
2929
"olefile==0.43",
30+
"pycrypto==2.6.1",
3031
"python-magic==0.4.12",
3132
],
3233
)

sflock/abstracts.py

+10
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,16 @@ def process_directory(self, dirpath, duplicates, password=None):
103103
shutil.rmtree(dirpath)
104104
return self.process(entries, duplicates)
105105

106+
class Decoder(object):
107+
"""Abstract class for Decoder engines."""
108+
109+
# Initiated at runtime - contains each Decoder subclass.
110+
plugins = {}
111+
112+
def __init__(self, f, password):
113+
self.f = f
114+
self.password = password
115+
106116
class File(object):
107117
"""Abstract class for all file operations.
108118

sflock/decode/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Copyright (C) 2017 Jurriaan Bremer.
2+
# This file is part of SFlock - http://www.sflock.org/.
3+
# See the file 'docs/LICENSE.txt' for copying permission.
4+
5+
from sflock.abstracts import Decoder
6+
from sflock.misc import import_plugins
7+
8+
plugins = import_plugins(__file__, "sflock.decode", globals(), Decoder)

sflock/decode/office.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Copyright (C) 2017 Jurriaan Bremer.
2+
# This file is part of SFlock - http://www.sflock.org/.
3+
# See the file 'docs/LICENSE.txt' for copying permission.
4+
5+
import hashlib
6+
import olefile
7+
import struct
8+
import xml.dom.minidom
9+
10+
from Crypto.Cipher import AES, PKCS1_v1_5
11+
from Crypto.PublicKey import RSA
12+
13+
from sflock.abstracts import Decoder, File
14+
15+
class EncryptedInfo(object):
16+
key_data_salt = None
17+
key_data_hash_alg = None
18+
encrypted_key_value = None
19+
spin_value = None
20+
password_salt = None
21+
password_hash_alg = None
22+
password_key_bits = None
23+
24+
class Office(Decoder):
25+
name = "office"
26+
27+
def get_hash(self, value, algorithm):
28+
if algorithm == "SHA512":
29+
return hashlib.sha512(value).digest()
30+
else:
31+
return hashlib.sha1(value).digest()
32+
33+
@property
34+
def secret_key(self):
35+
if self._secret_key:
36+
return self._secret_key
37+
38+
# TODO Add support for private keys.
39+
if False:
40+
rsa = PKCS1_v1_5.new(RSA.importKey(self._private_key))
41+
self._secret_key = rsa.decrypt(self.ei.encrypted_key_value, None)
42+
return self._secret_key
43+
44+
if self.password:
45+
block3 = bytearray([
46+
0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6,
47+
])
48+
49+
# Initial round sha512(salt + password).
50+
h = self.get_hash(
51+
self.ei.password_salt + self.password.encode("utf-16le"),
52+
self.ei.password_hash_alg
53+
)
54+
55+
# Iteration of 0 -> spincount-1; hash = sha512(iterator + hash).
56+
for i in range(self.ei.spin_value):
57+
h = self.get_hash(
58+
struct.pack("<I", i) + h, self.ei.password_hash_alg
59+
)
60+
61+
# Final skey and truncation.
62+
h = self.get_hash(h + block3, self.ei.password_hash_alg)
63+
skey = h[:self.ei.password_key_bits/8]
64+
65+
# AES decrypt the encryptedKeyValue with the skey and salt in
66+
# order to get secret key.
67+
aes = AES.new(skey, AES.MODE_CBC, self.ei.password_salt)
68+
self._secret_key = aes.decrypt(self.ei.encrypted_key_value)
69+
return self._secret_key
70+
71+
def decrypt_blob(self, f):
72+
ret = []
73+
# TODO Ensure that the assumption of "total size" being a 64-bit
74+
# integer is correct?
75+
for idx in xrange(0, struct.unpack("Q", f.read(8))[0], 0x1000):
76+
iv = self.get_hash(
77+
self.ei.key_data_salt + struct.pack("<I", idx),
78+
self.ei.key_data_hash_alg
79+
)
80+
aes = AES.new(self.secret_key, AES.MODE_CBC, iv[:16])
81+
ret.append(aes.decrypt(f.read(0x1000)))
82+
return File(contents="".join(ret))
83+
84+
def decrypt(self):
85+
try:
86+
ole = olefile.OleFileIO(self.f.stream)
87+
except IOError:
88+
return
89+
90+
self._secret_key = None
91+
92+
info = xml.dom.minidom.parseString(
93+
ole.openstream("EncryptionInfo").read()[8:]
94+
)
95+
key_data = info.getElementsByTagName("keyData")[0]
96+
password = info.getElementsByTagName("p:encryptedKey")[0]
97+
98+
self.ei = ei = EncryptedInfo()
99+
ei.key_data_salt = key_data.getAttribute("saltValue").decode("base64")
100+
ei.key_data_hash_alg = key_data.getAttribute("hashAlgorithm")
101+
ei.encrypted_key_value = (
102+
password.getAttribute("encryptedKeyValue").decode("base64")
103+
)
104+
ei.spin_value = int(password.getAttribute("spinCount"))
105+
ei.password_salt = password.getAttribute("saltValue").decode("base64")
106+
ei.password_hash_alg = password.getAttribute("hashAlgorithm")
107+
ei.password_key_bits = int(password.getAttribute("keyBits"))
108+
109+
return self.decrypt_blob(ole.openstream("EncryptedPackage"))

tests/files/encrypted1.docx

23 KB
Binary file not shown.

tests/test_decode.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright (C) 2017 Jurriaan Bremer.
2+
# This file is part of SFlock - http://www.sflock.org/.
3+
# See the file 'docs/LICENSE.txt' for copying permission.
4+
5+
from sflock.abstracts import File
6+
from sflock.decode.office import Office
7+
8+
def f(filename):
9+
return File.from_path("tests/files/%s" % filename)
10+
11+
def test_decode_docx():
12+
o = Office(f("encrypted1.docx"), "Password1234_").decrypt()
13+
assert o.magic in (
14+
"Microsoft Word 2007+", "Zip archive data, at least v2.0 to extract"
15+
)

0 commit comments

Comments
 (0)