Commit 826bee0bedbf0aa9428f22773e11372ba72fc343

Authored by Christian Herdtweck
1 parent 756fd308

crypto: generalize decrypt function

Showing 1 changed file with 49 additions and 41 deletions
oletools/crypto.py
... ... @@ -133,6 +133,8 @@ def is_encrypted(olefile):
133 133 return False
134 134  
135 135  
  136 +#: one way to achieve "write protection" in office files is to encrypt the file
  137 +#: using this password
136 138 WRITE_PROTECT_ENCRYPTION_PASSWORD = 'VelvetSweatshop'
137 139  
138 140  
... ... @@ -142,22 +144,19 @@ def _check_msoffcrypto():
142 144 raise ImportError('msoffcrypto-tools could not be imported')
143 145  
144 146  
145   -def try_decrypt_write_protection(filename, **temp_file_args):
  147 +def decrypt(filename, passwords=None, **temp_file_args):
146 148 """
147   - Try to decrypt a file that was encrypted just to achieve write protection.
  149 + Try to decrypt an encrypted file
148 150  
149   - One way to achieve write protection / read only access to an office
150   - document is to embed the file encrypted into an OLE file using a fixed
151   - password. This is of course no real protection, just a way to tell GUIs to
152   - discourage modifications of the file.
153   -
154   - This function tries to decrypt the given file using that standard password
155   - and returns an ole file accessing the decrypted data. If the decryption
156   - fails the encryption is probably a real one and None is returned.
157   -
158   - Decryption output will be sent to a temporary file, whose name is returned.
  151 + This function tries to decrypt the given file using a given set of
  152 + passwords. If no password is given, tries the standard password for write
  153 + protection. Creates a file with decrypted data whose file name is returned.
  154 + If the decryption fails, None is returned.
159 155  
160 156 :param str filename: path to an ole file on disc
  157 + :param passwords: list/set/tuple/... of passwords or a single password or
  158 + None
  159 + :type passwords: iterable or str or None
161 160 :param temp_file_args: arguments for :py:func:`tempfile.mkstemp` e.g.,
162 161 `dirname` or `prefix`. `suffix` will default to
163 162 suffix of input `filename`; `text` will be ignored
... ... @@ -167,42 +166,51 @@ def try_decrypt_write_protection(filename, **temp_file_args):
167 166 """
168 167 _check_msoffcrypto()
169 168  
170   - result = None
  169 + if passwords is None:
  170 + passwords = (WRITE_PROTECT_ENCRYPTION_PASSWORD, )
  171 + elif isinstance(passwords, str):
  172 + passwords = (passwords, )
  173 +
  174 + decrypt_file = None
171 175 with open(filename, 'rb') as reader:
172 176 crypto_file = msoffcrypto.OfficeFile(reader)
173 177 if not crypto_file.is_encrypted():
174 178 raise ValueError('Given input file {} is not encrypted!'
175 179 .format(filename))
176   - try:
177   - crypto_file.load_key(password=WRITE_PROTECT_ENCRYPTION_PASSWORD)
178   - except Exception:
179   - return None # password verification failed
180 180  
181   - # create temp file
182   - if 'suffix' not in temp_file_args:
183   - temp_file_args['suffix'] = splitext(filename)[1]
184   - temp_file_args['text'] = False
  181 + for password in passwords:
  182 + try:
  183 + crypto_file.load_key(password=password)
  184 + except Exception:
  185 + continue # password verification failed, try next
185 186  
186   - write_descriptor = None
187   - write_handle = None
188   - try:
189   - write_descriptor, result = mkstemp(**temp_file_args)
190   - write_handle = os.fdopen(write_descriptor, 'wb')
191   - crypto_file.decrypt(write_handle)
  187 + # create temp file
  188 + if 'suffix' not in temp_file_args:
  189 + temp_file_args['suffix'] = splitext(filename)[1]
  190 + temp_file_args['text'] = False
192 191  
193   - # non-error cleanup: close file descriptor and handle
194   - write_handle.close()
195   - write_handle = None
196   - # os.close(write_descriptor) already done by write_handle.close
197 192 write_descriptor = None
198   - except Exception:
199   - # error: clean up: close everything and del file ignoring errors;
200   - # then re-raise original exception
201   - if write_handle:
  193 + write_handle = None
  194 + try:
  195 + write_descriptor, decrypt_file = mkstemp(**temp_file_args)
  196 + write_handle = os.fdopen(write_descriptor, 'wb')
  197 + write_descriptor = None # is now handled via write_handle
  198 + crypto_file.decrypt(write_handle)
  199 +
  200 + # decryption was successfull; clean up and return
  201 + write_handle.close()
  202 + write_handle = None
  203 + break
  204 + except Exception:
  205 + # error: clean up: close everything and del file ignoring errors;
  206 + # then re-raise original exception
  207 + if write_handle:
202 208 write_handle.close()
203   - elif write_descriptor:
204   - os.close(write_descriptor)
205   - if result and isfile(result):
206   - os.unlink(result)
207   - raise
208   - return result
  209 + elif write_descriptor:
  210 + os.close(write_descriptor)
  211 + if decrypt_file and isfile(decrypt_file):
  212 + os.unlink(decrypt_file)
  213 + decrypt_file = None
  214 + raise
  215 + # if we reach this, all passwords were tried without success
  216 + return decrypt_file
... ...