Secure data transfer with Public Key encryption and pyNaCl
pyNaCl make it very easy to implement secure data transfer.
This is a short post about transferring data safely between two persons. For this we use the Python pyNaCl package to generate private and public keys and to encrypt and decrypt the data. I also added the Python keyring package to store the private_key and public_key. Not really that difficult. I needed a basic class to do this and here I share it. Maybe you find it useful.
As always I am developing on Ubuntu 22.04.
How it works
There are two persons, Bob and Alice. Bob wants to send some data over the internet to Alice in a secure way. Here we are using Public Key encryption.
Bob generates a unique private_key and public_key. He keeps the private_key secret and shares the public_key with Alice. Alice follows the same procedure, keeps her private_key secret, and shares her public_key with Bob.
To send the data to Alice, Bob does the following:
- Bob encrypts the data with the public_key from Alice
- Bob signs the data with his own private_key
When Alice receives the encrypted data, she uses her own private_key to decrypt the data, and uses the public_key of Bob to verify that the data is indeed from Bob.
Storing the private_key and public_key
Most systems have a place where passwords are stored safely, called the system keyring service. The Python keyring package can be used to interface with the system keyring service or use a standalone storage solution.
For every user of our application, we store the public_key and (optional) private_key in the password field, using a dictionary that is converted to and from JSON.
The code
Create a virtual environment and install the following:
> pip install pynacl
> pip install keyring
You can choose Base64 or Hex encoding for the keys and data. Base64 is far more efficient. Do not forget to delete the keys when switching to another encoding!
# my_app.py
import json
import os
import sys
import traceback
import keyring as kr
from nacl.utils import random
from nacl.public import Box, PrivateKey, PublicKey
from nacl.encoding import Base64Encoder, HexEncoder
class PKUtils:
def __init__(
self,
key_encoding='hex',
data_encoding='hex',
kr_servicename=None,
):
self.kr_servicename = kr_servicename
self.key_encoder = Base64Encoder if key_encoding == 'base64' else HexEncoder
self.data_encoder = Base64Encoder if data_encoding == 'base64' else HexEncoder
def get_publ_key_from_priv_key(self, priv_key):
priv_key_obj = PrivateKey(priv_key, encoder=self.key_encoder)
publ_key_obj = priv_key_obj.public_key
publ_key_encoded = publ_key_obj.encode(self.key_encoder)
return publ_key_encoded.decode('ascii')
def create_key_pair(self):
priv_key_obj = PrivateKey.generate()
priv_key_encoded = priv_key_obj.encode(self.key_encoder)
priv_key = priv_key_encoded.decode('ascii')
publ_key = self.get_publ_key_from_priv_key(priv_key)
return priv_key, publ_key
def get_box(self, sender_priv_key, receiver_publ_key):
return Box(
PrivateKey(sender_priv_key, encoder=self.key_encoder),
PublicKey(receiver_publ_key, encoder=self.key_encoder)
)
def encrypt_data(self, sender_priv_key, receiver_publ_key, data):
if isinstance(data, str):
data = bytes(data, 'utf-8')
box = self.get_box(sender_priv_key, receiver_publ_key)
nonce = random(Box.NONCE_SIZE)
encrypted_data = box.encrypt(data, nonce, encoder=self.data_encoder)
return encrypted_data
def decrypt_data(self, receiver_priv_key, sender_publ_key, encrypted_data, decode=None):
box = self.get_box(receiver_priv_key, sender_publ_key)
data = box.decrypt(encrypted_data, encoder=self.data_encoder)
if decode is not None:
data = data.decode('utf-8')
return data
def delete_key_pair_from_kr(self, username):
try:
kr.delete_password(self.kr_servicename, username)
except:
pass
def get_key_pair_from_kr_or_create_new(self, username):
try:
keys = json.loads(kr.get_password(self.kr_servicename, username))
return keys['priv_key'], keys['publ_key']
except:
pass
priv_key, publ_key = self.create_key_pair()
kr.set_password(
self.kr_servicename,
username,
json.dumps({'priv_key': priv_key, 'publ_key': publ_key})
)
return priv_key, publ_key
def set_public_key_in_kr(self, username, publ_key, keep_priv_key=False):
priv_key_cur, publ_key_cur = self.get_key_pair_from_kr_or_create_new(username)
if not keep_priv_key:
priv_key_cur = None
kr.set_password(
self.kr_servicename,
username,
json.dumps({'priv_key': priv_key_cur, 'publ_key': publ_key})
)
def main():
pku = PKUtils(
#key_encoding='base64',
#data_encoding='base64',
kr_servicename='my_app',
)
#pku.delete_key_pair_from_kr('bob')
#pku.delete_key_pair_from_kr('alice')
# bob gets/generates priv_key and publ_key
bob_priv_key, bob_publ_key = pku.get_key_pair_from_kr_or_create_new('bob')
print(f'bob_priv_key = {bob_priv_key}, bob_publ_key = {bob_publ_key}')
# alice gets/generates priv_key and publ_key
alice_priv_key, alice_publ_key = pku.get_key_pair_from_kr_or_create_new('alice')
print(f'alice_priv_key = {alice_priv_key}, alice_publ_key = {alice_publ_key}')
# bob wants to send this data
data = """this is line 1
this is line 2
this is line 3
this is line 4
this is line 5
"""
print(f'data = {data}')
# bob encrypts the data
try:
encrypted_data = pku.encrypt_data(bob_priv_key, alice_publ_key, data)
print(f'encrypted_data = {encrypted_data}')
except Exception as e:
print(f'encryption exception = {type(e).__name__}, e = {e}')
traceback.print_exc()
# bob sends encrypted_data to alice ...
# verify it is working, inject error
#alice_priv_key = alice_publ_key
#bob_publ_key = alice_publ_key
# alice decrypts the received encrypted_data
try:
decrypted_data = pku.decrypt_data(alice_priv_key, bob_publ_key, encrypted_data, decode='utf-8')
print(f'decrypted_data = {decrypted_data}')
except Exception as e:
print(f'decryption exception = {type(e).__name__}, e = {e}')
traceback.print_exc()
print('ready')
if __name__ == '__main__':
main()
Summary
The Python pyNaCl package makes it very easy to implement public_key encryption. Public Key encryption is weak towards man in the middle attack. A third party can modify the public keys.
Also, always remember that the encrypted data may be copied along the way. This means that once a third party gets hold of the keys, all (!) data that has been copied by a third party (over a long period) can be decrypted. To avoid this, you can renew your keys after some time and remove the old keys from your system!
Links / credits
C 431: Public-Key Encryption With Sodium (25 extra)
https://samsclass.info/141/proj/C431.htm
Public Key Encryption
https://www.geeksforgeeks.org/public-key-encryption
PyNaCl
https://pynacl.readthedocs.io/en/latest
Read more
Cryptography
Most viewed
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using PyInstaller and Cython to create a Python executable
- Reducing page response times of a Flask SQLAlchemy website
- Connect to a service on a Docker host from a Docker container
- SQLAlchemy: Using Cascade Deletes to delete related objects
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb