Skip to content

Encryption and decryption of text and Python objects

License

Notifications You must be signed in to change notification settings

ti-oluwa/dcrypt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dcrypt

Use RSA + Fernet encryption to encrypt and decrypt text and Python objects.

Installation

Install with pip:

pip install dcrypt

Usage

dcrypt contains three classes that can be used for encryption and decryption:

  • TextCrypt: Encrypts and decrypts text.
  • ObjectCrypt: Encrypts and decrypts text and Python objects (using pickle).
  • JSONCrypt: ObjectCrypt that encrypts into a JSON parsable format.

Let's start by encrypting some text we want to keep secret:

import dcrypt

# First, we create a cryptkey
cryptkey = dcrypt.CryptKey()

# Then, we create a TextCrypt object with the key
text_crypt = dcrypt.TextCrypt(key=cryptkey)

# Now, we can encrypt some text
encrypted_text = text_crypt.encrypt("This is a secret message!")
print(encrypted_text)

# And decrypt it again
decrypted_text = text_crypt.decrypt(encrypted_text)
print(decrypted_text)

How about encrypting Python objects?

# We can use our existing cryptkey

# Create an ObjectCrypt object with the key
object_crypt = dcrypt.ObjectCrypt(key=cryptkey)

# Encrypt a Python object
my_secrets = {
    "passcode": 1234,
    "password": "password123",
}

encrypted_object = object_crypt.encrypt(my_secrets)
print(encrypted_object)

# The Output would be something like:
# {
#   "passcode": "gAAAAABgJ0Z...",
#   "password": "gAAAAABgJ0Z...",
# }

You could also decide to use the object crypt to encrypt text too.

Okay! Let's assume that we want to store my_secrets in JSON format. It would be nice if my_secrets is encrypted in a format that is JSON serializable. We can do this by using the JSONCrypt class:

# Let's add a tuple of emails to our secrets
my_secrets["emails"] = ("user@host.com", "abc@xyz.com")

# With JSONCrypt
json_crypt = dcrypt.JSONCrypt(key=cryptkey)
encrypted_secrets = json_crypt.encrypt(my_secrets)

# Let's decrypt it again
decrypted_secrets = json_crypt.decrypt(encrypted_secrets)

# Just to be sure, let's check that the decrypted secrets are the same as the original secrets
assert decrypted_secrets == my_secrets

# Oops! We get an error:
# AssertionError
# Why? Because the tuple was converted to a list when we encrypted it.
# So take note of this when using JSONCrypt.

CryptKey

A cryptkey is simply a key that is used to encrypt and decrypt data.

Let's create a cryptkey:

import dcrypt

cryptkey = dcrypt.CryptKey()

Hmm... that was easy. But what actually is a cryptkey? A cryptkey is an object containing a signature used to encrypt and decrypt data. Wondering what the signature is? A cryptkey signature contains four things:

  • A rsa public key
  • A rsa private key
  • An encrypted master key (Fernet key)
  • The hash method used to sign and verify the master key

The rsa keys are used to encrypt and decrypt the master key. The master key is used to encrypt and decrypt data. The rsa keys are generated using the rsa library. The master key is generated using the cryptography library's Fernet class.

What if we want stronger encryption? We can specify the keys signature strength

cryptkey = dcrypt.CryptKey(signature_strength=2)
# The default is 1. But we can specify 2 or 3 for stronger encryption.
# The caveat is that the higher the signature strength, the longer it takes to generate the cryptkey.

We can also specify the hash method used to sign and verify the master key

cryptkey = dcrypt.CryptKey(hash_algorithm="SHA-512")

# See `dcrypt.signature.SUPPORTED_HASH_ALGORITHMS` for a list of supported hash methods.

Saving and Loading CryptKeys

I know, I know. You want to save your cryptkey so you can use it later. You can do this by saving the cryptkey's signature to a file. Let's see how:

import dcrypt

# Shinny new cryptkey
cryptkey = dcrypt.CryptKey()

# Now, let's save it
cryptkey.signature.dump("./secrets_folder/cryptkey.json")

# Yes! We saved it. Now, let's load our key signature back and recreate our cryptkey
signature = dcrypt.Signature.load("./secrets_folder/cryptkey.json")
cryptkey = dcrypt.CryptKey(signature=signature)

# Yay! We have our cryptkey back.

Another reason why you may want to save your key signature is to remove the overhead of generating a new cryptkey every time you want to encrypt or decrypt data. Especially when the signature strength is maxed out(3). You can just load the signature from a file and use it to create a new cryptkey.

Let's talk about cryptkey signatures

The cryptkey signature is a NamedTuple which contains...? Right! A public key, a private key, an encrypted master key and a hash method.

The cool thing about cryptkey signatures is that once created, they cannot be modified. So we can access the public key, private key, encrypted master key and hash method without worrying about them being modified.

Let's see how we can use cryptkey signatures:

import dcrypt

signature = dcrypt.CryptKey.make_signature()
# Yes! we use the `make_signature` classmethod to create a cryptkey signature.

# Now, let's access the public key
public_key = signature.pub_key

# What about the hash method?
hash_method = signature.hash_method

# And the encrypted master key?
encrypted_master_key = signature.enc_master_key

There are two types of cryptkey signatures:

  • Signature
  • CommonSignature

What are the differences between them? Let's start with the CommonSignature. A CommonSignature is a cryptkey signature whose values are all strings. This means that it can be easily serialized and deserialized. This is the type of signature that is saved to a file when we use the dump method.

Unlike the CommonSignature, a Signature is a cryptkey signature whose values are not all strings. Some are byte type. This means that it cannot be easily serialized and deserialized. This is the type of signature that is used to create a cryptkey.

However, we can convert a Signature to a CommonSignature and vice versa:

import dcrypt

# Let's create a cryptkey signature
signature = dcrypt.CryptKey.make_signature()

# Now, let's convert it to a common signature
common_signature = signature.common()

# And back to a signature
signature = dcrypt.Signature.from_common(common_signature)

Easy, right? But why do we need to convert a signature to a common signature? Well, we need to do this when we want to save a cryptkey signature to a file. We can't save a Signature to a file. We can only save a CommonSignature to a file.

Another use case is if we need to send a cryptkey signature over a network, we need to convert it to a common signature first and then convert it back to a signature when we receive it.

import dcrypt
import requests

# Let's create a cryptkey signature
signature = dcrypt.CryptKey.make_signature()

# Now, let's convert it to a common signature
common_signature = signature.common()

# Let's send it over a network
requests.post("https://example.com", json=common_signature.json())

# Now, let's receive it
common_signature_as_json = requests.get("https://example.com").json()

# Construct a common signature from the json
common_signature = dcrypt.CommonSignature(**common_signature_as_json)

# And convert it back to a signature
signature = dcrypt.Signature.from_common(common_signature)
# Easy peasy!

If you noticed, we converted the common signature to json before sending it over the network. You do this using the json method of the CommonSignature class.

Encrypting function output

Say you have a method in a class called Human which returns the contact information of the human which will be sent over a network. You may want to encrypt the result of the method before sending it. How do you do this?

First let's define our Human class:

from dataclasses import dataclass

@dataclass
class Human:
    name: str
    gender: str
    email: str
    phonenumber: str
    address: str
    ...

    def get_contact_info(self):
        return {
            "email": self.email,
            "phonenumber": self.phonenumber,
        }

Now, let's create a cryptkey and an ObjectCrypt object:

import dcrypt

# Create a cryptkey
cryptkey = dcrypt.CryptKey()

# Create an ObjectCrypt object
object_crypt = dcrypt.ObjectCrypt(key=cryptkey)

# Let save the cryptkey signature to a file
cryptkey.signature.dump("./secrets_folder/cryptkey.json")

All that's left is to decorate the get_contact_info method with the object crypt we just created:

class Human:
    ...

    @object_crypt
    def get_contact_info(self):
        return {
            "email": self.email,
            "phonenumber": self.phonenumber,
        }

That's it! Now, the result of the get_contact_info method will be encrypted before it is returned and yes you can decrypt it with the same object crypt or create a new object crypt with the already saved cryptkey signature.

tolu = Human(
    name="Tolu",
    gender="Male",
    email="tioluwa.dev@gmail.com",
    phonenumber="08012345678",
    address="Lagos, Nigeria."
)

# Let's get his contact info
encrypted_contact_info = tolu.get_contact_info()

# The output would be something like:
# {
#   "email": "gAAAAABgJ0Z...",
#   "phonenumber": "gAAAAABgJ0Z...",
# }

# Now, let's decrypt it
signature = dcrypt.Signature.load("./secrets_folder/cryptkey.json")
new_cryptkey = dcrypt.CryptKey(signature=signature)
new_object_crypt = dcrypt.ObjectCrypt(key=new_cryptkey)
decrypted_contact_info = new_object_crypt.decrypt(contact_info)

# The output should be:
# {
#   "email": "tioluwa.dev@gmail",
#   "phonenumber": "08012345678",
# }

You are now ready to use dcrypt to encrypt and decrypt your data. Goodluck!

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Testing

To run the tests, simply run the following command in the root directory of your cloned repository:

python -m unittest discover tests "test_*.py"