Symmetric vs. asymmetric encryption: Practical Python examples

snyk_sec

SnykSec

Posted on May 16, 2024

Symmetric vs. asymmetric encryption: Practical Python examples

Symmetric and asymmetric encryption are the two most common ways to protect sensitive data with cryptography. These methods use key(s) to transform an unencrypted message into an encrypted message (a ciphertext) that is extremely difficult to decrypt without the correct key(s). Symmetric encryption uses a single key to encrypt and decrypt data. In contrast, asymmetric encryption uses a pair of keys, a public and private key, to encrypt and decrypt sensitive data.

Secure data transmission and storage systems — which prioritize data secrecy — frequently use these encryption methods. For example, end-to-end messaging applications encrypt messages on one end before sending them. This approach prevents an attacker from eavesdropping and reading clear text messages via a man-in-the-middle attack. The contents of an encrypted message are unreadable to an attacker.

Both symmetric and asymmetric encryption are helpful in other use cases, like storing sensitive information such as passwords and health records. Systems typically encrypt this information before storing it on servers or sharing it between computers.

Applications like TLS/SSL also use these methods. During the handshake, when the server sends its public key to the client, the client generates a random pre-master secret and encrypts it using the server’s public key. The server then decrypts this pre-master secret with its private key. Next, both the server and client independently generate the master secret from the pre-master secret along with other handshake parameters.

Since they use the same parameters, both the client and server generate the same master secret, which the application uses to generate the session keys. These symmetric keys help encrypt and decrypt data during the session, ensuring secure communication between the server and client. This process leverages asymmetric encryption for secure and trustless key sharing and secure symmetric key sharing for fast and reliable data transmission.

Both methods help mitigate the security risks that compromise data, like unauthorized access, interception and tampering, theft, and privacy breaches. In this guide, we’ll discuss symmetric and asymmetric encryption, implement them in Python, and explore their best practices.

Prerequisites

To follow this tutorial, you’ll need:

  • Working knowledge of Python
  • A Python toolchain (version 3.7 or higher), including pip, installed
  • An Amazon Web Services (AWS) account. It’s free to create an account and use selected services.

Understanding symmetric encryption

Symmetric encryption uses the same key for encryption and decryption, making it faster and more efficient than asymmetric encryption. It usually follows the sequence below.

  1. Key generation: The sender or trusted third party generates a secret key and then securely shares it with the intended recipient.
  2. Encryption: The sender applies a symmetric encryption algorithm, such as data encryption standard (DES), triple DES (3DES), or advanced encryption standard (AES), to the plaintext message and the shared secret key. This process converts the plaintext to ciphertext.
  3. Data transmission: The sender transmits the ciphertext to the recipient over an insecure channel like the internet.
  4. Decryption: The recipient applies the same symmetric encryption algorithm in reverse using the shared key. This process decrypts the ciphertext to get the original plaintext message.

Due to its greater efficiency and speed, symmetric encryption is preferable over asymmetric encryption for cumbersome tasks like encrypting files, databases, or even entire disk partitions. This method ensures that even if this data at rest becomes compromised, it remains unreadable and encrypted without the proper key.

Symmetric encryption algorithms

Let’s explore DES, 3DES, and AES further and discuss how envelope encryption helps key management.

Data encryption standard (DES)

DES has been a popular symmetric encryption algorithm since the 1970s. It uses a 56-bit key, limiting the key set to 256 (2 to the power of 56) possible keys. DES was fairly secure for about two decades but is now easy to crack with brute-force attacks using modern computing power.

Triple data encryption standard (3DES)

3DES is an upgraded version of DES that uses three DES keys. It offers a higher level of security at 112-bit. However, it’s still inefficient because the key must be 168 bits long to guarantee 112-bit security.

Advanced encryption standard (AES)

AES dates back to the 1990s and is the modern standard for symmetric encryption. Its keys offer 128-, 196-, or 256-bit security levels. Most applications use 128-bit keys since the security difference between 128 and 256-bits is negligible. AES is faster and more secure than 3DES.

Envelope encryption

One significant problem that symmetric encryption faces is key management. A key leak in symmetric encryption is disastrous since encryption and decryption use the same key. Envelope encryption solves this problem. It uses a data encryption key (DEK) and a key encryption key (KEK). The DEK is the primary key the application uses to encrypt data, while the KEK encrypts the DEK.

This two-layer encryption provides additional security because a successful attack would require both keys. Cloud computing environments typically employ envelope encryption for its increased security, flexibility, and scalability. As you’ll learn below, cloud providers often offer helpful interfaces for using envelope encryption and protecting your master key (KEK) from observation.

Implementing symmetric encryption in Python

Let’s build a Python application for encryption and decryption using envelope symmetric encryption. AWS Key Management Service (KMS) offers envelope encryption for key management and the AWS Encryption SDK to communicate with the service.

Getting started

To get started, make a directory for your project:



$ mkdir encryption-demo && cd encryption-demo


Enter fullscreen mode Exit fullscreen mode

Then, follow these steps to create a symmetric KMS key on your AWS dashboard. This KMS key is your customer master key (CMK).

Configure your key’s access permissions, then copy the Amazon Resource Name (ARN) from the General configuration section of your key’s dashboard.


The ARN is your CMK identifier for interacting with the AWS Encryption SDK. It’s the KEK that encrypts the generated DEK.

Then, install the AWS CLI and configure it using the following command:



$ aws configure


Enter fullscreen mode Exit fullscreen mode

When configuring your AWS CLI, ensure the profile has proper CMK permissions. This configuration lets the application communicate correctly with the AWS KMS using saved credentials.

Building the rest of the application

We use the aws-encryption-sdk and argparse packages to build the rest of our application. The aws-encryption-sdk is an implementation of the AWS Encryption SDK for Python. Install these packages using this pip command:



$ pip install aws-encryption-sdk argparse


Enter fullscreen mode Exit fullscreen mode

Now, create a sym_encryptor.py file and add this code to import the required modules:



import aws_encryption_sdk
from aws_encryption_sdk.exceptions import GenerateKeyError
import argparse


Enter fullscreen mode Exit fullscreen mode

Then, add the following code to create the client and key provider classes:



client = aws_encryption_sdk.EncryptionSDKClient()

kms_key_provider = aws_encryption_sdk.StrictAwsKmsMasterKeyProvider(key_ids=[
    ""
])


Enter fullscreen mode Exit fullscreen mode

The EncryptionSDKClient class provides high-level client methods such as encrypt, decrypt, and stream for data encryption. The code configures the StrictAwsKmsMasterKeyProvider class with a list of AWS KMS master keys for encryption and decryption.

Pass the ARN identifier of the CMK you created earlier. Passing the CMK ARN lets the code derive a key from the CMK to encrypt the plaintext.

aws-encryption-sdk automatically generates, encrypts, and decrypts the data key from the CMK. This automation makes it simple to manage keys.

Creating the encryption function

Add the following function to your script to create the encryption function:



def encrypt(message):
    """
    Encrypts the passed message using the provided key.
    Returns the ciphertext of the message.
    """
    try:
        ciphertext, encryptor_header = client.encrypt(
            source=message,
            key_provider=kms_key_provider,
            encryption_context={
                'this is': 'not a secret',
                'but adds': 'some authentication'
            }
        )

        with open("encrypted.bin", "wb") as f:
            f.write(ciphertext)
        print("encryption complete")
    except GenerateKeyError:
        print("Error: data key generation from CMK failed, check ARN")


Enter fullscreen mode Exit fullscreen mode

This function takes the message as binary data and encrypts it using the client’s encrypt() method. The encrypt method takes the data to encrypt, key provider class, and encryption context.

By passing the key provider class created earlier, the client generates a data encryption key from the CMK and encrypts the data. The client returns the ciphertext and an encryptor header, which contains information to cross-check for authenticity. The encryption context is a dictionary with information from the encryptor header that the client returns.

Creating the decryption function

Add the following lines of code to the script to build the decryption function:



def decrypt(ciphertext):
    """
    Decrypts the passed ciphertext using the provided key.
    Returns the original decrypted message.
    """   
    try:
        original_text, decryptor_header = client.decrypt(
            source=ciphertext,
            key_provider=kms_key_provider,
        )

        with open("decrypted.bin", "wb") as f:
            f.write(original_text)
        print("decryption complete")
    except GenerateKeyError:
        print("Error: data key generation from CMK failed, check ARN")


Enter fullscreen mode Exit fullscreen mode

This function takes the ciphertext and calls the client’s decrypt() method with the key provider and ciphertext as arguments. The decrypt method works similarly to the encrypt method. It derives a data key from the configured CMK and reverses the encryption by applying the same key and algorithm to the data. This method returns the decrypted text and a decryptor header similar to the encryptor header. These headers help cross-check the data in real-world applications as both contain the same information, including cipher suite, ARN, and encryption context.

Making a command-line application

To make testing easier, let’s change our application into a command-line application by parsing command-line arguments. Add the following code:



def parse_arguments():
    parser = argparse.ArgumentParser(description="Encrypt/Decrypt files")
    parser.add_argument("-i", metavar="[file]", type=str, required=False, help="file to encrypt/decrypt")

    group = parser.add_mutually_exclusive_group()
    group.add_argument("-e", "--encrypt", action="store_true", help="encrypt mode")
    group.add_argument("-d", "--decrypt", action="store_true", help="decrypt mode")

    args = parser.parse_args()
    return args

# Parse command-line arguments
args = parse_arguments()

file_name = args.i
if file_name:
    with open(file_name, "rb") as f:
        data = f.read()
    if args.encrypt:
        encrypt(data)
    elif args.decrypt:
        decrypt(data)
    else:
        print("must provide encryption/decryption mode: (-e/-d)")


Enter fullscreen mode Exit fullscreen mode

This code parses the command-line arguments. It sets a key flag and flags for choosing between encryption or decryption modes. Save the file and run the following command:



$ python sym_encryptor.py --help


Enter fullscreen mode Exit fullscreen mode

The output is as follows:



usage: sym_encryptor.py [-h] [-i [file]] [-e | -d]

Encrypt/Decrypt files

options:
  -h, --help        show this help message and exit
  -i [file]         file to encrypt/decrypt
  -e, --encrypt  encrypt mode
  -d, --decrypt  decrypt mode


Enter fullscreen mode Exit fullscreen mode

Testing the application

To test the application, first, create an arbitrary text file like the one below:



$ cat > test
Hello World!


Enter fullscreen mode Exit fullscreen mode

Run the file through the application by passing the filename and encryption mode -e flags.



$ python sym_encryptor.py -e -i test


Enter fullscreen mode Exit fullscreen mode

This action creates a new file encrypted.bin.



$ cat encrypted.bin


Enter fullscreen mode Exit fullscreen mode

The output will be similar to the following:



���/Oֶ7E.~RoZР�IT2ƺ�0r%...


Enter fullscreen mode Exit fullscreen mode

This file represents the ciphertext that our application created. We can pass this encrypted file to the recipient. The recipient or other parties must have correct KMS access permissions to the CMK.

Decrypting the file

To simulate the decryption process, run the encrypted file through the application:



$ python sym_encryptor.py -d -i encrypted.bin


Enter fullscreen mode Exit fullscreen mode

You’ll see a notification saying decryption complete.

The code creates a new file named decrypted.file within your directory, as follows:



$ cat decrypted.bin
Output:
Hello World!


Enter fullscreen mode Exit fullscreen mode

We’ve now successfully recovered the original text after encrypting and decrypting the message using envelope symmetric encryption with AWS KMS.

Understanding asymmetric encryption

While the symmetric encryption we explored uses the same key for encryption and decryption, asymmetric encryption uses a key pair: a public key (which the user can publicly share over the internet or other channels) and a second private key (which the user must keep secret).

Security and key management

While symmetric encryption is generally faster and more efficient, asymmetric encryption overcomes the vulnerabilities associated with securely sharing a key between sender and receiver. With asymmetric encryption, we can share the public key freely with anyone — even over insecure channels — since the only way to decrypt such files is with the corresponding private key. The receiver keeps this key secret.

If Alice wants to securely receive encrypted files from Bob, she must generate a public/private key pair and send the public key to Bob. When Bob receives the public key, he can encrypt and transmit the files over the Internet. Alice, who has the private key, is the only person capable of decrypting them.

If Alice then wants to send an encrypted file to Bob, he must create his own public/private key pair. When he sends the public key to Alice, she can encrypt the file using that key and then send it to Bob. Bob then uses his own private key to decrypt that file.

Each person must have their own unique public/private key pair for asymmetric encryption.

Use cases

Applications primarily use asymmetric encryption for secure key exchange in TLS/SSL protocols, secure email communication with Pretty Good Privacy (PGP), and digital certificates verifying the identity of a message’s sender or receiver. Asymmetric encryption provides high security while eliminating the need for secure key sharing.

Asymmetric encryption algorithms

Let’s explore some popular asymmetric encryption algorithms for key-pair generation, including RSA, EIGamal, and Curve25519.

Rivest-Shamir-Adleman (RSA)

RSA is one of the oldest and most popular asymmetric encryption algorithms. It’s based on the mathematical difficulty of factoring large composite numbers into their prime factors.

Key generation in RSA selects two large prime numbers, calculates the modulus and Euler’s totient function, and derives the private and public key components. We can find RSA almost everywhere. It’s incredibly secure when using sufficiently large key sizes — 2048-bit or higher — but more computationally expensive than other algorithms.

ElGamal

ElGamal is based on solving the discrete logarithm problem. This algorithm is faster than RSA but less secure and depends on larger key sizes for better security. Key generation in ElGamal selects a cyclic group and a generator, then calculates the public and private keys using modular exponentiation. Encryption randomizes the plaintext and multiplies it with the recipient’s public key.

Curve25519

Curve25519 is one of the fastest and most secure elliptic curve cryptography (ECC) algorithms. It works for key exchanges and digital signatures, and its ECC algorithms offer strong security but with shorter key lengths than RSA.

ECC uses the mathematical properties of elliptic curves over finite fields. Key generation involves selecting an elliptic curve and a base point, then calculating public and private keys based on scalar multiplication on the curve.

Resource-constrained environments and devices such as IoT and blockchain systems primarily use ECC. It’s more efficient for computation while retaining similar security levels to RSA.

Implementing asymmetric encryption in Python

First, create a new asym_encryptor.py file in your project directory using the following command:



$ touch asym_encryptor.py


Enter fullscreen mode Exit fullscreen mode

Import the following libraries:



from nacl.public import PrivateKey, PublicKey, SealedBox
from nacl.exceptions import TypeError, CryptoError
import argparse


Enter fullscreen mode Exit fullscreen mode

The nacl.public module contains classes for generating private/public key pairs with associated methods for handling encryption/decryption. The PrivateKey and PublicKey classes initialize the matching keys from a sequence of bytes, while the SealedBox encrypts and decrypts messages addressed to a specific key pair. The command also imports the TypeError and CryptoError exception classes for error handling.

Creating a helper function

Next, create a helper function to generate key pairs:



def generate_keys():
    """
    Generates a new key pair
    """
    private_key = PrivateKey.generate()
    public_key = private_key.public_key
    with open("prv.key", "wb") as f:
        f.write(private_key.encode())
    with open("pub.key", "wb") as f:
        f.write(public_key.encode())
    print("generated private/public key pair")


Enter fullscreen mode Exit fullscreen mode

Using the Curve25519 ECC algorithm, this function generates a random PrivateKey object via the generate() method and the corresponding public key for that private key. It then writes the generated keys to their respective files.

Building a function to encrypt messages

Now, add the following function to encrypt messages using the public key:



def encrypt(message, receiver_public_key):
    """
    Encrypts the given message with the public key.
    Returns the ciphertext of the message.
    """
    try:
        with open(receiver_public_key, "rb") as f:
            rpk = PublicKey(f.read())

        box = SealedBox(rpk)
        ciphertext = box.encrypt(message)

        with open("encrypted.bin", "wb") as f:
            f.write(ciphertext)
        print("public key encryption completed")
    except ValueError:
        print("Error: public key must be exactly 32 bytes long, check keys")


Enter fullscreen mode Exit fullscreen mode

This function takes a binary-encoded message in plaintext to encrypt and a string representing the public key’s location. It then loads and initializes the public key with the PublicKey class and initializes a SealedBox using the public key.

Making the decryption function

PyNaCl supports asymmetric encryption using two classes: Box and SealedBox. Box creates a shared key using the sender’s private key and the recipient’s public key for encryption. It requires private and public keys for decryption, providing cryptographic proof of the sender’s authorship.

We use the SealedBox class to build the decryption function, which allows encryption/decryption with just a key pair and no cryptographic proof of authorship. We initialize the SealedBox with the public key for encryption and call the encrypt() method with the message to encrypt. Next, we write the corresponding ciphertext to a file within the working directory.

Add the following code to the application:



def decrypt(ciphertext, receiver_private_key):
    """
    Decrypts the given message with the private key.
    Returns the original decrypted message.
    """
    with open(receiver_private_key, "rb") as f:
        secret_key = PrivateKey(f.read())

    try:
        box = SealedBox(secret_key)
        decrypted_text = box.decrypt(ciphertext)

        with open("decrypted.file", "wb") as f:
            f.write(decrypted_text)
        print("private key decryption completed")
    except TypeError:
        print("Error: invalid ciphertext")
    except CryptoError:
        print("Error: decryption failed, make sure keys are correct")


Enter fullscreen mode Exit fullscreen mode

This function takes the ciphertext to decrypt and the private key. It loads and initializes the private key with the PrivateKey class and initializes a SealedBox with the private key for decryption. The function decrypts the message using the decrypt() method and then saves the decrypted text to a file within the working directory.

Adding command-line parsing

Add command-line parsing using the following code to make it easier to test your application:



def parse_arguments():
    parser = argparse.ArgumentParser(description="Encrypt/Decrypt messages with Asymmetric Encryption")
    parser.add_argument("-i", metavar="[file]", type=str, required=False, help="file to encrypt/decrypt")
    parser.add_argument("-g", "--generate", action="store_true", help="generate keys")
    parser.add_argument("-p", metavar="[key]", type=str, required=False, help="key file")

    group = parser.add_mutually_exclusive_group()
    group.add_argument("-e", "--encrypt", action="store_true", help="encrypt mode")
    group.add_argument("-d", "--decrypt", action="store_true", help="decrypt mode")

    args = parser.parse_args()
    return args

# Parse command-line arguments
args = parse_arguments()

if args.generate:
    generate_keys()
else:
    file_name = args.i
    if file_name:
        with open(file_name, "rb") as f:
            data = f.read()
        key = args.p
        if key:
            if args.encrypt:
                encrypt(data, key)
            elif args.decrypt:
                decrypt(data, key)
            else:
                print("must provide encryption/decryption mode: (-e/-d)")
        else:
            print("provide key for encryption/decryption with -p flag")


Enter fullscreen mode Exit fullscreen mode

This code parses the command-line arguments and sets flags for choosing between encryption and decryption modes, generating key pairs, and passing files.

Save the file, and run the following command:



$ python asym_encryptor.py --help


Enter fullscreen mode Exit fullscreen mode

The output will be the following:



usage: asym_encryptor.py [-h] [-i [file]] [-g] [-p [key]] [-e | -d]

Encrypt/Decrypt messages with Asymmetric Encryption

options:
  -h, --help    show this help message and exit
  -i [file]     file to encrypt/decrypt
  -g, --generate  generate keys
  -p [key]      key file
  -e, --encrypt   encrypt mode
  -d, --decrypt   decrypt mode


Enter fullscreen mode Exit fullscreen mode

Testing the application

To test the application, create an arbitrary text file like the one below:



$ cat > test
Hello World!!!


Enter fullscreen mode Exit fullscreen mode

Use the following command to generate key pairs for testing before running the file through your encryption:



$ python asym_encryptor.py -g


Enter fullscreen mode Exit fullscreen mode

The following is the output:



generated private/public key pair


Enter fullscreen mode Exit fullscreen mode

This command creates the private/public key pairs within the project directory.

Now, run the following command to encrypt the text file using the generated public key:



$ python asym_encryptor.py -e -p pub.key -i test


Enter fullscreen mode Exit fullscreen mode

The output will read:



public key encryption completed


Enter fullscreen mode Exit fullscreen mode

The application creates the ciphertext and saves it as encrypted.bin within the project directory. This encrypted file is completely unreadable. Decryption is only possible with the public key’s corresponding private key, like below:



$ python asym_encryptor.py -d -p prv.key -i encrypted.bin


Enter fullscreen mode Exit fullscreen mode

The output is as follows: 

private key decryption completed

The application has now saved the decrypted file as decrypted.file within the project directory. Read the decrypted file using the command below:



$ cat decrypted.file


Enter fullscreen mode Exit fullscreen mode

You’ll see the output “Hello World!!!” once complete. 

We’ve now successfully encrypted and decrypted a plaintext file using asymmetric encryption.

Security implications and best practices for symmetric and asymmetric encryption

A notable drawback of symmetric encryption is the challenge of sharing keys securely. As the number of participants in the system increases, so does the challenge of key distribution. While asymmetric encryption bypasses this problem with public/private key pairs, symmetric encryption uses methods like envelope encryption to achieve higher security levels.

Here are some practices to follow for symmetric and asymmetric encryption:

  • Leverage tools from providers such as Google Cloud Platform (GCP) or AWS for key management in symmetric key applications, such as AWS KMS.
  • If possible, avoid RSA encryption to future-proof your applications. Its security vectors include undersized keys (fewer than 2048 bits) — which hackers can crack — and poor initialization parameters. It’s challenging to properly implement RSA.
  • Choose symmetric encryption for huge file sizes. It’s more performant than asymmetric solutions.
  • When choosing keys, select appropriately sized keys for the encryption algorithm. Undersized keys provide loopholes. Don’t use insecure algorithms like DES.
  • Don’t roll out your own cryptographic solutions, as it’s easy to make mistakes.

Use tools like Snyk to detect security issues within your code and suggest recommendations. Snyk’s platform enables you to scan, fix, and prioritize security vulnerabilities in your code, open source dependencies, container images, and infrastructure as code (IaC) configurations.

Consider these factors when deciding which type of encryption to use since each technique has its strengths and weaknesses. In practice, you may employ a combination of symmetric and asymmetric encryption to balance efficiency and security.

Using Snyk

To discover how Snyk can find vulnerabilities during development, let’s use it to test our code.

First, create a Snyk account. It’s free to get started.


After signing up, Snyk directs you to the following page, which asks you where to find the code to scan — on GitHub, in Bitbucket Cloud, or through a CLI.


Select CLI and click Next step.

On the next screen, follow any step listed to install the Snyk CLI, then click Next step:


Next, run the following command to authenticate your machine:



$ snyk auth


Enter fullscreen mode Exit fullscreen mode

This command opens a browser window asking you to log in. When you complete this process, the Snyk CLI shows a prompt letting you know you have authenticated successfully.


Now click Next step.


Click Source code and follow the Settings > Snyk Code link to your dashboard. Toggle the option to Enable Snyk Code on your dashboard to run the Snyk CLI on your code. Then, click Save changes.


To test your code, run the following command in the project directory:



$ snyk code test


Enter fullscreen mode Exit fullscreen mode

This command automatically uploads your code files and scans them for vulnerabilities:


The screenshot above shows that our application has a path traversal vulnerability that allows an attacker to read arbitrary files. We can let this vulnerability remain in the code for our demonstration app, but it presents an attack vector in more functional code bases.

We’ve successfully used Snyk to scan our project for vulnerabilities.

Summary

Symmetric and asymmetric encryption differ in how they use keys to encrypt and decrypt files. The former uses a single shared key, while the latter uses a public/private key pair.

Thanks to its implementation and computational efficiency, symmetric encryption is more suited for bulk data encryption at rest. Asymmetric encryption is suitable for small-scale data transmission, such as TLS/SSL and digital certificates.

In this guide, we implemented envelope encryption (combining symmetric and asymmetric encryption) using the AWS Encryption SDK. We also implemented asymmetric encryption and decryption using the Python pynacl library. Along the way, we explored the strengths and weaknesses of both encryption types to smaller key sizes. We also demonstrated how to leverage platforms like Snyk for vulnerability scanning within our applications.

Now that you know more about encryption, you can use these methods to make your applications safer for you and your users. Then, use Snyk to ensure your approach is secure.

💖 💪 🙅 🚩
snyk_sec
SnykSec

Posted on May 16, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related