marcobellaccini/pyAesCrypt

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.