0

I'm currently trying to encrypt and decrypt passwords using the python cryptography library and RSA. The key generation works fine, the encryption works but when it comes to decrypting the password, I'm faced with this traceback error message:

raise ValueError("Ciphertext length must be equal to key size.")

My encryption and decryption code looks like this:

class AppCryptography():

    def generate_rsa_keypair(self, bits=4096):
        return rsa.generate_private_key(
            public_exponent=65537,
            key_size=bits,
            backend=default_backend()
        )

    def load_private_key(self, pem_file_path):

        with open(pem_file_path, "rb") as key_file:
            private_key = serialization.load_pem_private_key(
                key_file.read(),
                password=None,
                backend=default_backend()
            )

        return private_key

    def load_public_key(self, pem_file_path):

        with open(pem_file_path, "rb") as key_file:
            public_key = serialization.load_pem_public_key(
                key_file.read(),
                backend=default_backend()
            )

        return public_key

    def encrypt_password(self, public_key, password):
        password = bytes(password) if not isinstance(password, bytes) else password
        public_key = public_key if isinstance(public_key, RSAPublicKey) else self.load_pem_public_key(
            public_key_pem_export=public_key
        )

        cipher_pass = public_key.encrypt(
            password,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA1()),
                algorithm=hashes.SHA1(),
                label=None
            )
        )

        return str(base64.b64encode(cipher_pass))

    def decrypt(self, private_key, cipher_pass):
        cipher_pass = base64.b64decode(cipher_pass) if not isinstance(cipher_pass, bytes) else cipher_pass
        private_key = private_key if isinstance(private_key, RSAPrivateKey) else self.load_pem_private_key(
            private_key_pem_export=private_key
        )

        plain_text_pass = private_key.decrypt(
            cipher_pass,
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA1()),
                algorithm=hashes.SHA1(),
                label=None
            )
        )
        return str(plain_text_pass)

And the error happens when I run this script:

crypter = AppCryptography()

backend_public_key = crypter.load_public_key(dir_path + "/util/keys/backend_public_key.pem")
frontend_private_key = crypter.load_private_key(dir_path + "/util/keys/frontend_private_key.pem")

encrypted_password = crypter.encrypt_password(backend_public_key, password)

signature = crypter.sign_data(frontend_private_key, password)

backend_private_key = crypter.load_private_key(dir_path + "/util/keys/backend_private_key.pem")
cleartext = crypter.decrypt(backend_private_key, encrypted_password)

My stacktrace show that the error comes from the decrypt function but I'm unable to see where the error is in the function definition.

File "/Users/Me/anaconda/envs/flask_dream/lib/python2.7/site-packages/sqlalchemy/orm/state.py", line 411, in _initialize_instance
  return manager.original_init(*mixed[1:], **kwargs)
File "/Users/Me/anaconda/envs/flask_dream/lib/python2.7/site-packages/sqlalchemy/ext/declarative/base.py", line 650, in _declarative_constructor
  setattr(self, k, kwargs[k])
File "/Users/Me/Desktop/Projects/Flask-Dream/project_app/models.py", line 114, in password
  cleartext = crypter.decrypt(backend_private_key, encrypted_password)
File "/Users/Me/Desktop/Projects/Flask-Dream/project_app/util/crypto.py", line 121, in decrypt
  label=None
File "/Users/Me/anaconda/envs/flask_dream/lib/python2.7/site-packages/cryptography/hazmat/backends/openssl/rsa.py", line 397, in decrypt
  raise ValueError("Ciphertext length must be equal to key size.")
martineau
  • 119,623
  • 25
  • 170
  • 301
mandok
  • 492
  • 1
  • 5
  • 20
  • Why are you encryption passwords since depending un usage that may be a very insecure practice. – zaph Feb 05 '17 at 14:38
  • It's not my service password that i'm encrypting (i'm using a hash+salt for that) this is another use case where I want to encrypt the data entered by my users in their personal account. I have a very specific architecture. – mandok Feb 05 '17 at 14:40
  • Note: Just using a hash function is not sufficient and just adding a salt does little to improve the security. Instead iterate over an HMAC with a random salt for about a 100ms duration and save the salt with the hash. Use functions such as `PBKDF2` (aka `Rfc2898DeriveBytes`), `password_hash`/`password_verify`, `Bcrypt` and similar functions. The point is to make the attacker spend a lot of time finding passwords by brute force. Protecting your users is important, please use secure password methods. – zaph Feb 05 '17 at 14:43
  • Well in my case I'm using werkzeug.security methods called generate_password_hash and check_password_hash. But if you recommend Bcrypt I can switch to that. But in this issue it's not about that. Thank you. – mandok Feb 05 '17 at 14:55
  • If you are using `werkzeug.security.generate_password_hash` with `method='pbkdf2:sha1'` that is secure. Better yet explicitly set the iterations with the mode format: `pbkdf2:sha1:iterations` where `iterations` is an integer such that the method takes about 100ms. Note: NIST now recommends PBKDF2 for hashing passwords. (I realize this is not the answer you are looking for but password security is important to the users.) – zaph Feb 05 '17 at 15:31
  • Examine the decrypt key size (`private_key`) and decrypt data size (`cipher_pass`) after Base64 decoding to see which one is incorrect. – zaph Feb 05 '17 at 15:45
  • I'm not sure about this line: `cipher_pass = base64.b64decode(cipher_pass) if not isinstance(cipher_pass, bytes) else cipher_pass`, could you check if base 64 decoding is performed if the input is a string (because a string, in some sense, may also be bytes). – Maarten Bodewes Feb 05 '17 at 16:43
  • Thank you @MaartenBodewes ! That was the issue ! – mandok Feb 05 '17 at 16:55
  • I'm deeply sorry @MaartenBodewes for answering my own question using your guess. I thought that it was the thing to do. I didn't investigate the why of the issue. But the thing is that the if condition was somehow triggered and it ended passing the cipher_pass without the b64decode which created the issue of lenght when decrypting. To my knowledge there was no indentation issue. – mandok Feb 06 '17 at 00:54
  • @mandok No problem, I actually voted up your answer; it was just something to keep in mind for next questions/ – Maarten Bodewes Feb 06 '17 at 01:07

1 Answers1

3

What seems to be the issue in your code is the following line:

cipher_pass = base64.b64decode(cipher_pass) if not isinstance(cipher_pass, bytes) else cipher_pass

Now - if I understand Python correctly - strings are stored in bytes with a specific encoding (this is definitely the case for Python 2, and may as well for Python 3 when str is used).

This means that the base64 string is also a byte string, and isinstance(cipher_pass, bytes) returns true. But that means that the base64 decoding is not triggered, which in turn means that your ciphertext is too large and will fail during decryption.

Always decoding base64 would be the best thing to do if you require a textual interface.

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263