Symmetric vs. asymmetric encryption: Practical Python examples
May 15, 2024
0 mins readSymmetric 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.
Key generation: The sender or trusted third party generates a secret key and then securely shares it with the intended recipient.
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.
Data transmission: The sender transmits the ciphertext to the recipient over an insecure channel like the internet.
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
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
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
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
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=[
"<your-kms-ARN>"
])
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:
1def encrypt(message):
2 """
3 Encrypts the passed message using the provided key.
4 Returns the ciphertext of the message.
5 """
6 try:
7 ciphertext, encryptor_header = client.encrypt(
8 source=message,
9 key_provider=kms_key_provider,
10 encryption_context={
11 'this is': 'not a secret',
12 'but adds': 'some authentication'
13 }
14 )
15
16 with open("encrypted.bin", "wb") as f:
17 f.write(ciphertext)
18 print("encryption complete")
19 except GenerateKeyError:
20 print("Error: data key generation from CMK failed, check ARN")
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:
1def decrypt(ciphertext):
2 """
3 Decrypts the passed ciphertext using the provided key.
4 Returns the original decrypted message.
5 """
6 try:
7 original_text, decryptor_header = client.decrypt(
8 source=ciphertext,
9 key_provider=kms_key_provider,
10 )
11
12 with open("decrypted.bin", "wb") as f:
13 f.write(original_text)
14 print("decryption complete")
15 except GenerateKeyError:
16 print("Error: data key generation from CMK failed, check ARN")
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:
1def parse_arguments():
2 parser = argparse.ArgumentParser(description="Encrypt/Decrypt files")
3 parser.add_argument("-i", metavar="[file]", type=str, required=False, help="file to encrypt/decrypt")
4
5 group = parser.add_mutually_exclusive_group()
6 group.add_argument("-e", "--encrypt", action="store_true", help="encrypt mode")
7 group.add_argument("-d", "--decrypt", action="store_true", help="decrypt mode")
8
9 args = parser.parse_args()
10 return args
11
12# Parse command-line arguments
13args = parse_arguments()
14
15file_name = args.i
16if file_name:
17 with open(file_name, "rb") as f:
18 data = f.read()
19 if args.encrypt:
20 encrypt(data)
21 elif args.decrypt:
22 decrypt(data)
23 else:
24 print("must provide encryption/decryption mode: (-e/-d)")
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
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
Testing the application
To test the application, first, create an arbitrary text file like the one below:
$ cat > test
Hello World!
Run the file through the application by passing the filename and encryption mode -e
flags.
$ python sym_encryptor.py -e -i test
This action creates a new file encrypted.bin
.
$ cat encrypted.bin
The output will be similar to the following:
���/Oֶ7E.~RoZР�IT2ƺ�0r%...
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
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!
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
Import the following libraries:
from nacl.public import PrivateKey, PublicKey, SealedBox
from nacl.exceptions import TypeError, CryptoError
import argparse
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:
1def generate_keys():
2 """
3 Generates a new key pair
4 """
5 private_key = PrivateKey.generate()
6 public_key = private_key.public_key
7 with open("prv.key", "wb") as f:
8 f.write(private_key.encode())
9 with open("pub.key", "wb") as f:
10 f.write(public_key.encode())
11 print("generated private/public key pair")
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:
1def encrypt(message, receiver_public_key):
2 """
3 Encrypts the given message with the public key.
4 Returns the ciphertext of the message.
5 """
6 try:
7 with open(receiver_public_key, "rb") as f:
8 rpk = PublicKey(f.read())
9
10 box = SealedBox(rpk)
11 ciphertext = box.encrypt(message)
12
13 with open("encrypted.bin", "wb") as f:
14 f.write(ciphertext)
15 print("public key encryption completed")
16 except ValueError:
17 print("Error: public key must be exactly 32 bytes long, check keys")
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:
1def decrypt(ciphertext, receiver_private_key):
2 """
3 Decrypts the given message with the private key.
4 Returns the original decrypted message.
5 """
6 with open(receiver_private_key, "rb") as f:
7 secret_key = PrivateKey(f.read())
8
9 try:
10 box = SealedBox(secret_key)
11 decrypted_text = box.decrypt(ciphertext)
12
13 with open("decrypted.file", "wb") as f:
14 f.write(decrypted_text)
15 print("private key decryption completed")
16 except TypeError:
17 print("Error: invalid ciphertext")
18 except CryptoError:
19 print("Error: decryption failed, make sure keys are correct")
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:
1def parse_arguments():
2 parser = argparse.ArgumentParser(description="Encrypt/Decrypt messages with Asymmetric Encryption")
3 parser.add_argument("-i", metavar="[file]", type=str, required=False, help="file to encrypt/decrypt")
4 parser.add_argument("-g", "--generate", action="store_true", help="generate keys")
5 parser.add_argument("-p", metavar="[key]", type=str, required=False, help="key file")
6
7 group = parser.add_mutually_exclusive_group()
8 group.add_argument("-e", "--encrypt", action="store_true", help="encrypt mode")
9 group.add_argument("-d", "--decrypt", action="store_true", help="decrypt mode")
10
11 args = parser.parse_args()
12 return args
13
14# Parse command-line arguments
15args = parse_arguments()
16
17if args.generate:
18 generate_keys()
19else:
20 file_name = args.i
21 if file_name:
22 with open(file_name, "rb") as f:
23 data = f.read()
24 key = args.p
25 if key:
26 if args.encrypt:
27 encrypt(data, key)
28 elif args.decrypt:
29 decrypt(data, key)
30 else:
31 print("must provide encryption/decryption mode: (-e/-d)")
32 else:
33 print("provide key for encryption/decryption with -p flag")
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
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
Testing the application
To test the application, create an arbitrary text file like the one below:
$ cat > test
Hello World!!!
Use the following command to generate key pairs for testing before running the file through your encryption:
$ python asym_encryptor.py -g
The following is the output:
generated private/public key pair
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
The output will read:
public key encryption completed
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
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
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
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
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.