Very slow decryption
gellnerm opened this issue · 2 comments
I have 18 3.5KB files that I want to decrypt. This takes 18 seconds with pyAesCrypt.
I already did some profiling and found that 99.9% of the decryption time is spent in key = stretch(passw, iv1).
In stretch() there is a line that takes 53% of the time (it is called in a loop 8192 times):
passHash = hashes.Hash(hashes.SHA256(), backend=default_backend())
Can we speed this up by using other parameters or another implementation?
Here is the profile:
Total time: 18.0321 s
File: python/lib/python3.9/site-packages/pyAesCrypt/crypto.py
Function: decryptStream at line 293
Line # Hits Time Per Hit % Time Line Contents
==============================================================
293 @profile
294 def decryptStream(fIn, fOut, passw, bufferSize, inputLength):
295 # validate bufferSize
296 67 109.0 1.6 0.0 if bufferSize % AESBlockSize != 0:
297 raise ValueError("Buffer size must be a multiple of AES block size")
298
299 67 83.0 1.2 0.0 if len(passw) > maxPassLen:
300 raise ValueError("Password is too long.")
301
302 67 75.0 1.1 0.0 fdata = fIn.read(3)
303 # check if file is in AES Crypt format (also min length check)
304 67 110.0 1.6 0.0 if (fdata != bytes("AES", "utf8") or inputLength < 136):
305 raise ValueError("File is corrupted or not an AES Crypt "
306 "(or pyAesCrypt) file.")
307
308 # check if file is in AES Crypt format, version 2
309 # (the only one compatible with pyAesCrypt)
310 67 58.0 0.9 0.0 fdata = fIn.read(1)
311 67 58.0 0.9 0.0 if len(fdata) != 1:
312 raise ValueError("File is corrupted.")
313
314 67 45.0 0.7 0.0 if fdata != b"\x02":
315 raise ValueError("pyAesCrypt is only compatible with version "
316 "2 of the AES Crypt file format.")
317
318 # skip reserved byte
319 67 77.0 1.1 0.0 fIn.read(1)
320
321 # skip all the extensions
322 while True:
323 201 173.0 0.9 0.0 fdata = fIn.read(2)
324 201 157.0 0.8 0.0 if len(fdata) != 2:
325 raise ValueError("File is corrupted.")
326 201 152.0 0.8 0.0 if fdata == b"\x00\x00":
327 67 50.0 0.7 0.0 break
328 134 191.0 1.4 0.0 fIn.read(int.from_bytes(fdata, byteorder="big"))
329
330 # read external iv
331 67 56.0 0.8 0.0 iv1 = fIn.read(16)
332 67 50.0 0.7 0.0 if len(iv1) != 16:
333 raise ValueError("File is corrupted.")
334
335 # stretch password and iv
336 67 18006446.0 268752.9 99.9 key = stretch(passw, iv1)
337
338 # read encrypted main iv and key
339 67 124.0 1.9 0.0 c_iv_key = fIn.read(48)
340 67 73.0 1.1 0.0 if len(c_iv_key) != 48:
341 raise ValueError("File is corrupted.")
342
343 # read HMAC-SHA256 of the encrypted iv and key
344 67 56.0 0.8 0.0 hmac1 = fIn.read(32)
345 67 54.0 0.8 0.0 if len(hmac1) != 32:
346 raise ValueError("File is corrupted.")
347
348 # compute actual HMAC-SHA256 of the encrypted iv and key
349 134 1650.0 12.3 0.0 hmac1Act = hmac.HMAC(key, hashes.SHA256(),
350 67 373.0 5.6 0.0 backend=default_backend())
351 67 402.0 6.0 0.0 hmac1Act.update(c_iv_key)
352
353 # HMAC check
354 67 650.0 9.7 0.0 if hmac1 != hmac1Act.finalize():
355 raise ValueError("Wrong password (or file is corrupted).")
356
357 # instantiate AES cipher
358 134 1421.0 10.6 0.0 cipher1 = Cipher(algorithms.AES(key), modes.CBC(iv1),
359 67 371.0 5.5 0.0 backend=default_backend())
360 67 4768.0 71.2 0.0 decryptor1 = cipher1.decryptor()
361
362 # decrypt main iv and key
363 67 1981.0 29.6 0.0 iv_key = decryptor1.update(c_iv_key) + decryptor1.finalize()
364
365 # get internal iv and key
366 67 69.0 1.0 0.0 iv0 = iv_key[:16]
367 67 57.0 0.9 0.0 intKey = iv_key[16:]
368
369 # instantiate another AES cipher
370 134 925.0 6.9 0.0 cipher0 = Cipher(algorithms.AES(intKey), modes.CBC(iv0),
371 67 425.0 6.3 0.0 backend=default_backend())
372 67 3184.0 47.5 0.0 decryptor0 = cipher0.decryptor()
373
374 # instantiate actual HMAC-SHA256 of the ciphertext
375 134 1429.0 10.7 0.0 hmac0Act = hmac.HMAC(intKey, hashes.SHA256(),
376 67 386.0 5.8 0.0 backend=default_backend())
377
378 # decrypt ciphertext, until last block is reached
379 134 176.0 1.3 0.0 while fIn.tell() < inputLength - 32 - 1 - AESBlockSize:
380 # read data
381 134 214.0 1.6 0.0 cText = fIn.read(
382 134 121.0 0.9 0.0 min(
383 67 56.0 0.8 0.0 bufferSize,
384 67 70.0 1.0 0.0 inputLength - fIn.tell() - 32 - 1 - AESBlockSize
385 )
386 )
387 # update HMAC
388 67 916.0 13.7 0.0 hmac0Act.update(cText)
389 # decrypt data and write it to output file
390 67 1130.0 16.9 0.0 fOut.write(decryptor0.update(cText))
391
392 # last block reached, remove padding if needed
393
394 # read last block
395
396 # this is for empty files
397 67 59.0 0.9 0.0 if fIn.tell() != inputLength - 32 - 1:
398 67 65.0 1.0 0.0 cText = fIn.read(AESBlockSize)
399 67 63.0 0.9 0.0 if len(cText) < AESBlockSize:
400 raise ValueError("File is corrupted.")
401 else:
402 cText = bytes()
403
404 # update HMAC
405 67 308.0 4.6 0.0 hmac0Act.update(cText)
406
407 # read plaintext file size mod 16 lsb positions
408 67 63.0 0.9 0.0 fs16 = fIn.read(1)
409 67 54.0 0.8 0.0 if len(fs16) != 1:
410 raise ValueError("File is corrupted.")
411
412 # decrypt last block
413 67 1482.0 22.1 0.0 pText = decryptor0.update(cText) + decryptor0.finalize()
414
415 # remove padding
416 67 79.0 1.2 0.0 toremove = ((16 - fs16[0]) % 16)
417 67 58.0 0.9 0.0 if toremove != 0:
418 63 69.0 1.1 0.0 pText = pText[:-toremove]
419
420 # write decrypted data to output file
421 67 90.0 1.3 0.0 fOut.write(pText)
422
423 # read HMAC-SHA256 of the encrypted file
424 67 55.0 0.8 0.0 hmac0 = fIn.read(32)
425 67 61.0 0.9 0.0 if len(hmac0) != 32:
426 raise ValueError("File is corrupted.")
427
428 # HMAC check
429 67 623.0 9.3 0.0 if hmac0 != hmac0Act.finalize():
430 raise ValueError("Bad HMAC (file is corrupted).")
======================================
Total time: 18.7916 s
File: python/lib/python3.9/site-packages/pyAesCrypt/crypto.py
Function: stretch at line 59
Line # Hits Time Per Hit % Time Line Contents
==============================================================
59 @profile
60 def stretch(passw, iv1):
61
62 # hash the external iv and the password 8192 times
63 67 77.0 1.1 0.0 digest = iv1 + (16 * b"\x00")
64
65 548931 191790.0 0.3 1.0 for i in range(8192):
66 548864 9903957.0 18.0 52.7 passHash = hashes.Hash(hashes.SHA256(), backend=default_backend())
67 548864 2176946.0 4.0 11.6 passHash.update(digest)
68 548864 2363760.0 4.3 12.6 passHash.update(bytes(passw, "utf_16_le"))
69 548864 4155059.0 7.6 22.1 digest = passHash.finalize()
70
71 67 18.0 0.3 0.0 return digest
Hi,
it's normal for Key Stretching to take a bit of time.
Of course, if you're running on some very old or otherwise very slow HW, it may take a relatively big amount of time.
Unfortunately, in order to be compatible with the AES Crypt format - version 2, I cannot let programmers change the parameters.
The implementation that I'm currently using is the pyca/cryptography one and it's probably the most used Python crypto primitives library.
Cheers
Marco
Thanks, I handled it by merging the contents of all files into a single encrypted file.