Implementing A Base Encryption Class With One-Time Pad

Learning Lab
My Journey Through Books, Discoveries, and Ideas

Implementing a base encryption class with one-time pad

To manage different cryptographic methods within my simulation, I created a common structure using an abstract base class in Python. This EncryptionBase class defines a standard interface for all encryption protocols I plan to implement, such as “No Encryption”, BB84, and Ekert.

Abstract base class design

I used Python’s abc module to define EncryptionBase as an abstract base class. This ensures that any concrete encryption protocol inheriting from it must implement certain methods.


from abc import ABC, abstractmethod
import numpy as np

class EncryptionBase(ABC):
    def __init__(self):
        self._key = None
        self._isKeyValid = False
        self._protocol = None # To store the protocol name

    @abstractmethod
    def generateKey(self):
        # Must be implemented by subclasses to create a key
        pass

    @abstractmethod
    def reconcileKey(self, key: str):
        # Must be implemented by subclasses for key agreement/validation
        pass

    @property
    def key(self):
        return self._key

    @property
    def key_bits(self):
        # Helper to view key as a bit string
        if self._key:
            return ''.join(format(byte, '08b') for byte in self._key)
        return None

    @property
    def protocol(self):
        return self._protocol

    def isKeyValid(self):
        return self._isKeyValid

    # encrypt and decrypt methods follow...

The __init__ method initializes the key (_key) to None and a flag _isKeyValid to False. Subclasses are required to implement generateKey for key creation and reconcileKey for the key agreement or verification process specific to the protocol. After successful key generation and reconciliation, the subclass should set _key and turn _isKeyValid to True.

One-time pad encryption and decryption

Once a valid key is established (i.e., _isKeyValid is True), the encrypt and decrypt methods can be used. I implemented these based on the one-time pad (OTP) principle, which relies on the XOR operation. The security of OTP hinges on using a truly random key, keeping it secret, using it only once, and ensuring it’s at least as long as the message.

The encryption process C = P \oplus K involves:

  1. checking if the key is valid,
  2. converting the plaintext message P (string) into a sequence of bytes, then into bits,
  3. converting the shared secret key K (stored as bytes) into bits,
  4. performing a bitwise XOR operation between the message bits and the key bits,
  5. packing the resulting ciphertext bits C back into bytes.

The decryption process P = C \oplus K mirrors this:

  1. checking key validity,
  2. converting the ciphertext C (bytes) into bits,
  3. converting the key K into bits,
  4. performing XOR between ciphertext bits and key bits to recover the message bits,
  5. packing the message bits into bytes and decoding back to a string.

I used NumPy for efficient handling of bit-level operations:


    def encrypt(self, message: str) -> bytes:
        if not self._isKeyValid:
            raise ValueError("Key is not valid for encryption")

        # Convert message string to bits
        message_bytes = message.encode()
        message_bits = np.unpackbits(
            np.frombuffer(message_bytes, dtype=np.uint8))

        # Convert key bytes to bits
        key_bits = np.unpackbits(np.frombuffer(self._key, dtype=np.uint8))

        if len(message_bits) > len(key_bits):
            raise ValueError("Key length is insufficient for this message")

        # XOR message bits with key bits (up to message length)
        cipher_bits = np.bitwise_xor(message_bits,
                                     key_bits[:len(message_bits)])

        # Pack cipher bits back to bytes
        return np.packbits(cipher_bits).tobytes()

    def decrypt(self, cipher: bytes) -> str:
        if not self._isKeyValid:
            raise ValueError("Key is not valid for decryption")

        # Convert cipher bytes to bits
        cipher_bits = np.unpackbits(np.frombuffer(cipher, dtype=np.uint8))

        # Convert key bytes to bits
        key_bits = np.unpackbits(np.frombuffer(self._key, dtype=np.uint8))

        if len(cipher_bits) > len(key_bits):
            # This case implies an issue, key should match during encryption
            raise ValueError("Key length is insufficient (mismatch with ciphertext)")

        # XOR cipher bits with key bits to get original message bits
        message_bits = np.bitwise_xor(cipher_bits, key_bits[:len(cipher_bits)])

        # Pack message bits to bytes and decode to string
        message_bytes = np.packbits(message_bits).tobytes()
        return message_bytes.decode()

This base class now provides a standardized way to handle encryption once a key is securely established by a derived protocol implementation, using the mathematically secure OTP method as the underlying cryptographic operation.

For access to the complete simulation code, please visit the GitHub repository here.