Commit 826bee0bedbf0aa9428f22773e11372ba72fc343
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,6 +133,8 @@ def is_encrypted(olefile): | ||
| 133 | return False | 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 | WRITE_PROTECT_ENCRYPTION_PASSWORD = 'VelvetSweatshop' | 138 | WRITE_PROTECT_ENCRYPTION_PASSWORD = 'VelvetSweatshop' |
| 137 | 139 | ||
| 138 | 140 | ||
| @@ -142,22 +144,19 @@ def _check_msoffcrypto(): | @@ -142,22 +144,19 @@ def _check_msoffcrypto(): | ||
| 142 | raise ImportError('msoffcrypto-tools could not be imported') | 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 | :param str filename: path to an ole file on disc | 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 | :param temp_file_args: arguments for :py:func:`tempfile.mkstemp` e.g., | 160 | :param temp_file_args: arguments for :py:func:`tempfile.mkstemp` e.g., |
| 162 | `dirname` or `prefix`. `suffix` will default to | 161 | `dirname` or `prefix`. `suffix` will default to |
| 163 | suffix of input `filename`; `text` will be ignored | 162 | suffix of input `filename`; `text` will be ignored |
| @@ -167,42 +166,51 @@ def try_decrypt_write_protection(filename, **temp_file_args): | @@ -167,42 +166,51 @@ def try_decrypt_write_protection(filename, **temp_file_args): | ||
| 167 | """ | 166 | """ |
| 168 | _check_msoffcrypto() | 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 | with open(filename, 'rb') as reader: | 175 | with open(filename, 'rb') as reader: |
| 172 | crypto_file = msoffcrypto.OfficeFile(reader) | 176 | crypto_file = msoffcrypto.OfficeFile(reader) |
| 173 | if not crypto_file.is_encrypted(): | 177 | if not crypto_file.is_encrypted(): |
| 174 | raise ValueError('Given input file {} is not encrypted!' | 178 | raise ValueError('Given input file {} is not encrypted!' |
| 175 | .format(filename)) | 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 | write_descriptor = None | 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 | write_handle.close() | 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 |