I'm going to develop a web application based on CherryPy. It will be an app where public users can register and will login afterwards - the usual stuff. Other frameworks like Rails and Django contain sophisticated code regarding security: encrypted and salted passwords, prevention of session hijacking, ... Is there something like this already available for CherryPy? I found only very simple solutions so far!?
I used this Authentication example including the fixes in the comments.
http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions
Here's how I encrypt
import Crypto.Random
from Crypto.Cipher import AES
import hashlib
# salt size in bytes
SALT_SIZE = 16
# number of iterations in the key generation
NUMBER_OF_ITERATIONS = 20
# the size multiple required for AES
AES_MULTIPLE = 16
__all__ = ['Encryption']
class Encryption(object):
def generate_key(self, password, salt, iterations):
assert iterations > 0
key = password + salt
for i in range(iterations):
key = hashlib.sha256(key).digest()
return key
def pad_text(self, text, multiple):
extra_bytes = len(text) % multiple
padding_size = multiple - extra_bytes
padding = chr(padding_size) * padding_size
padded_text = text + padding
return padded_text
def unpad_text(self, padded_text):
padding_size = padded_text[-1]
text = padded_text[:-padding_size]
return text
def encrypt(self, plaintext, password):
salt = Crypto.Random.get_random_bytes(SALT_SIZE)
key = Encryption.generate_key(self, password, salt, NUMBER_OF_ITERATIONS)
cipher = AES.new(key, AES.MODE_ECB)
padded_plaintext = Encryption.pad_text(self, plaintext, AES_MULTIPLE)
ciphertext = cipher.encrypt(padded_plaintext)
ciphertext_with_salt = salt + ciphertext
return ciphertext_with_salt
Then call the encrypt function
encryptedPassword = Encryption.encrypt(self, Password, bytes(cherrypy.request.app.config['Encryption']['Password'], 'UTF-8'))
Hope this helps!
Andrew
Does this help? - CherryPy Digest and Basic Authentication Tutorial (Internet Archive copy)
I have a cherypy application which uses the ldap module to interact with ldap auth system, but seems your app needs to manage the usr/passwds within itself. You could probably put the registered user/passwords encrypted in a database table and tweak the scheme outlined in the above blog.
Related
I'm working at some project, where i need strong security, because it works with networks.
So, I've made a class for token storage, which encrypts this token via pycrypto, encodes it in base64, and reverse.
Steps are following:
Set up AES encryption keys
Initialize token object
Encrypt it
I need to make sure that there's not any holes in security, and also I'm interested in knowledge: How to make any class/method/variable accessible in only one file?
Code:
from base64 import b64encode, b64decode
from Crypto.Cipher import AES
import sys
import math
class Token:
print("[cobalt] setting up encrytion keys...")
print("[cobalt] reading encryption key file")
key = open('/usr/share/doc/cobalt/encryption', 'r')
lines = key.readlines()[0].split(':')
print("[cobalt] read values, writing private constants...")
__COMMON_ENCRYPTION_KEY = lines[0].strip()
__COMMON_AES_IV16 = lines[1].strip()
del lines
key.close()
print("[cobalt] wrote constants, closing file stream...")
print("[cobalt] setted up encryption keys, initializing token object...")
def __init__(self, token:str):
self._token = token
self._encrypted = self.__encrypt()
print("[cobalt] token object initialized")
def __get_common(self):
print("[cobalt] getting common AES...")
try:
return AES.new(Token.__COMMON_ENCRYPTION_KEY, AES.MODE_CBC, Token.__COMMON_AES_IV16)
except ValueError as e:
print(f"[cobalt]\033[1;31m error:\033[m {e}")
sys.exit(1)
def __encrypt(self) -> str:
print("[cobalt] encrypting token...")
common_cipher = self.__get_common(); print(f"[cobalt] got common AES: {common_cipher}")
token_len = len(self._token); print(f"[cobalt] got token length: {token_len}")
# padded token with next multiple of 16
padded_token = self._token.rjust(16 * math.ceil(token_len / 16)); print("[cobalt] got padded token")
raw_encrypted_token = common_cipher.encrypt(padded_token); print("[cobalt] encrypted, encoding...\n")
return b64encode(raw_encrypted_token).decode('utf-8')
def _decrypt(self) -> str:
print("[cobalt] decrypting token...")
common_cipher = self.__get_common(); print(f"[cobalt] got common AES: {common_cipher}")
raw_token = b64decode(self._encrypted); print("[cobalt] decoded raw token")
decrypted_wpadding = common_cipher.decrypt(raw_token); print("[cobalt] decrypted token\n")
return decrypted_wpadding.decode('utf-8').strip()
For storage you should use one-way functions (sha-2, for example), because you will never need to have its actual value, so you will never need to decode it. Each time user try to get access and use his token (password, cvv code or etc), you just use the same function upon the token and waiting to get the exact same result.
For accessibility you can use accessify
from accessify import private, protected
class Unit:
#protected
def move(self):
pass
#private
def run(self):
pass
Personal thoughts you probably should ignore:
Also I noticed, you use "__". It's good practise, when you want to ship your library to the world. Competent user will not use this methods (and IDE will not provide information about them :)). But it won't save your from bad-bad people. So I personally don't see the point of usage this ugly notation here, when you're coding your server application.
I have a Flask application running that has Flask-Security on it. My config.py file looks like this:
...
# Flask-Security config
SECURITY_URL_PREFIX = "..." # my URL prefix
SECURITY_PASSWORD_HASH = "pbkdf2_sha512"
SECURITY_PASSWORD_SALT = "..." # my 29-character long salt
...
and it generates a hash like this on the database:
$pbkdf2-sha512$25000$pXROaU2JUSrlnDPm3BsjBA$ckspsls2SWPhl9dY7XDiAZh5yucWq27fWRuVj4aOUc5dA2Ez5VH1LYiz5KjaZJscaJYAFhWIwPhkAsHiPOrvrg
which is the password 123456 hashed.
In another application, I needed this hash to be verified, but by using Flask-Security it demands me to be in a Flask application with the same configurations set.
I've tried many things but I just can't validate this password without Flask-Security and there MUST be a way of doing that.
Can you guys please help me on this?
Okay, so thanks to #jwag and few more digging on the Flask-Security source code, I've managed to do it.
For those on the same situation, here's what I did:
import hmac
import hashlib
import base64
from passlib.context import CryptContext
text_type = str
salt = "YOUR_SALT_HERE"
def get_pw_context ():
pw_hash = 'pbkdf2_sha512'
schemes = ['bcrypt', 'des_crypt', 'pbkdf2_sha256', 'pbkdf2_sha512', 'sha256_crypt', 'sha512_crypt', 'plaintext']
deprecated = ['auto']
return CryptContext (schemes=schemes, default=pw_hash, deprecated=deprecated)
def encode_string(string):
if isinstance(string, text_type):
string = string.encode('utf-8')
return string
def get_hmac (password):
h = hmac.new(encode_string(salt), encode_string(password), hashlib.sha512)
return base64.b64encode(h.digest())
def verify_password (password, hash):
return get_pw_context().verify(get_hmac(password), hash)
# Finally
verify_password ("MY_PASSWORD", "MY_PASSWORD_HASH")
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.
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.