angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Secure data transfer with Public Key encryption and pyNaCl

pyNaCl make it very easy to implement secure data transfer.

2 December 2023 Updated 2 December 2023
post main image
https://www.pexels.com/@reezky11

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

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.