To keep it short: I am sending an encrypted message (AES with CBC) to another service, and it returns me an encrypted response, but I can't decrypt it because they are not using padding? (to be honest, I don't know much about encryption and its mechanisms).
This is my implementation (based on the documentation) of a class used to encrypt and decrypt messages.
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
class AESCipher:
def __init__(self, key, iv):
self.key = bytes.fromhex(key)
self.iv = bytes.fromhex(iv)
def encrypt(self, msg):
msg = pad(msg.encode(), AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, self.iv)
return cipher.encrypt(msg)
def decrypt(self, msg):
cipher = AES.new(self.key, AES.MODE_CFB, self.iv)
return unpad(cipher.decrypt(msg), AES.block_size)
I can easily encrypt (and even decrypt my own message) using this implementation, but the actual response from the host always fails to decrypt. First I thought it was a problem with the host, but when I use an AES online tool, it kinda works (I used this one)
To reproduce the issue, I send a malformed message and receive an encrypted error:
msg = b"X\xb4\xc6\xc9j\x92\x8f\xe5\x84\xe5\\N7\x8bv\xb8\x02\x0e\xed*\xe7\x92\xdd/\xf1\xff\xdfj 5\x00\x91\xb5;\xb6Q\x08\xc8\xf1PFF\x1aw\x93\xa7\xbe\xa7\xafD\xe7:=\x8b\x1d\x86i\xa8\x95\x107\xf2\xbcF1\x80D\x8c\x98\x1f\xfc\x80\xc3\xd6\x81'\xf3\xd98\x93\x8bv\xf7P\xc9\xb1L,\x8aJ\x05\xd8\xd0P\x10\rQ\xba\xf5&4\x0e\xf0\x97\xf5\xa5B\xb7\xbda_?\xcbk~\xe6\xfe\xf6\x8f\x92\x1b;#\xd2\x87\xc6^\n"
The key and iv are:
key = AEC273769C9C4E9830D5FA3929BE1F5115E4BF085BCBA6ACCBAEF63E654D8AE3
iv = ACE499278E5FDC6849DDF23A8966D7CF
I get this error:
File "/home/richter/Code/Test_encryption/encryptation.py", line 22, in decrypt
return unpad(uncrypt, AES.block_size)
File "/home/richter/Code/Test_encryption/.venv/lib64/python3.10/site-packages/Crypto/Util/Padding.py", line 92, in unpad
raise ValueError("Padding is incorrect.")
ValueError: Padding is incorrect.
I also tried a different library, one written entirely in python (and boy, it's slow) but got the same issue with the size of the package.
Related
I've got a encrypt/decrypt class setup based on this SO answer. I've tested it and it works fine. It's not working for a new API I'm pulling information from. The new API is built with PHP and is using the following package to encrypt information: https://laravel.com/docs/8.x/encryption using Laravel Crypt() command. All encrypted values are encrypted using OpenSSL and the AES-256-CBC cipher.
The enc value after the first line of the decrypt method
def decrypt(self, enc):
enc = base64.b64decode(enc)
iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')
looks like this (Don't worry, this is just non-sensitive demo data):
b"{'iv': 'Ld2pjRJqoKcWnW1P3tiO9Q==', 'value': 'M9QeHtbybeUxAVuIRcQ3bA==', 'mac': 'xxxxxx......'}"
, which basically looks like a byte-string JSON. The testing encryption key is base64:69GnnXWsW1qnW+soLXOVd8Mi4AdXKBFfkw88/7F2kSg=.
I know I can turn it into a dictionary like this
import json
d = json.loads(enc)
How should I manipulate values from this dictionary to prepare it to be decrypted like other encrypted text this class can successfully decrypt?
Update:
Based on comments I've tried to modify the method to look like this:
def decrypt(self, encrypted):
enc = base64.b64decode(encrypted)
if b'{"iv":' == enc[:6]:
d = json.loads(enc)
iv = base64.b64decode(d['iv'])
val = base64.b64decode(d['value'])
else:
iv = enc[:AES.block_size]
val = enc[AES.block_size:]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(val)).decode('utf-8')
This still does not work. It doesn't crash, but I'm getting a blank string back ('') and I know that's not what was encrypted. The answer should be 'Demo'
The code in the "Update" section of the question will work without any changes. You just need to make sure to remove the "base64:" prefix in the encryption key provided. Once that is removed, it will work as expected.
I have written some code using the pycrypt library and I must be doing something wrong, but I can't figure out what it is that I am doing wrong. I can (nearly) decrypt messages with the wrong initialization vector even though I believe I am following their examples.
from Crypto.Cipher import AES
import os
from string import ascii_letters
key, iv = os.urandom(32), os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = ascii_letters
plaintext += ' ' * (-len(plaintext) % 16) # Padding
ciphertext = cipher.encrypt(plaintext)
cipher = AES.new(key, AES.MODE_CBC, os.urandom(16))
text = cipher.decrypt(ciphertext)
# text[16:] = b'qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ '
Now aside from the first 16 bytes of the decrypted text, you have everything decrypted correctly with a totally random choice of IV. Can someone help me figure out where I am going wrong?
That's normal for cipher block chaining. In CBC decryption, the IV is only necessary to reconstruct the first block of plaintext. The computation of other blocks of plaintext doesn't actually involve the IV. Here's a diagram (source: Wikimedia user WhiteTimberwolf, public domain):
I'm using python to encrypt and decrypt files. When file encrypted, then try to decrypt like this:
from Crypto.Cipher import AES
from Crypto import Random
def decrypt(in_file, out_file, pwd, key_len=32):
bs = AES.block_size
salt = in_file.read(bs)[len('Salted__'):]
key, iv = derive_keyiv(pwd, salt, key_len, bs)
cipher = AES.new(key, AES.MODE_CBC, iv)
next_chunk = ''
finished = False
try:
while not finished:
chunk, next_chunk = next_chunk, cipher.decrypt(in_file.read(1024*bs))
if len(next_chunk) == 0:
padding_len = ord(chunk[-1])
chunk = chunk[:-padding_len]
finished = True
out_file.write(chunk)
return True, None
except Exception as e:
return False, e
But if the password input error, the decrypt still decrypt in_file and write to out_file and no exception throw.
How to check the password error during decrypt?
AES by itself cannot check if the key is "correct". It is simply a pure function that transforms some bytes to some other bytes.
To achieve what you want, you need to implement it yourself. One way to do is to add a fixed header (like 16 bytes of zero) to the plaintext before encryption. Upon decryption, check and discard the said header if it matches or raise an error if the header mismatches.
A side note: you are doing encryption without any authentication, which is probably insecure.
Edit
First of all, you should add authentication. Encryption without authentication easily leads to many security flaws, many not obvious to the untrained. Especially since you are using AES in CBC mode, you may open yourself to padding oracle attacks without authentication.
When you do authenticated encryption the right way (encrypt-then-mac), you will get an authentication error if the user input the wrong password. If you want to further distinguish a wrong password from tampered data, you have to devise your own method, like prepending a ciphertext of magic number.
I have a rails project that has sensitive string type values stored on a remote Postgresql database. I encrypted these strings using the ActiveSupport::MessageEncryptor (http://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html) functions. I have the key I used to encrypt them and trying to find a way to retrieve them from the database and decrypt them in a python script.
I'am open for any suggestions on how to achieve this in any other way using rails and python. And much appreciated for any advice on how to decrypt these values in python.
Thanks,
So we managed to solve this with a lot of hit and trial and lot of help from some outdated or similar codes on internet.
Versions used of different libraries:
Rails version(from which messages were being encrypted): 5.2.x
Python version we are using: 3.8
We are also using Django rest framework(3.12.2).
Versions of libraries used in the script(this bit us hard, because some libraries' new versions were not working as expected, didn't dig in much detail as to why):
pycryptodomex: 3.9.7
cryptography: 3.3.1
rubymarshal: 1.2.7
Actual encryptor/decryptor
# pylint: disable=missing-module-docstring,too-few-public-methods
import base64
import hashlib
import os
from Cryptodome.Cipher import AES
from cryptography.hazmat.primitives.ciphers import Cipher
from rubymarshal.reader import loads
from rest_framework.response import Response
from rest_framework import status
from rubymarshal.writer import writes
from dotenv import load_dotenv
load_dotenv()
class MyRailsEncryptor():
"""
This is a class for providing encryption/decryption functionality.
"""
#classmethod
def get_encrypted_data(cls, data):
"""
This method handles encryption algorithm takes in data and return encrypted data
"""
key = cls.get_key()
iv = os.urandom(16)
auth_tag = os.urandom(16)
cipher = AES.new(key, AES.MODE_GCM, iv)
ciphertext = cipher.encrypt(writes(data))
ciphertext = base64.b64encode(ciphertext)
iv = base64.b64encode(iv)
auth_tag = base64.b64encode(auth_tag)
blob = f'{ciphertext.decode("utf-8")}--{iv.decode("utf-8")}--{auth_tag.decode("utf-8")}'
return blob
#classmethod
def get_decrypted_data(cls, data):
"""
This method handles decryption algorithm takes in encrypted_data and return decrypted plain text
"""
key = cls.get_key()
ciphertext, iv, auth_tag = data.split("--")
ciphertext = base64.b64decode(ciphertext)
iv = base64.b64decode(iv)
cipher = AES.new(key, AES.MODE_GCM, iv)
try:
decrypted_data = cipher.decrypt(ciphertext)
except AssertionError as err:
return Response({"Assertion Error": err.message_dict}, status=status.HTTP_400_BAD_REQUEST)
plaintext = loads(decrypted_data)
return plaintext
#classmethod
def get_key(cls):
"""
Returns key generated by Encryption key and Encryption secret using hashlib on rails methodology
"""
return hashlib.pbkdf2_hmac('sha1', os.getenv("ENCRYPTION_KEY").encode(),
os.getenv("ENCRYPTION_SECRET").encode(), 65536, 32)
Keys will obviously be synced/provided by the encryption party, this contains a method for encryption as well, though we only need decryption.
I am implementing a Django website in which uploaded files are encrypted with a user provided key before they are saved on the server (/media). When users wish to view them, they are prompted for the key, the encrypted file is decrypted, and then displayed for them. Here's my encrypt/decrypt file code:
from Crypto import Random
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
def encryption_pad(string):
pad = b"\0" * (AES.block_size - len(string) % AES.block_size)
padded_string = string + pad
return padded_string
def encrypt_file(key, file):
with open(file, 'rb') as out:
byte_output = out.read()
hash = SHA256.new()
hash.update(key)
byte_output = encryption_pad(byte_output)
initialization_vector = Random.new().read(AES.block_size)
cipher = AES.new(hash.digest(), AES.MODE_CBC, initialization_vector)
encrypted_output = initialization_vector + cipher.encrypt(byte_output)
with open(file + ".enc", 'wb') as out:
out.write(encrypted_output)
def decrypt_file(file, key):
with open(file, 'rb') as input:
ciphertext = input.read()
hash = SHA256.new()
hash.update(key)
initialization_vector = ciphertext[:AES.block_size]
cipher = AES.new(hash.digest(), AES.MODE_CBC, initialization_vector)
decrypted_output = cipher.decrypt(ciphertext[AES.block_size:])
decrypted_output = decrypted_output.rstrip(b"\0")
with open(file[:-4], 'wb') as output:
output.write(decrypted_output)
I am relatively new to security, so my question is: For this setup the keys must exist in the server's memory for some duration of time, so what is the proper way for my views.py function to pass them to this module and then properly dispose of them after?
There are some existing questions on how to securely handle (or not) in-memory objects in Python: see here and here.
If security is that important, though, you might want to consider an even more secure option: doing the encryption and decryption on the client, in Javascript. That way the key never gets sent over the wire, and never exists on the server. That's how LastPass works, for example.