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:
- checking if the key is valid,
- converting the plaintext message P (string) into a sequence of bytes, then into bits,
- converting the shared secret key K (stored as bytes) into bits,
- performing a bitwise XOR operation between the message bits and the key bits,
- packing the resulting ciphertext bits C back into bytes.
The decryption process P = C \oplus K mirrors this:
- checking key validity,
- converting the ciphertext C (bytes) into bits,
- converting the key K into bits,
- performing XOR between ciphertext bits and key bits to recover the message bits,
- 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.