From a13bb8c4e79efdfc57bd55324b94c98aa4a804d3 Mon Sep 17 00:00:00 2001 From: lisp3r Date: Tue, 26 Apr 2022 02:18:48 +0300 Subject: [PATCH 1/5] Support of encypted documents --- doc/html/opendocument_8py_source.html | 4 +- odf/odfmanifest.py | 55 ++++- odf/opendocument.py | 115 +++++++++- setup.py | 2 +- tests/examples/enc_sample.odt | Bin 0 -> 13010 bytes tests/examples/enc_sample_old.odt | Bin 0 -> 33667 bytes tests/testenencrypted.py | 301 ++++++++++++++++++++++++++ 7 files changed, 462 insertions(+), 15 deletions(-) create mode 100644 tests/examples/enc_sample.odt create mode 100644 tests/examples/enc_sample_old.odt create mode 100644 tests/testenencrypted.py diff --git a/doc/html/opendocument_8py_source.html b/doc/html/opendocument_8py_source.html index 2bcbb52..a2bc5ff 100644 --- a/doc/html/opendocument_8py_source.html +++ b/doc/html/opendocument_8py_source.html @@ -1093,7 +1093,7 @@
1000 # an open readable stream
1001 # @return a reference to the structure (an OpenDocument instance)
1002 #
-
1003 def load(odffile):
+
1003 def load(odffile, password=None):
1004  assert(type(odffile)==type(u"") or 'rb' in repr(odffile) \
1005  or 'BufferedReader' in repr(odffile) or 'BytesIO' in repr(odffile))
1006 
@@ -1104,7 +1104,7 @@
1011  # Look in the manifest file to see if which of the four files there are
1012  manifestpart = z.read('META-INF/manifest.xml')
1013  manifest = manifestlist(manifestpart)
-
1014  __loadxmlparts(z, manifest, doc, u'')
+
1014  __loadxmlparts(z, manifest, doc, u'', password)
1015  for mentry,mvalue in manifest.items():
1016  if mentry[:9] == u"Pictures/" and len(mentry) > 9:
1017  doc.addPicture(mvalue['full-path'], mvalue['media-type'], z.read(mentry))
diff --git a/odf/odfmanifest.py b/odf/odfmanifest.py index 2e2c9b5..54005d9 100644 --- a/odf/odfmanifest.py +++ b/odf/odfmanifest.py @@ -38,19 +38,21 @@ # #----------------------------------------------------------------------------- + class ODFManifestHandler(handler.ContentHandler): - """ The ODFManifestHandler parses a manifest file and produces a list of - content """ def __init__(self): self.manifest = {} - - # Tags - # FIXME: Also handle encryption data self.elements = { - (MANIFESTNS, 'file-entry'): (self.s_file_entry, self.donothing), + (MANIFESTNS, 'file-entry'): (self.s_file_entry, self.donothing), + (MANIFESTNS, 'encryption-data'): (self.e_file_entry, self.e_file_entry_close), + (MANIFESTNS, 'algorithm'): (self.e_alg_file_entry, self.donothing), + (MANIFESTNS, 'key-derivation'): (self.e_key_der_file_entry, self.donothing), + (MANIFESTNS, 'start-key-generation'): (self.e_key_gen_file_entry, self.donothing) } + self._encr_el_key = None + def handle_starttag(self, tag, method, attrs): method(tag,attrs) @@ -81,9 +83,46 @@ def donothing(self, tag, attrs=None): pass def s_file_entry(self, tag, attrs): - m = attrs.get((MANIFESTNS, 'media-type'),"application/octet-stream") + m = attrs.get((MANIFESTNS, 'media-type'),"") p = attrs.get((MANIFESTNS, 'full-path')) - self.manifest[p] = { 'media-type':m, 'full-path':p } + + self.manifest[p] = {'media-type': m, 'full-path': p} + + s = attrs.get((MANIFESTNS, 'size'), None) + # only encrypted entries have 'size' attr + # so there we assume that the next element will be encrypted-data + if s: + self.manifest[p]['size'] = s + self._encr_el_key = p + self.manifest[p]['encrypted-data'] = {} + + def e_file_entry(self, tag, attrs): + self.manifest[self._encr_el_key]['encrypted-data']['checksum-type'] = \ + attrs.get((MANIFESTNS, 'checksum-type'), "SHA1/1K") + self.manifest[self._encr_el_key]['encrypted-data']['checksum'] = attrs.get((MANIFESTNS, 'checksum'), "") + + def e_file_entry_close(self, tag): + self._encr_el_key = None + + def e_alg_file_entry(self, tag, attrs): + self.manifest[self._encr_el_key]['encrypted-data']['algorithm'] = { + 'algorithm-name': attrs.get((MANIFESTNS, 'algorithm-name'), "Blowfish CFB"), + 'initialisation-vector': attrs.get((MANIFESTNS, 'initialisation-vector'), "") + } + + def e_key_der_file_entry(self, tag, attrs): + self.manifest[self._encr_el_key]['encrypted-data']['key-derivation'] = { + 'key-derivation-name': attrs.get((MANIFESTNS, 'key-derivation-name'), "PBKDF2"), + 'key-size': attrs.get((MANIFESTNS, 'key-size'), "16"), + 'iteration-count': attrs.get((MANIFESTNS, 'iteration-count'), "1024"), + 'salt': attrs.get((MANIFESTNS, 'salt'), "") + } + + def e_key_gen_file_entry(self, tag, attrs): + self.manifest[self._encr_el_key]['encrypted-data']['start-key-generation'] = { + 'start-key-generation-name': attrs.get((MANIFESTNS, 'start-key-generation-name'), "SHA1"), + 'key-size': attrs.get((MANIFESTNS, 'key-size'), "20") + } #----------------------------------------------------------------------------- diff --git a/odf/opendocument.py b/odf/opendocument.py index 25d736b..46166c9 100644 --- a/odf/opendocument.py +++ b/odf/opendocument.py @@ -24,9 +24,15 @@ __doc__="""Use OpenDocument to generate your documents.""" +import base64 +import hashlib import zipfile, time, uuid, sys, mimetypes, copy, os.path # to allow Python3 to access modules in the same path +import zlib + +from Crypto.Cipher import Blowfish, AES, DES3 + sys.path.append(os.path.dirname(__file__)) # using BytesIO provides a cleaner interface than StringIO @@ -869,7 +875,8 @@ def OpenDocumentTextMaster(): doc.body.addElement(doc.text) return doc -def __loadxmlparts(z, manifest, doc, objectpath): + +def __loadxmlparts(z, manifest, doc, objectpath, password=None): """ Parses a document from its zipfile @param z an instance of zipfile.ZipFile @@ -895,7 +902,11 @@ def __loadxmlparts(z, manifest, doc, objectpath): from xml.sax._exceptions import SAXParseException ########################################################## try: - xmlpart = z.read(xmlfile).decode("utf-8") + xmlpart = z.read(xmlfile) + if 'encrypted-data' in manifest[xmlfile].keys(): + xmlpart = __decrypt(xmlpart, manifest[xmlfile]['encrypted-data'], password) + xmlpart = xmlpart.decode("utf-8") + doc._parsing = xmlfile parser = make_parser() @@ -972,7 +983,7 @@ def __detectmimetype(zipfd, odffile): # Fall-through to last mechanism return u'application/vnd.oasis.opendocument.text' -def load(odffile): +def load(odffile, password=None): """ Load an ODF file into memory @param odffile unicode string: name of a file, or as an alternative, @@ -986,7 +997,7 @@ def load(odffile): # Look in the manifest file to see if which of the four files there are manifestpart = z.read('META-INF/manifest.xml') manifest = manifestlist(manifestpart) - __loadxmlparts(z, manifest, doc, u'') + __loadxmlparts(z, manifest, doc, u'', password) for mentry,mvalue in manifest.items(): if mentry[:9] == u"Pictures/" and len(mentry) > 9: doc.addPicture(mvalue['full-path'], mvalue['media-type'], z.read(mentry)) @@ -1027,4 +1038,100 @@ def load(odffile): return doc + +# -------------------------------------- +# Encryption functions +# -------------------------------------- + +def __normalize_name(algorithm_name): + """ According to the OpenDocument docs (https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part3.html), + the algorithms presented in the "encrypted-data" section can be specified in different ways: + + - As plain algorithm name: SHA1, SHA1/1K, Blowfish CFB + - As IRI: http://www.w3.org/2000/09/xmldsig#sha256 + - With MANIFEST prefix: urn:oasis:names:tc:opendocument:xmlns:manifest:1.0#sha256-1k + + This function tries to normalize algorithm name. + """ + assert isinstance(algorithm_name, type(u'')) + if algorithm_name.startswith('http') or algorithm_name.startswith(MANIFESTNS): + algorithm_name = algorithm_name.split('#')[1] + return algorithm_name.lower() + + +def __inflate(data): + decompress = zlib.decompressobj(-zlib.MAX_WBITS) + inflated = decompress.decompress(data) + inflated += decompress.flush() + return inflated + + +def __deflate(data, compress_level=9): + compress = zlib.compressobj(compress_level, zlib.DEFLATED, -zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL, 0) + deflated = compress.compress(data) + deflated += compress.flush() + return deflated + + +def __make_key(password, algorithm, salt, deriv_iter_count, deriv_key_size): + """ Makes encryption key from password with addition derivation. + :param password: document's password. + :param algorithm: manifest:start-key-generation-name, the algorithm used to generate a start key from the + user password. Can be SHA1, SHA256. + :param salt: manifest:salt, base64-encoded salt + :param deriv_iter_count: manifest:iteration-count, the number of iterations used by the key derivation algorithm + to derive a key. + :param deriv_key_size: manifest:key-size, the length in octets of a key delivered by a key-developing algorithm. + """ + assert algorithm in ('sha1', 'sha256'), 'Only sha251 and sha1 are allowed' + sha_key = hashlib.new(algorithm, password).digest() + + return hashlib.pbkdf2_hmac('sha1', sha_key, base64.b64decode(salt), int(deriv_iter_count), int(deriv_key_size)) + + +def __decrypt_data(algorithm, iv, derived_key, encrypted_data): + """ Decrypt data. + :param algorithm: manifest:algorithm-name, the algorithm and mode used to encrypt a file entry. Can be: + 1. An IRI listed in §5.2 of xmlenc-core (Block Encryption Algorithms): tripledes-cbc, aes128-cbc, aes192-cbc, + aes256-cbc + 2. Blowfish CFB: The Blowfish algorithm in 8-bit CFB mode. + 3. An IRI listed in §5.1 of xmlenc-core (Algorithm Identifiers and Implementation Requirements): NOT IMPLEMENTED + :param iv: manifest:initialisation-vector, base64-encrypted initialization vector used by the encryption algorithm. + :param derived_key: the key derived from a password. + :param encrypted_data: the encrypted data + """ + algorithm = __normalize_name(algorithm) + assert algorithm in ('blowfish cfb', 'blowfish', 'aes128-cbc', 'aes192-cbc', 'aes256-cbc', 'tripledes-cbc'), \ + 'Unknown algorithm: {}'.format(algorithm) + + iv = base64.b64decode(iv) + + if 'blowfish' in algorithm: + alg_obj = Blowfish.new(key=derived_key, mode=Blowfish.MODE_CFB, IV=iv) + elif 'aes' in algorithm: + alg_obj = AES.new(key=derived_key, mode=AES.MODE_CBC, IV=iv) + elif 'tripledes' in algorithm: + alg_obj = DES3.new(key=derived_key, mode=DES3.MODE_CBC, IV=iv) + + return alg_obj.decrypt(encrypted_data) + + +def __decrypt(raw_data, manifest_data, password): + # Stage 1: Get the encryption key from the password. + start_key_generation_alg = __normalize_name(manifest_data['start-key-generation']['start-key-generation-name']) + derived_key = __make_key(password, + algorithm=start_key_generation_alg, + salt=manifest_data['key-derivation']['salt'], + deriv_iter_count=manifest_data['key-derivation']['iteration-count'], + deriv_key_size=manifest_data['key-derivation']['key-size']) + + # Stage 3: Decrypt data. + decrypted_data = __decrypt_data(algorithm=manifest_data['algorithm']['algorithm-name'], + iv=manifest_data['algorithm']['initialisation-vector'], + derived_key=derived_key, + encrypted_data=raw_data) + + # Stage 3: Inflate decrypted data. + return __inflate(decrypted_data) + # vim: set expandtab sw=4 : diff --git a/setup.py b/setup.py index bbebf57..dd599e1 100644 --- a/setup.py +++ b/setup.py @@ -115,5 +115,5 @@ 'odfuserfield/odfuserfield', 'xml2odf/xml2odf'], data_files=datafiles, - install_requires=['defusedxml', ] + install_requires=['defusedxml', 'pycrypto'] ) diff --git a/tests/examples/enc_sample.odt b/tests/examples/enc_sample.odt new file mode 100644 index 0000000000000000000000000000000000000000..cb508c25749e15d3528f7a1b817524ce047a32ae GIT binary patch literal 13010 zcmb7r1yo$iwk!3j=qcMtCT9rGuRnASvWD+*_+rJ+X0+yOl*M+Kod8h;=h6Y^8c4%|Eh`F*_v9I zIXnIX#)*XyXlG|_Wa!BFfAcc6wlf5p{BK^kf6;8Wk{I8mgp{<3fi4%~)(b!b8{1wR6_pRbpwC6@M zMu-dd0Put|@qQYsy~F0{4>f>PU^aRhmM_YSZie7qbxfsa)?whFcd;r!Jk;Tp$&Hdz z-NCq>UOV$xYWLc3NZg^5!fKircpf|RGGB&5ovQKyubs-LZ~6U^W9DloM&(w6vN;n4 zoqu(mJn|-xd?fcwIJ9)i3sE4`^$YGBP1BxG#~xV5O!Km>U7lF@7OeW2O)01mlDPPh zP9F^!(fG%(xc%_Q3}vLeC_%3&_9ich>sOc^majYAG;2`(%~sxJ{G(# zGJ4}4tPJVDa;b2mu%46JG3;X%LBD~(!?(E_B|dDBQp`=aX#nQUtqzCg?M}{{0)Nz+ z7xyD|3sU$PA;#T6NKV#-edAS{jMslLUY>A?4ai8^fofa_nT@~Vqbk9c)X^ZMzU4T| zLdhLOOHZV~GEI)phi=%VZ?uLD>rlL@i62mkD%e8JgH!AuE8@Ud{T!0J#uLL+>{*Hzs?C`bQ#_$=KFGKsxjnLv8ZmHd*U18Q9Ybt?q7@;AONc%}* zC3iNsXPSNPIYQ#qBzo<>Z3wO{ZZUsB#80U&%V=g$D1Xx*Z7>CrdMZwZ9p7aydd?^P z$Zd69nXla@)NI3Xg@FD{LUTDbmF<2q|9CvsRiVY#UbGB<6+@nG!&CE(QWj#lyH=mP zH`mDc+L(QQ>qi+ z5l?K`sBA;CE%ne;W`MguIDcmgC#p@Gp8coGhAJQDhI6j9bs~1w>5)nze}AK&IGQ|g zN;p|3wp@#&y{*CPj}{w%_rOHWh=#zVPk=@{gc-25ZrHBQI3Byf;QoU};!&+m644pU zB(E=*pV&M?2WK*<4W%Dn#tq0BFfSHrTC`%ii$r3w|Z2%H0^hYmiFXu|r0r_42i+=uU=K2Q%ng9(M+-$5b;d7}PW+XgZ*^FM=hT+a24PSJiCL=qG zoEy`<^;;jid)%jZC5IA=8kU1B1Y(A(v?94w+jqdcw>;lX18Nee*v4UWJCt4dY3q&i z;-~MG6rkNt^bI~->F<`XJTriH73!?P6p@Hfl1rcKUO8cqE!Ws3HflI;<{4|N^rbvX zDiR#_xDk&j8NTzMMe9D%@v-@)2tJe7P1Qj>mN@An@iO9MwlYy&tyhk5qRAb{JNTKR zwxAm&Nm&Z(qSx^p`!1puA-OHD9RzPMif-3heBKB8%;Ebnx|kSSe)MBjFjSFWPqz>f z;flM3HBHM`DK_kIkY#k4;aNp%L102rkS152 zIW;*4*HLQh%noY1{USd@NH=X6TpY}IGwaMj4xnMJvR7y&dJnkiSj{vPumnBlBU?jG zq>)aEYn-NBO1Db@BKi`+fW8Xi3t9CQtT2Z?Gic}L$iIoem?1ShfpsaL1sED;!J!XA z&%-6a)hc5}T`h*a?Z1(Wcx~9Se?++C=Hg)@M9wv$cQdf`R}EPEDv53Gh?I>GUOnlP zHx7b_^i4Dn`R$7nRUWsae%U5f#03okG@&^!S+=#ujWc!rLOl2Ga?bu~WTMhZteV4P zrXR_>GY|aFt@lw=SyzlEw~)Jw_xt?LcLgTKViF+}o9A;wL%t99KH{}bqce`TU#x~J zne89|0St=3IDZHgn3{l?opWj(z6AjN$vay=d8S2&2T`pMsSPnsT`xe-k2 z!!9Eva+ScuEqmheBF2)_FC6<=3vJ%ne=e6R-Ds}s!9IBnlbunx+PgcT1pkF{JWALs zm0R*Oq+{1MWJa;|Ypm*KrGIpQ_(Yu`ZE)Yq@s5z|4em;jlQ+clWqh3D!2z#daMIfW zQztD+p%3(HB^drgFZuCxgb`5zdh~-uKi3vmDaDMCHmRTvCGn%!SkE>tv_<0U?NQ|H zvYn57a9goHVBrAdKnUu|=Axcqo5zSy$?~I}VJL47D>XVr_{2czso37DQJXBjn74*X zhn`btJ1luAl{ONxC-o)03y+Zdg219ar)(Pw=I&l*!@?|&h`1v~kRSCg;v+(9k#%#V zzq`pj2)By&ynGD_HC(P#lY{V}+&8j)#&KFgX#S`uTL=+;I+zgd(9PE<>hul?nu8PA z8lmQL#ul(LI2TUQRYq>_R^Ou`d3Ks+^sdN-;w!YK*3nYV1mYTWivf2Unk(3~Y?E+s z=lu-+vDOOSTxvKcYxXF)xQ`|`Un+c0_4h0~l&BAh3<(1AyZrqu`g{H7WC8?Q*qS;0 zo=Gtt`lsKRkfsCD$Hri@)J@A(kxWE+1FXWxZtB7^(}(VSlGT7UACC;KlLtHRIxOqIcb2GyrjrNC!lTQXjHEeH4?y- z>3eFpFU`*Zdn)RjmM$pay`paKAR<_YRznQ$Dj^&)iB--I zN4oiT#5B9y3YZJO7hWvqtyPIEUdAwC*S+Hm7O)GbYSzD6re27G60MHnKPPE0-}Cmi z3sV)SqY@1-8eTx7h~plauC1L?mz^q`PTSE`5?_*4CK3?hL3~U$j-dPLLH;F|M5zbD zTh;#iQj=eN_nLsywU9SpE4kt&v;7d`)+1sr}nhZ4GF{M1> z7u|28AN)8qEGT`2iPdXMizjNltyh?1_dWBXZzL+#0{{kOnh2;t%nP{_G)MUxJF=(d zFqtE*bz?7=$L@vMr|gKVErCKb`Fz@V;kv1tsY92Q*siVu(LxGu0UYtX@KYSpf7hzNC{gO4LFI1sC zk6%vumBZ+jxbBAJKqbD&n19Eyh+WG*fcjL3y5Q9?A+ATrH}jD_?5k^woE=E$47rqS zQ1eBd9WHAs7HF^?g0`l3CKr9EaEtr%D`rJk8%C8s^0yk)sO^Lz8#3{6>0pE;)Ib`2G| zd=>1q8NHrA-|a2(RWM~EM(Ef}QmPt}Iiu4b)>lSXV8)%rEugtB$}+#nN8MUvX|)}O z$7RPBV`J^Pp9=rP4$#@l21Bz?eE?r{OGiE7Xb}R@3rkGa{gT4Fe1w!$w`dF=?r?X8 z-?n2>qcwA}FmMo?K#Q5&?8bB2$%JWXL}qzJ&37heF&>~A^rPVb*D>x80buyN8<=Yr zy0&^OWc@+xi3Xb`k}X8t+P8gkY-RDYfx($BcIlC3PaCH|Q+RZ+C*A@M6Z)J8GZ@H+ zo9-_#APnr9)RV_-1n3UU9O-(>x81fMIIK6F!?MAHbiTMP3*1v*bv01RZ(gd$1mp8GcKo+PM_hDFI0`2y?}qKk}BF79UGoP&b^z# zg?abc_iQ|RVPl%i*}XvT>GFoBoFp@YYb*Om?n}JeK4p-4#38b=Ope7(H5J#%8l>zT z3gP7k-ZE#{Ns*|`8@vQciwBGn?YnBc8YAJuAyY1KjntXtN%e7T>jblYoZbVnn^$=) z3{#{Sh~#>Wd$%yh@1S9khRMggU2rWy=%BfqNLZ+Y%HO2biT1SE+2+z=4CFZ4An;Qn)+4Ur-DGWI$w3@x=>attmt=50Io@ zPxc3>(y?X?euW|L<3uyPBYzo*7%l#Jc)HuK{uH@)W$DYIfh2jw7iw)Vj~M0=q}CsRSPCdV_zhp}liN&06loCNCf-VT{Nl zCWZdzbyr!cuThkZcG=Kl`5Jk!lPo7Xj7BKtTKroDi>Hq1nJ&lq^G?#7f3lOcjUpb$ z>)1ufsCmp9IQ-9~+Z0sib;=3>1C~p# z?&fV|K|L{CO4=M<`8E1R;DX?4aC$28o1C8V-MvdFLr3|&pR{_{i3&zuJ{9exN1>Jp zA{iM9+G%&E1Azjkg8`Q5#6LCsNKN|no$(}hiRFjYFZ&1ykC((F3^7a2S0RU=^u7xg zD88nppZ9^wOPS18Y;=Up0!90xs$k-)(~t_pT%SvKeWPoUn@;fOuB2k3=hNwf>N~(F zG{krqyt)da`ma8ZlJWK0onS_+89K4c3J4ScqEQ%nXQL-bUVh* z+;*KpE2Mx-pvBW7F4h@&iukg(yUk4KlVPuS#|3Vo(aC(?u;cw8E40IP->w=(krTehwYRmI7^y}i+4tC3`d)Beh(2Y%t=S%ASpaW2%%xJL=q)!q z{{s+pU;kdly#RLpK&8)ud~?uNtNbXmU>0<<2h!x30*W>>57xuHvjSFNkSa0<8oQ$s zUThoPNgz6*G&7LRly})>A5ZeKnIO6u%`?{vp6gv=PSzg7Bw5@{I)-?uBy3OaPpf%x z*pMR!f(zL(HEU+FF%ZC+5>N@jF;!E>xwZN#s$;u zW-MM2J2wbv6>L&#_5J61pRJzKEjNMk9WXdqh@oalO1>+sTExO0*`jL{L>{b%+GgYm zomr3{fk{wHD{V!4PZT;b(T7toTe*&oGHKHly0vS2h@-d&UXnmZQOyiyv-=~VAUt&< z__P=m7e%`>-YqCeU?~<^f1}##*SZa*bFy}hb8A9{0J`R|qJ zS9z_kiXVQhLVuTkY(oFe=M#VJX#Z;)@>k{8k04G!cWaY>tV4s{q=xG_HzlSb;ijCQ z-#Zi=d$u<6yKuZC4d|W1xkYyaO$hx+JIM88jpiCl=#d0FY_KjY6kJ1=7d4deyIdT{-w^)09%&$R2QQhYp1&%YH^{= zy+yL#Zh&W;*teG%%;J%H>xcfxfm`cVPpINgYR*No`D_b4Nt}1+@w+H-`}3R^vf?y3 z(PF7h`rCd(;|fGzKQ#NOCE^l?Us8jlow06YpvLy8aLfgeVw2A#9q<|6R+qQUoLq8! zsOG2DOzV@U#Y&S_NY3xN%Y+NJdt3pNU|F*Rg2eR{>5g^Wcknq@Aj_hc5=H}4-swwr z;aIWNpIxDu_F773XTyuCDIM@EKF@ghtll#ve=tA7Q#^yK*&&fr8S71NnHHPpuY{ik z=Va+csh=zeF23lWOo^hO-_tnw#)nW0%5Jd|H4U88LE}`% z+A^c}fmTJ@(w|4#=<{iiQd&+Fj7|;k`&zq+`nqI7nsZx}; z%wg{WbAj&;-00Okc!<+uNoL#xhJk6np^yx11I&X$D*XK z9ugP~SHX@Y$ z?iL*3eRN4VwP-bU{a zYkZzyb0|rJo2(=!wZt)G-r9PN)N?=b@|yta zR-*Aj(tGYfpJ{-2NbKAL%C-#Uq7SfJZ=PP2_IS7?bsb|BKtjXOpGc>qp!mU93i{9 zNpGw6Wr#R<-i254%2jA6QtRr9Ah&jzksfa^G1BRg@~z6sPkiUPm-Q+wW zK_fA9b2(@)&+U04kRv~R1&qJxmOy6;2Dj3m4GKMSAJ+wTuo)9g%C1u?sca#RYWBdG z;Q`RRK2-%pE(GGITzMd7-HI|bD!oFXd~#2Uw;W*(X~g-_;1R28V6BX^^>K-O2KDM= zc(EF6Fi(oYlf&Hj=^S%{uVkW2dG`GQbg@us<QxE913}Dg0p&&OuVH!TyEFb%p`K7?_s%FArZ)S? zBOK=$}-%s4`#EKZ^<^T7~w;1=6^os4~jZth*|M8(#`)LJR8#S5CV=r&)EZ z!63OaR&gQgQY4GMKoB;&c<}chGLJLHH)f2Ps?r64Vm`<|5IkTuQ?qnO<{{#iH7OB)ANFNcfVC)9|o<m0~T26VijnAX` zdEB8;Er8U4H}2qPAahh5cQ4oEg`PDFkXgyKOVK%f60r<;V^)ZZhDE+M~K6QL&VK_h}YswWen z7(x&4e8{lWfS#M3-Ce({!*o7Lso3F+dTeK?`)g9a#Bo1SF5`|8524|e$_Tq#=rWBB(pehbur+-fGS%_fzXX+a;| zqoPTYJBg;x{1Eg>J)W9c{xl#{%Eqodm3@cVI5PnU`G&_m_2!0ysY45R0X+J%j^ovQ)I#BK_knQ!+ zl|?0tzw}4gYW8Bv6w~VAQ>TBR?SKC$U}?b1wr{&h#+fF(1@hhh>h)R3h!!4s>sMb^ zKT^dCj;R!m>Ke~my)1c+z`JLwW*Y6J`UA7%KgqzCf?7r5yePPe6wCMyrUBzc!;ljhj&-hFQhr~*Emwet?Vqd+xGp!d`Yw==$Y z`Im}Qv^hcT+@NtP=(lOiQ<(9O<1t2BGiALGQyc8J_8=oqBN=KYZo~rA7Y1Hrn63AKEO;?L~q%bazM7^hx@4krc<9Tvm&c;WZG>lAsNYatse`IWWl}qR%I5JPnZ(y+>>eSZ?98!*5VhQ z+oS+n(t{FKKNl=m>e*?BL$jG6FDtxsPXWLN^am>Q?f06CRK>{4Z~fBy_ZK`Vy9|)v z`Onu)X_^ih?3a*RT=wDE{b*C6~q^__FqzEl`gARF_R7Z zB`{QCEP9`Z1)MjEG%2vmV>4RxTTcAO#|=~kst#yMK&k2oxCqIW9KnWuf#-jO3 z1_dj~zB)X+HtrnD)pUa5>p}B@a5lRpaGxH0Gx0#nGm_7*ic=q&ve}mKR0RhHF-z## zAs~H=Tfajh=4-?z?QUlr_mR}PZ{4h~@zZeWev-GQPav5zuo~UTJ|xlRpCw*%H7M;n1;j1yD;)Mk%@$PLT8azTeCSUH-5R=2 zDp2zF@Q$PNFB+^FNS0?%g_2>wb&iI@+yyr`{nE*WTSxW{L4VQ_2OcCe2|_kQpm?<( z%nsOmAB>Wj9>T$@P0GfWzzP8>RSOfP%Vq<3ov?SvSWl1S!G|g%<-_psP78RkS0!8X zhj|}8zNo4rtz!O7*c!WexRzM0TR|vC{HgmCnta)hqa8?BfPJ^5qN+?^Z|{bagGVU! z=4}%-by?a_CD)zq(97r857=;i%p88bldKSXTLCg;{Wce-8GW)<@E@C`{MagvNV3_g?{$pd-DbSUHm;HHbto>@zpp<1RK4!BlmWJe|=~Pzt0Hk+ zk~Rc$w(x@%3lk&3z;;x0E)kotk&o=gy6Xi?g4rCyYDl1=ASy*ZK$t~&!FzN>UEzaC zh3H4`(9s1&i?|{v?w~9NjkNB5guuLwX}!F!ec%(@+==v{)p95b8eJ!%E*2$MzVPc8 z-<>vTuXvU6Ja4`YJ4KFv=xHnxG0K~(G8@IJSzMDr&>e0Vq{?r+_3gvwHa-+q+Rzi1 z60~`nmBE>`Dj}wPa_ADI((#jFQ)HtuXc)SaD08$zM8#r22}^&hx<=O}Vr|7NuFnmo zs(e_ec^(d_=w$}21^eEitePzoD1Pd0c$YQLW+skbr14a3l*mSPXE)`g{`A<3w$wG4 zVm^pB9lZ9#7+Lg2K4^XCS@e{VB~YcYT>!}$vp=I8R@45`!Dx_D zG=-P%5K^TT*Brgc)fc{w?Q#`X%^`HaZ29|;cNYh*usww)<8_XiU# zYERbfZMPOMPTwNQ%*%SVbFjo|Pheofw4egD9@Wo0``hxIHTZVEy5k*pFendyi22u; zmcyFso3k@)bOV>0kjTJJ3(9&;Mz?OQJiy!u9KxKAJE2^=F(&1c`7_KWkR&sDS3Dy+ z_ekV^jyGScydinIyjijKil4Cnn>x_%Fj>Glhv17fCUg^p4bFMf9GC$V9hw> znC-PBTJ#r}H1qGyQ5M+Q1gnJ&aVE!o$>@1!VQv9~u>^DpVj=yeGa&UNZItlfs-Yz+ zrTUas6GJ*K_rsJPos+1ubE$Y{2N4A*H2Yj81^(81)U{G)lt?z_!*p28z4H~P3|E|C zRGX#<8P{2yEDu;qSN$F7`+0~6mXjUl4ovegs29z!()}+;+K4Ymkfqok+Q0Uz|D?X% zjX=`D)4UPiTqNhJC_G?l#chbNN;_0q5K(OYQGPSk4~cw4BS`TPEx zU>T!f@y5EyQeD=HN!&0NmEYW=rlSj(i8kFW@1>lFgQ~dgEj>Pt*;*kK!DrxbJ5`20 zZ-)b3X!1I-_?vhu?^A1ppg}->mwys3%>UTn!}vwK^5Uw(^wOUs82_a?{6)cMDLPL1 zGRQ+4_bQsF1?0rJ#29rLKT>aT3%1V?9Q=9>TgH)l6nuN4V7a+%Z5+$v`GP19kB;6H z6y6P59^yPo9g#kQc~>$!C!5iLWV|67=>Trrn~RwQ8M^h{ph7T-Rr%LcuiR7l1T5w7 z4d*#w$Y?nDgj6ldeLyb1a{`bI$_4Hf&+avk&Z5$V3DLX8)KP+}M=H`Yl@>6sYUwG6gYcTqnPY-P zF$aJMUsHv(5AohvfE*!}TG5wnyOel% zXO-PjnVxFbh2#ogNUbqF*F$|Ns0UdXCc?}W$#!`^+4_Ngt(0Hyk>74RZ?d>Trpy

WgQp~UDUR*?d#uF zMP=%Tv)G|CLVGP_(u-oo*S33C-+plGZ#xz}=6B=yp&yS|X~jb`7fuPq;-6@?LsQ(6 zMje5cVNmWAL*p7~%gc#sTR-y3^V%9|VdqJRS$W3JTl@81WL3X7+$cD@N84dIxq^%7QhLg{wLd4qqs*APSx=$On`34@Y5eHVUWoVeKCA@fj|P z)%>uFQa3ywtEM1JLH*jAOX}nz2nsZD-wfsolkYg6j!h7bD3L)<5>#YZ5d^ zow-hYUwX@Vxa1_h=E4iD!2GbS@mNWz>FOdhLsDniJgON?hw4O_n_y30Vr6tE;<@U^ z(thFY1VxfA;}n1+_%Q54Xe-e>?(n>-NtyLcSgTl;`V^tBcd)~WJJ8CN(v9o&!EUHl zw9s-HxU^8JJlBX#M=5D&xqULJ#itQ$%zT)%WT*Bm(m5qx~zQ-y{9! z$YN;;I0#4%%I}d53Wg5y_ww{#!~IY8PqF%cdj07Zkl#A=KTQ5t_3u^b|K9c=ULgO0 z68IM=f2h>|9_6Lr7-&O5@ zkM!^TGW-jqKdapT9_Qa9O8X0(e^50oRPz6PiT~;OXRXz5UFshO_KOmK{Y&E?D%Jn=`t!Ni zf3)}O^3STQKYRW@H~Vv@`}^GN51YaK^C{YY`u&-xf9Jt}7!T>cWyOCu{uht`dEEcz e@jtAd=5JVCQ5F*F_if0(K19Eiyjcvtul^tTbTmu= literal 0 HcmV?d00001 diff --git a/tests/examples/enc_sample_old.odt b/tests/examples/enc_sample_old.odt new file mode 100644 index 0000000000000000000000000000000000000000..2e697b464cc8a0053e0022f870f24c107d7bce8d GIT binary patch literal 33667 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 Date: Tue, 26 Apr 2022 02:18:48 +0300 Subject: [PATCH 2/5] Update tests for encrypted documents --- tests/testenencrypted.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/testenencrypted.py b/tests/testenencrypted.py index 1e24e9a..81e4fde 100644 --- a/tests/testenencrypted.py +++ b/tests/testenencrypted.py @@ -295,7 +295,7 @@ def test_manifest_parsed_correctly(self): manifest = manifestlist(manifestpart) self.assertEqual(sorted(sample_1_manifest.items()), sorted(manifest.items())) - # z = zipfile.ZipFile(sample_2_name) - # manifestpart = z.read('META-INF/manifest.xml') - # manifest = manifestlist(manifestpart) - # self.assertEqual(sorted(sample_2_manifest.items()), sorted(manifest.items())) \ No newline at end of file + z = zipfile.ZipFile(sample_2_name) + manifestpart = z.read('META-INF/manifest.xml') + manifest = manifestlist(manifestpart) + self.assertEqual(sorted(sample_2_manifest.items()), sorted(manifest.items())) From 12f8389fe47f450d8387f87d71a27693fafe6430 Mon Sep 17 00:00:00 2001 From: lisp3r Date: Thu, 5 May 2022 17:54:06 +0300 Subject: [PATCH 3/5] Add pitures decryption and tests --- odf/opendocument.py | 94 +++++++++++++++--- .../{enc_sample.odt => aes_sample.odt} | Bin ...enc_sample_old.odt => blowfish_sample.odt} | Bin tests/testenencrypted.py | 46 ++++++--- 4 files changed, 113 insertions(+), 27 deletions(-) rename tests/examples/{enc_sample.odt => aes_sample.odt} (100%) rename tests/examples/{enc_sample_old.odt => blowfish_sample.odt} (100%) diff --git a/odf/opendocument.py b/odf/opendocument.py index 46166c9..9f4c7ba 100644 --- a/odf/opendocument.py +++ b/odf/opendocument.py @@ -804,6 +804,15 @@ def getElementsByType(self, elt): return result + +class OpenDocumentException(Exception): + pass + + +class OpenDocumentEncryptionException(OpenDocumentException): + pass + + # Convenience functions def OpenDocumentChart(): """ @@ -904,7 +913,12 @@ def __loadxmlparts(z, manifest, doc, objectpath, password=None): try: xmlpart = z.read(xmlfile) if 'encrypted-data' in manifest[xmlfile].keys(): - xmlpart = __decrypt(xmlpart, manifest[xmlfile]['encrypted-data'], password) + if not password: + raise OpenDocumentEncryptionException('Document is encrypted and password is not provided') + try: + xmlpart = __decrypt(xmlpart, manifest[xmlfile]['encrypted-data'], password, verify_checksum=False) + except OpenDocumentEncryptionException as err: + raise OpenDocumentEncryptionException('{}: {}'.format(xmlfile, err.message)) xmlpart = xmlpart.decode("utf-8") doc._parsing = xmlfile @@ -998,9 +1012,17 @@ def load(odffile, password=None): manifestpart = z.read('META-INF/manifest.xml') manifest = manifestlist(manifestpart) __loadxmlparts(z, manifest, doc, u'', password) - for mentry,mvalue in manifest.items(): + for mentry, mvalue in manifest.items(): if mentry[:9] == u"Pictures/" and len(mentry) > 9: - doc.addPicture(mvalue['full-path'], mvalue['media-type'], z.read(mentry)) + raw_pic = z.read(mentry) + if 'encrypted-data' in mvalue.keys(): + if not password: + raise OpenDocumentEncryptionException('Document is encrypted and password is not provided') + try: + raw_pic = __decrypt(raw_pic, mvalue['encrypted-data'], password, verify_checksum=True) + except OpenDocumentEncryptionException as err: + raise OpenDocumentEncryptionException('{}: {}'.format("filename", err.message)) + doc.addPicture(mvalue['full-path'], mvalue['media-type'], raw_pic) elif mentry == u"Thumbnails/thumbnail.png": doc.addThumbnail(z.read(mentry)) elif mentry in (u'settings.xml', u'meta.xml', u'content.xml', u'styles.xml'): @@ -1084,8 +1106,7 @@ def __make_key(password, algorithm, salt, deriv_iter_count, deriv_key_size): :param deriv_key_size: manifest:key-size, the length in octets of a key delivered by a key-developing algorithm. """ assert algorithm in ('sha1', 'sha256'), 'Only sha251 and sha1 are allowed' - sha_key = hashlib.new(algorithm, password).digest() - + sha_key = hashlib.new(algorithm, password.encode()).digest() return hashlib.pbkdf2_hmac('sha1', sha_key, base64.b64decode(salt), int(deriv_iter_count), int(deriv_key_size)) @@ -1107,17 +1128,15 @@ def __decrypt_data(algorithm, iv, derived_key, encrypted_data): iv = base64.b64decode(iv) if 'blowfish' in algorithm: - alg_obj = Blowfish.new(key=derived_key, mode=Blowfish.MODE_CFB, IV=iv) + return Blowfish.new(key=derived_key, mode=Blowfish.MODE_CFB, IV=iv, segment_size=64).decrypt(encrypted_data) elif 'aes' in algorithm: - alg_obj = AES.new(key=derived_key, mode=AES.MODE_CBC, IV=iv) + return AES.new(key=derived_key, mode=AES.MODE_CBC, IV=iv).decrypt(encrypted_data) elif 'tripledes' in algorithm: - alg_obj = DES3.new(key=derived_key, mode=DES3.MODE_CBC, IV=iv) - - return alg_obj.decrypt(encrypted_data) + return DES3.new(key=derived_key, mode=DES3.MODE_CBC, IV=iv).decrypt(encrypted_data) -def __decrypt(raw_data, manifest_data, password): - # Stage 1: Get the encryption key from the password. +def __decrypt(raw_data, manifest_data, password, verify_checksum=True): + # Get the encryption key from the password. start_key_generation_alg = __normalize_name(manifest_data['start-key-generation']['start-key-generation-name']) derived_key = __make_key(password, algorithm=start_key_generation_alg, @@ -1125,13 +1144,58 @@ def __decrypt(raw_data, manifest_data, password): deriv_iter_count=manifest_data['key-derivation']['iteration-count'], deriv_key_size=manifest_data['key-derivation']['key-size']) - # Stage 3: Decrypt data. + # Add padding if needed + raw_data = __append_padding(raw_data) + + # Decrypt data. decrypted_data = __decrypt_data(algorithm=manifest_data['algorithm']['algorithm-name'], iv=manifest_data['algorithm']['initialisation-vector'], derived_key=derived_key, encrypted_data=raw_data) - # Stage 3: Inflate decrypted data. - return __inflate(decrypted_data) + # Verify the result with checksum + if verify_checksum and not verify(manifest_data['checksum'], manifest_data['checksum-type'], decrypted_data): + raise OpenDocumentEncryptionException("Checksum verification failed. Wrong password or corrupted document") + + # Inflate decrypted data. + try: + return __inflate(decrypted_data) + except zlib.error: + raise OpenDocumentEncryptionException("Wrong password or corrupted document") + + +def __append_padding(ciphertext, segment_size=64., block_size=8.): + assert isinstance(segment_size, float) + assert isinstance(block_size, float) + while not (len(ciphertext) % segment_size / block_size).is_integer(): + ciphertext += b'\x00' + return ciphertext + + +def verify(checksum, checksum_type, decrypted_data): + """ + Verify password. + + :param checksum: base64 encoded checksum from manifest + :param checksum_type: name of a digest algorithm that can be used to check password correctness. SHA1 or SHA256. + Can be: + 1. SHA1/1K: SHA1 algorithm applied to first 1024 bytes of the compressed unencrypted file. + 2. urn:oasis:names:tc:opendocument:xmlns:manifest:1.0#sha1-1k: The same as SHA1/1K. + 3. SHA1: The same as http://www.w3.org/2000/09/xmldsig#sha1. + 4. urn:oasis:names:tc:opendocument:xmlns:manifest:1.0#sha256-1k: SHA256 algorithm applied to first 1024 bytes + of the compressed unencrypted file. + :param decrypted_data: ... + """ + + checksum_type = __normalize_name(checksum_type) + assert checksum_type in ('sha1/1k', 'sha1-1k', 'sha1', 'sha256-1k'), \ + 'Wrong checksum algorithm: {}'.format(checksum_type) + + checksum = base64.b64decode(checksum) + + if 'sha256' in checksum_type: + return checksum == hashlib.sha256(decrypted_data[:1024]).digest() + elif 'sha1' in checksum_type: + return checksum == hashlib.sha1(decrypted_data[:1024]).digest()[:1024] # vim: set expandtab sw=4 : diff --git a/tests/examples/enc_sample.odt b/tests/examples/aes_sample.odt similarity index 100% rename from tests/examples/enc_sample.odt rename to tests/examples/aes_sample.odt diff --git a/tests/examples/enc_sample_old.odt b/tests/examples/blowfish_sample.odt similarity index 100% rename from tests/examples/enc_sample_old.odt rename to tests/examples/blowfish_sample.odt diff --git a/tests/testenencrypted.py b/tests/testenencrypted.py index 81e4fde..13a9eb2 100644 --- a/tests/testenencrypted.py +++ b/tests/testenencrypted.py @@ -1,21 +1,21 @@ import unittest, sys, os import zipfile -from odf import easyliststyle from odf.odfmanifest import manifestlist -from odf.opendocument import OpenDocumentText -from odf.style import Style, TextProperties -from odf.text import P, List, ListItem +from odf.opendocument import load, OpenDocumentEncryptionException if sys.version_info[0] == 3: unicode = str class TestEncryption(unittest.TestCase): + BLOWFISH_SAMPLE = (os.path.join(os.path.abspath('tests/examples/blowfish_sample.odt')), '12345') + AES_SAMPLE = (os.path.join(os.path.abspath('tests/examples/aes_sample.odt')), 'qwerty') + TEMPFILE = 'tmpfile' + def test_manifest_parsed_correctly(self): self.maxDiff = None - sample_1_name = os.path.join(os.path.abspath('tests/examples/enc_sample_old.odt')) - sample_1_manifest = { + blowsidh_sample_manifest = { '/': { 'full-path': u'/', 'media-type': u'application/vnd.oasis.opendocument.presentation' @@ -163,8 +163,7 @@ def test_manifest_parsed_correctly(self): } } } - sample_2_name = os.path.join(os.path.abspath('tests/examples/enc_sample.odt')) - sample_2_manifest = { + aes_sample_manifest = { "/": { "full-path": u"/", "media-type": u"application/vnd.oasis.opendocument.text" @@ -290,12 +289,35 @@ def test_manifest_parsed_correctly(self): } } - z = zipfile.ZipFile(sample_1_name) + z = zipfile.ZipFile(self.BLOWFISH_SAMPLE[0]) manifestpart = z.read('META-INF/manifest.xml') manifest = manifestlist(manifestpart) - self.assertEqual(sorted(sample_1_manifest.items()), sorted(manifest.items())) + self.assertEqual(sorted(blowsidh_sample_manifest.items()), sorted(manifest.items())) - z = zipfile.ZipFile(sample_2_name) + z = zipfile.ZipFile(self.AES_SAMPLE[0]) manifestpart = z.read('META-INF/manifest.xml') manifest = manifestlist(manifestpart) - self.assertEqual(sorted(sample_2_manifest.items()), sorted(manifest.items())) + self.assertEqual(sorted(aes_sample_manifest.items()), sorted(manifest.items())) + + def test_decryption_with_right_password(self): + for sample in (self.BLOWFISH_SAMPLE, self.AES_SAMPLE): + load(sample[0], sample[1]) + return True + + def test_decryption_with_wrong_password(self): + for sample in (self.BLOWFISH_SAMPLE, self.AES_SAMPLE): + with self.assertRaises(OpenDocumentEncryptionException) as context: + load(sample[0], 'wrong_password') + + def test_decryption_and_save(self): + for sample in (self.BLOWFISH_SAMPLE, self.AES_SAMPLE): + doc = load(sample[0], sample[1]) + doc.save(self.TEMPFILE, False) + + load(self.TEMPFILE) + + return True + + def tearDown(self): + if os.path.exists(self.TEMPFILE): + os.remove(self.TEMPFILE) From b3cb32edca2481d7ca74bdd7a5ad0348673e461b Mon Sep 17 00:00:00 2001 From: lisp3r Date: Fri, 6 May 2022 18:55:28 +0300 Subject: [PATCH 4/5] Return description that was deleted by mistake --- odf/odfmanifest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/odf/odfmanifest.py b/odf/odfmanifest.py index 54005d9..e93b8f2 100644 --- a/odf/odfmanifest.py +++ b/odf/odfmanifest.py @@ -38,11 +38,14 @@ # #----------------------------------------------------------------------------- - class ODFManifestHandler(handler.ContentHandler): + """ The ODFManifestHandler parses a manifest file and produces a list of + content """ def __init__(self): self.manifest = {} + + # Tags self.elements = { (MANIFESTNS, 'file-entry'): (self.s_file_entry, self.donothing), (MANIFESTNS, 'encryption-data'): (self.e_file_entry, self.e_file_entry_close), From af4cc2c579de5ea7fe82edb6be3e52da33bc226c Mon Sep 17 00:00:00 2001 From: lisp3r Date: Fri, 6 May 2022 18:56:00 +0300 Subject: [PATCH 5/5] Return description that was deleted by mistake --- odf/odfmanifest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odf/odfmanifest.py b/odf/odfmanifest.py index e93b8f2..93d74ad 100644 --- a/odf/odfmanifest.py +++ b/odf/odfmanifest.py @@ -40,7 +40,7 @@ class ODFManifestHandler(handler.ContentHandler): """ The ODFManifestHandler parses a manifest file and produces a list of - content """ + content """ def __init__(self): self.manifest = {}