A text s has been encrypted with:
s2 = iv + Crypto.Cipher.AES.new(Crypto.Hash.SHA256.new(pwd).digest(),
Crypto.Cipher.AES.MODE_CFB,
iv).encrypt(s.encode())
Then, later, a user inputs the password pwd2 and we decrypt it with:
iv, cipher = s2[:Crypto.Cipher.AES.block_size], s2[Crypto.Cipher.AES.block_size:]
s3 = Crypto.Cipher.AES.new(Crypto.Hash.SHA256.new(pwd2).digest(),
Crypto.Cipher.AES.MODE_CFB,
iv).decrypt(cipher)
Problem: the last line works even if the entered password pw2 is wrong. Of course the decrypted text will be random chars, but no error is triggered.
Question: how to make Crypto.Cipher.AES.new(...).decrypt(cipher) fail if the password pw2 is incorrect? Or at least how to detect a wrong password?
Here is a linked question: Making AES decryption fail if invalid password
and here a discussion about the cryptographic part (less programming) of the question: AES, is this method to say “The password you entered is wrong” secure?
.
AES provides confidentiality but not integrity out of the box - to get integrity too, you have a few options. The easiest and arguably least prone to "shooting yourself in the foot" is to just use AES-GCM - see this Python example or this one.
You could also use an HMAC, but this generally requires managing two distinct keys and has a few more moving parts. I would recommend the first option if it is available to you.
A side note, SHA-256 isn't a very good KDF to use when converting a user created password to an encryption key. Popular password hashing algorithms are better at this - have a look at Argon2, bcrypt or PBKDF2.
Edit: The reason SHA-256 is a bad KDF is the same reason it makes a bad password hash function - it's just too fast. A user created password of, say, 128 bits will usually contain far less entropy than a random sequence of 128 bits - people like to pick words, meaningful sequences etc. Hashing this once with SHA-256 doesn't really alleviate this issue. But hashing it with a construct like Argon2 that is designed to be slow makes a brute-force attack far less viable.
The best way is to use authenticated encryption, and a modern memory-hard entropy-stretching key derivation function such a scrypt to turn the password into a key. The cipher's nounce can be used as salt for the key derivation. With PyCryptodome that could be:
from Crypto.Random import get_random_bytes
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import scrypt
# initialize an AES-128-GCM cipher from password (derived using scrypt) and nonce
def cipherAES(pwd, nonce):
# note: the p parameter should allow use of several processors, but did not for me
# note: changing 16 to 24 or 32 should select AES-192 or AES-256 (not tested)
return AES.new(scrypt(pwd, nonce, 16, N=2**21, r=8, p=1), AES.MODE_GCM, nonce=nonce)
# encryption
nonce = get_random_bytes(16)
print("deriving key from password and nonce, then encrypting..")
ciphertext, tag = cipherAES(b'pwdHklot2',nonce).encrypt_and_digest(b'bonjour')
print("done")
# decryption of nonce, ciphertext, tag
print("deriving key from password and nonce, then decrypting..")
try:
plaintext = cipherAES(b'pwdHklot2', nonce).decrypt_and_verify(ciphertext, tag)
print("The message was: " + plaintext.decode())
except ValueError:
print("Wrong password or altered nonce, ciphertext, tag")
print("done")
Note: Code is here to illustrate the principle. In particular, the scrypt parameters should not be fixed, but rather be included in a header before nonce, ciphertext, and tag; and that must be somewhat grouped for sending, and parsed for decryption.
Caveat: nothing in this post should be construed as an endorsement of PyCryptodome's security.
Addition (per request):
We need scrypt or some other form of entropy stretching only because we use a password. We could use a random 128-bit key directly.
PBKDF2-HMAC-SHAn with 100000 iterations (as in the OP's second code fragment there) is only barely passable to resist Hashcat with a few GPUs. It would would be almost negligible compared to other hurdles for an ASIC-assisted attack: a state of the art Bitcoin mining ASIC does more than 2*1010 SHA-256 per Joule, 1 kWh of electricity costing less than $0.15 is 36*105 J. Crunching these numbers, testing the (62(8+1)-1)/(62-1) = 221919451578091 passwords of up to 8 characters restricted to letters and digits cost less than $47 for energy dedicated to the hashing part.
scrypt is much more secure for equal time spent by legitimate users because it requires a lot of memory and accesses thereof, slowing down the attacker, and most importantly making the investment cost for massively parallel attack skyrocket.
Doesn't use the Crypto package, but this should suit your needs:
import base64
import os
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
def derive_password(password: bytes, salt: bytes):
"""
Adjust the N parameter depending on how long you want the derivation to take.
The scrypt paper suggests a minimum value of n=2**14 for interactive logins (t < 100ms),
or n=2**20 for more sensitive files (t < 5s).
"""
kdf = Scrypt(salt=salt, length=32, n=2**16, r=8, p=1, backend=default_backend())
key = kdf.derive(password)
return base64.urlsafe_b64encode(key)
salt = os.urandom(16)
password = b'legorooj'
bad_password = b'legorooj2'
# Derive the password
key = derive_password(password, salt)
key2 = derive_password(bad_password, salt) # Shouldn't re-use salt but this is only for example purposes
# Create the Fernet Object
f = Fernet(key)
msg = b'This is a test message'
ciphertext = f.encrypt(msg)
print(msg, flush=True) # Flushing pushes it strait to stdout, so the error that will come
print(ciphertext, flush=True)
# Fernet can only be used once, so we need to reinitialize
f = Fernet(key)
plaintext = f.decrypt(ciphertext)
print(plaintext, flush=True)
# Bad Key
f = Fernet(key2)
f.decrypt(ciphertext)
"""
This will raise InvalidToken and InvalidSignature, which means it wasn't decrypted properly.
"""
See my comment for links to the documentation.
For future reference, here is a working solution following the AES GCM mode (recommended by #LukeJoshuaPark in his answer):
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
# Encryption
data = b"secret"
key = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(data)
nonce = cipher.nonce
# Decryption
key2 = get_random_bytes(16) # wrong key
#key2 = key # correct key
try:
cipher = AES.new(key2, AES.MODE_GCM, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
print("The message was: " + plaintext.decode())
except ValueError:
print("Wrong key")
It does fail with an exception when the password is wrong indeed, as desired.
The following code uses a real password derivation function:
import Crypto.Random, Crypto.Protocol.KDF, Crypto.Cipher.AES
def cipherAES(pwd, nonce):
return Crypto.Cipher.AES.new(Crypto.Protocol.KDF.PBKDF2(pwd, nonce, count=100000), Crypto.Cipher.AES.MODE_GCM, nonce=nonce)
# encryption
nonce = Crypto.Random.new().read(16)
cipher = cipherAES(b'pwd1', nonce)
ciphertext, tag = cipher.encrypt_and_digest(b'bonjour')
# decryption
try:
cipher = cipherAES(b'pwd1', nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
print("The message was: " + plaintext.decode())
except ValueError:
print("Wrong password")
#fgrieu's answer is probably better because it uses scrypt as KDF.
Related
I want to achieve the same result in Dart and in Python in encryption using AES.
My problem is that my Python version only outputs the first two bytes successfully and nothing else.
In doing so, I (must) start in Dart.
As seen in the code, I generate an IV and a key, which is passed as a parameter.
List<String> aesEncryption(List<int> key, String message) {
final iv =
enc.IV.fromLength(16); // IV generation
final aesKey = enc.Key(Uint8List.fromList(
key)); // transform the key into the right format
enc.Encrypter encrypter =
enc.Encrypter(enc.AES(aesKey, mode: enc.AESMode.ctr));
final encryptedMessage = encrypter.encrypt(message, iv: iv);
// Here I send my encrypted message and IV to my Flask app, to use the same IV and to
// compare the results with eacht other
return [encryptedMessage.base64, iv.base64];
}
Then, eventually, I just encrypt the message "test" with the following key:
The sha256 Hash of the string "low;high"
I then send this tuple to my Flask app and receive the data as follows:
#app.route("/test", methods=["POST"])
def test():
values = request.get_json()
iv = base64.b64decode(values["iv"]) # my IV which was sent of the Dart App
sig = values["signature"] # just the AES encryption value of the Dart App
key = values["key"] # the string "low;high"
aes_key = hashlib.sha256(key.encode('utf-8')).digest() # as mentioned before, I have
# to hash the string "low;high" first and I will use this as my aes key
# I tested if the computed hash values are the same in python and dart, and yes it is so
# I create a Counter with my IV, I do not completely understand this part, maybe here is my mistake somewhere.
counter = Counter.new(128, initial_value=int.from_bytes(iv, byteorder='big'))
cipher = AES.new(aes_key, AES.MODE_CTR, counter=counter)
test = "test".encode('utf-8')
# The output of this print is: b'\xd52\xf8m\x0b\xcb"\xa7\x0e\xf7\xab\x96\x0f\xfc\x9f\n'
print(base64.b64decode(sig))
encrypted = cipher.encrypt(test)
# The output of this print is: b'\xd52\xf8m'
print(encrypted)
return "", 200
As you can see only the first two bytes are output of the python AES encryption and unfortunately, I don't understand why. I hope someone can help me with this because this implementation is essential for me.
Thank you in advance for every helpful answer!
Kind regards,
Bagheera
I was able to find the error again pretty quickly.
The problem is that automatic PKCS7 padding is used in Dart and not in Python.
So the solution (just for the matter that the encryption values are the same) is the following in Dart:
enc.Encrypter encrypter =
enc.Encrypter(enc.AES(aesKey, mode: enc.AESMode.ctr, padding: null));
i am relatively new to pyhton and the cryptography module, so i'm trying to learn the basics of encrypting and decrypting.
It all works fine when i encrypt a file and decrypt it on the same program, but if i try to just run a decrypt code on a pre-encrypted file (i used the same key, of course) i get an InvalidSignature error, followed by an InvalidToken.
Now, i assumed that for some reason the key didn't match, but they are indeed the same.
Then i thought that for some reason i was passing a string instead of a byte to the functions, or that there are some sort of conversion errors that might alter the encrypted message. But the encrypt-decrypt code works, so i can't figure why the decrypt-only should face errors.
At last, i had a look at the source code for the decrypt function, and tried to figure out if the time stamp had something to do with the error i get, but i couldn't get anything relevant since i'm not too experienced.
This is the encrypt-decrypt code: given a password by the user it encrypts and prints a file, that can decrypt right away.
import base64
import os
from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
print("Insert password: ")
password_str = input()
password = password_str.encode()
salt = os.urandom(16)
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt,
iterations=100000, backend=default_backend())
key = base64.urlsafe_b64encode(kdf.derive(password))
f = Fernet(key)
message = "A really secret message. Not for prying eyes.".encode()
token = f.encrypt(message)
file = open("text_encrypted.txt", "wb")
file.write(token)
file.close()
file = open("text_encrypted.txt", "rb")
data = file.read()
file.close()
token = f.decrypt(data)
file = open("text_decrypted.txt", "wb")
file.write(token)
file.close()
Now, that works just fine and i get the two files that contain the encrypted and decrypted message.
If i delete the:
message = "A really secret message. Not for prying eyes.".encode()
token = f.encrypt(message)
file = open("text_encrypted.txt", "wb")
file.write(token)
file.close()
part, i should be left with just a decryption code, that should work on a previously generated encrypted file, that should be decrypted by the same password.
I'm obviously missing something maybe trivial, since it raises both invalid signature and invalid token.
Thanks for your help
The encryption key you’re using is a result of a PBKDF2. In order for PBKDF2 to return the same encryption key it must get the exact same parameters. That includes salt, which in your example is generated randomly every time.
You need to store the generated salt together with the encrypted file in order to be able to decrypt it later.
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.