Quantum Cryptography Simulation in Python — Deep Dive
BB84 Implementation with Qiskit
Full Protocol Simulation
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
import numpy as np
class BB84Protocol:
def __init__(self, n_bits, eavesdrop=False):
self.n_bits = n_bits
self.eavesdrop = eavesdrop
self.simulator = AerSimulator()
def run(self):
# Alice's random choices
alice_bits = np.random.randint(0, 2, self.n_bits)
alice_bases = np.random.randint(0, 2, self.n_bits) # 0=Z, 1=X
# Bob's random measurement bases
bob_bases = np.random.randint(0, 2, self.n_bits)
# Eve's random intercept bases (if eavesdropping)
eve_bases = np.random.randint(0, 2, self.n_bits)
bob_results = []
for i in range(self.n_bits):
qc = QuantumCircuit(1, 1)
# Alice prepares qubit
if alice_bits[i] == 1:
qc.x(0) # Set to |1⟩
if alice_bases[i] == 1:
qc.h(0) # Rotate to X basis
# Eve intercepts (if enabled)
if self.eavesdrop:
# Eve measures in her random basis
if eve_bases[i] == 1:
qc.h(0)
qc.measure(0, 0)
# Eve must resend — simulated by re-preparing
# based on her measurement result
qc = self._eve_resend(qc, eve_bases[i])
# Bob measures in his chosen basis
if bob_bases[i] == 1:
qc.h(0)
qc.measure(0, 0)
result = self.simulator.run(qc, shots=1).result()
measured = int(list(result.get_counts().keys())[0])
bob_results.append(measured)
bob_results = np.array(bob_results)
# Basis reconciliation: keep matching bases
matching = alice_bases == bob_bases
sifted_alice = alice_bits[matching]
sifted_bob = bob_results[matching]
return {
'sifted_key_alice': sifted_alice,
'sifted_key_bob': sifted_bob,
'sifted_length': len(sifted_alice),
'error_rate': self._compute_qber(sifted_alice, sifted_bob),
'matching_bases': np.sum(matching),
}
def _eve_resend(self, original_qc, eve_basis):
"""Simulate Eve resending after measurement."""
# In simulation: Eve's measurement collapses the state,
# then she re-prepares based on her result
qc = QuantumCircuit(1, 1)
# The result of Eve's measurement determines her resend state
# This is handled implicitly by the quantum circuit
if eve_basis == 1:
qc.h(0)
return qc
def _compute_qber(self, alice_key, bob_key):
"""Quantum Bit Error Rate."""
if len(alice_key) == 0:
return 0.0
errors = np.sum(alice_key != bob_key)
return errors / len(alice_key)
# Run without eavesdropper
bb84 = BB84Protocol(n_bits=1000, eavesdrop=False)
result = bb84.run()
print(f"No Eve - Sifted key length: {result['sifted_length']}")
print(f"No Eve - QBER: {result['error_rate']:.4f}")
# Run with eavesdropper
bb84_eve = BB84Protocol(n_bits=1000, eavesdrop=True)
result_eve = bb84_eve.run()
print(f"With Eve - Sifted key length: {result_eve['sifted_length']}")
print(f"With Eve - QBER: {result_eve['error_rate']:.4f}")
# Expected QBER with Eve: ~25%
Mathematical BB84 Simulation (Fast)
For large-scale analysis, skip the quantum circuit overhead:
def bb84_fast(n_bits, eavesdrop=False, channel_noise=0.0):
"""
Fast mathematical simulation of BB84.
Returns QBER and key material.
"""
alice_bits = np.random.randint(0, 2, n_bits)
alice_bases = np.random.randint(0, 2, n_bits)
bob_bases = np.random.randint(0, 2, n_bits)
# Start with perfect transmission
received_bits = alice_bits.copy()
if eavesdrop:
eve_bases = np.random.randint(0, 2, n_bits)
# Eve measures: wrong basis → 50% chance of flipping
wrong_basis = eve_bases != alice_bases
flip_mask = wrong_basis & (np.random.random(n_bits) < 0.5)
received_bits = received_bits ^ flip_mask
# Channel noise
if channel_noise > 0:
noise_mask = np.random.random(n_bits) < channel_noise
received_bits = received_bits ^ noise_mask
# Bob's measurement: wrong basis → random result
wrong_basis_bob = bob_bases != alice_bases
if eavesdrop:
# After Eve, Bob's wrong-basis measurement on Eve's
# re-prepared state is more complex
random_mask = wrong_basis_bob & (np.random.random(n_bits) < 0.5)
else:
random_mask = wrong_basis_bob & (np.random.random(n_bits) < 0.5)
bob_bits = received_bits ^ random_mask
# Sifting
matching = alice_bases == bob_bases
sifted_alice = alice_bits[matching]
sifted_bob = bob_bits[matching]
qber = np.mean(sifted_alice != sifted_bob)
return sifted_alice, sifted_bob, qber
# Statistical analysis over many runs
qbers_no_eve = []
qbers_with_eve = []
for _ in range(100):
_, _, q = bb84_fast(10000, eavesdrop=False)
qbers_no_eve.append(q)
_, _, q = bb84_fast(10000, eavesdrop=True)
qbers_with_eve.append(q)
print(f"QBER without Eve: {np.mean(qbers_no_eve):.4f} ± {np.std(qbers_no_eve):.4f}")
print(f"QBER with Eve: {np.mean(qbers_with_eve):.4f} ± {np.std(qbers_with_eve):.4f}")
E91 Protocol Implementation
from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
import numpy as np
class E91Protocol:
"""
E91 QKD using entangled pairs and Bell inequality.
Alice measures in bases: 0°, 45°, 90°
Bob measures in bases: 45°, 90°, 135°
"""
def __init__(self, n_pairs):
self.n_pairs = n_pairs
self.simulator = AerSimulator()
# Measurement angles (in units of π/4)
self.alice_angles = [0, np.pi/4, np.pi/2]
self.bob_angles = [np.pi/4, np.pi/2, 3*np.pi/4]
def create_bell_pair(self):
"""Create a Bell state |Φ⁺⟩ = (|00⟩ + |11⟩)/√2."""
qc = QuantumCircuit(2, 2)
qc.h(0)
qc.cx(0, 1)
return qc
def measure_in_basis(self, qc, qubit, angle):
"""Measure qubit along a direction specified by angle."""
qc.ry(-2 * angle, qubit)
return qc
def run(self):
alice_basis_choices = np.random.randint(0, 3, self.n_pairs)
bob_basis_choices = np.random.randint(0, 3, self.n_pairs)
alice_results = []
bob_results = []
for i in range(self.n_pairs):
qc = self.create_bell_pair()
# Apply measurement rotations
a_angle = self.alice_angles[alice_basis_choices[i]]
b_angle = self.bob_angles[bob_basis_choices[i]]
self.measure_in_basis(qc, 0, a_angle)
self.measure_in_basis(qc, 1, b_angle)
qc.measure([0, 1], [0, 1])
result = self.simulator.run(qc, shots=1).result()
bits = list(result.get_counts().keys())[0]
alice_results.append(int(bits[1]))
bob_results.append(int(bits[0]))
alice_results = np.array(alice_results)
bob_results = np.array(bob_results)
# Key generation: use pairs where both chose matching angles
# Alice basis 1 (45°) matches Bob basis 0 (45°)
key_mask = (alice_basis_choices == 1) & (bob_basis_choices == 0)
key_alice = alice_results[key_mask]
key_bob = bob_results[key_mask]
# Bell test: use other basis combinations
bell_value = self._compute_bell(
alice_results, bob_results,
alice_basis_choices, bob_basis_choices
)
return {
'key_alice': key_alice,
'key_bob': key_bob,
'key_length': len(key_alice),
'bell_value': bell_value,
'qber': np.mean(key_alice != key_bob) if len(key_alice) > 0 else 0,
}
def _compute_bell(self, a_res, b_res, a_bases, b_bases):
"""Compute CHSH Bell inequality value."""
def correlation(a_base, b_base):
mask = (a_bases == a_base) & (b_bases == b_base)
if np.sum(mask) == 0:
return 0
a = 2 * a_res[mask] - 1 # Convert 0,1 to -1,+1
b = 2 * b_res[mask] - 1
return np.mean(a * b)
# S = E(a1,b1) - E(a1,b3) + E(a3,b1) + E(a3,b3)
S = (correlation(0, 0) - correlation(0, 2)
+ correlation(2, 0) + correlation(2, 2))
return S
# Run E91
e91 = E91Protocol(n_pairs=5000)
result = e91.run()
print(f"Key length: {result['key_length']}")
print(f"QBER: {result['qber']:.4f}")
print(f"Bell value: {result['bell_value']:.3f}")
print(f"Bell violation (|S| > 2): {abs(result['bell_value']) > 2}")
# Classical limit: |S| ≤ 2
# Quantum maximum: |S| = 2√2 ≈ 2.83
Eavesdropping Strategies
Intercept-Resend Attack
The simplest attack — Eve measures and resends:
def intercept_resend_analysis(n_bits=10000, n_trials=50):
"""Analyze the intercept-resend attack detection probability."""
qbers = []
for _ in range(n_trials):
_, _, qber = bb84_fast(n_bits, eavesdrop=True)
qbers.append(qber)
mean_qber = np.mean(qbers)
print(f"Expected QBER: 25% (theoretical)")
print(f"Measured QBER: {mean_qber*100:.1f}%")
# Detection probability for checking n_check bits
for n_check in [10, 20, 50, 100]:
# Probability of detecting Eve with n_check test bits
p_detect = 1 - (1 - mean_qber) ** n_check
print(f" Detection prob with {n_check} check bits: {p_detect:.6f}")
Beam-Splitting Attack
Eve splits each photon and measures her copy:
def beam_splitting_attack(n_bits, split_ratio=0.5):
"""
Eve splits the photon beam, keeping a fraction.
She can only measure her portion; the rest reaches Bob undisturbed.
"""
alice_bits = np.random.randint(0, 2, n_bits)
alice_bases = np.random.randint(0, 2, n_bits)
bob_bases = np.random.randint(0, 2, n_bits)
# Eve captures some photons entirely (split_ratio)
# and forwards the rest untouched
eve_intercepts = np.random.random(n_bits) < split_ratio
# For intercepted photons: Eve measures, Bob gets nothing (or noise)
eve_info = np.zeros(n_bits, dtype=int)
eve_correct = np.zeros(n_bits, dtype=bool)
eve_bases = np.random.randint(0, 2, n_bits)
eve_correct_basis = eve_bases == alice_bases
eve_info[eve_intercepts & eve_correct_basis] = alice_bits[
eve_intercepts & eve_correct_basis
]
# Bob receives photons Eve didn't intercept
bob_bits = np.full(n_bits, -1)
forwarded = ~eve_intercepts
correct_basis = bob_bases == alice_bases
bob_bits[forwarded & correct_basis] = alice_bits[forwarded & correct_basis]
# Wrong basis → random
wrong_basis = forwarded & ~correct_basis
bob_bits[wrong_basis] = np.random.randint(0, 2, np.sum(wrong_basis))
# Bob needs to report which photons he received
received = forwarded
sifted = received & correct_basis
print(f"Photons forwarded: {np.sum(forwarded)}/{n_bits}")
print(f"Sifted key bits: {np.sum(sifted)}")
print(f"Eve's information on sifted bits: "
f"{np.sum(eve_intercepts & correct_basis)} intercepted in key")
return np.sum(sifted), np.sum(eve_intercepts & correct_basis)
Privacy Amplification
After error correction, Alice and Bob perform privacy amplification to eliminate any information Eve might have:
import hashlib
def privacy_amplification(key, eve_info_bits, security_parameter=10):
"""
Reduce key length to remove Eve's partial information.
key: raw sifted key (numpy array of bits)
eve_info_bits: estimated bits of information Eve has
security_parameter: additional bits to sacrifice for security
"""
raw_length = len(key)
final_length = raw_length - eve_info_bits - security_parameter
if final_length <= 0:
raise ValueError("Not enough key material after privacy amplification")
# Use universal hash function (Toeplitz matrix method)
# Simplified: use SHA-256 and truncate
key_bytes = np.packbits(key).tobytes()
hash_input = key_bytes
# Generate final_length bits using hash
final_key_bits = []
counter = 0
while len(final_key_bits) < final_length:
h = hashlib.sha256(hash_input + counter.to_bytes(4, 'big')).digest()
bits = np.unpackbits(np.frombuffer(h, dtype=np.uint8))
final_key_bits.extend(bits)
counter += 1
return np.array(final_key_bits[:final_length])
# Example
raw_key = np.random.randint(0, 2, 500)
final_key = privacy_amplification(raw_key, eve_info_bits=50)
print(f"Raw key: {len(raw_key)} bits → Final key: {len(final_key)} bits")
Information-Theoretic Security Proof
The security of BB84 can be quantified:
def secure_key_rate(qber, basis_efficiency=0.5):
"""
Calculate the asymptotic secure key rate for BB84.
Rate = basis_efficiency * [1 - 2*h(QBER)]
where h is the binary entropy function.
"""
if qber >= 0.5:
return 0.0
def binary_entropy(p):
if p == 0 or p == 1:
return 0
return -p * np.log2(p) - (1-p) * np.log2(1-p)
rate = basis_efficiency * (1 - 2 * binary_entropy(qber))
return max(0, rate)
# Key rate vs QBER
qbers = np.linspace(0, 0.15, 100)
rates = [secure_key_rate(q) for q in qbers]
# Security threshold: rate drops to 0 at QBER ≈ 11%
threshold = qbers[np.argmin(np.array(rates) > 0) - 1]
print(f"Security threshold QBER: {threshold:.3f}")
# Above this QBER, no secure key can be extracted
Channel Noise vs. Eavesdropping
A key challenge: how do you distinguish noise from eavesdropping?
def noise_vs_eve_analysis():
"""Compare QBER from channel noise vs eavesdropping."""
noise_levels = [0.01, 0.03, 0.05, 0.08, 0.10]
print("Scenario | QBER | Secure?")
print("-" * 52)
for noise in noise_levels:
# Pure channel noise
_, _, q = bb84_fast(50000, eavesdrop=False, channel_noise=noise)
rate = secure_key_rate(q)
status = "✓" if rate > 0 else "✗"
print(f"Noise={noise:.2f}, no Eve | {q:.4f} | {status} (rate={rate:.3f})")
print()
# Eve + channel noise
for noise in [0.00, 0.02, 0.05]:
_, _, q = bb84_fast(50000, eavesdrop=True, channel_noise=noise)
rate = secure_key_rate(q)
status = "✓" if rate > 0 else "✗"
print(f"Noise={noise:.2f}, with Eve | {q:.4f} | {status} (rate={rate:.3f})")
noise_vs_eve_analysis()
In practice, QKD systems must distinguish between physical noise and eavesdropping. Error correction handles noise; privacy amplification handles Eve. The total QBER from both combined must stay below the security threshold.
Real-World QKD Simulation
def realistic_qkd_simulation(
distance_km=50,
fiber_loss_db_per_km=0.2,
detector_efficiency=0.1,
dark_count_rate=1e-6,
source_rate_hz=1e9,
protocol='bb84'
):
"""
Simulate QKD performance with realistic physical parameters.
"""
# Channel transmission
total_loss_db = fiber_loss_db_per_km * distance_km
channel_transmission = 10 ** (-total_loss_db / 10)
# Detection probability
p_detect = channel_transmission * detector_efficiency
# QBER from dark counts
p_dark = dark_count_rate / source_rate_hz
qber_dark = p_dark / (2 * (p_detect + p_dark))
# Raw key rate
raw_rate = source_rate_hz * p_detect * 0.5 # 0.5 for basis matching
# Secure key rate
total_qber = qber_dark # Add other noise sources as needed
skr_fraction = secure_key_rate(total_qber)
secure_rate = raw_rate * skr_fraction
return {
'distance_km': distance_km,
'channel_loss_db': total_loss_db,
'detection_prob': p_detect,
'qber': total_qber,
'raw_key_rate_bps': raw_rate,
'secure_key_rate_bps': secure_rate,
}
# Performance vs distance
for d in [10, 50, 100, 200, 300]:
result = realistic_qkd_simulation(distance_km=d)
print(f"{d:>3} km: loss={result['channel_loss_db']:.0f}dB, "
f"QBER={result['qber']:.6f}, "
f"secure rate={result['secure_key_rate_bps']:.0f} bps")
One thing to remember: Simulating quantum cryptography in Python isn’t just an academic exercise — it’s how researchers design real QKD systems, model attack strategies, optimize protocols for specific hardware, and prove security bounds before building expensive physical infrastructure. The simulation code and the physics it models are two sides of the same coin.
See Also
- Python Cirq Quantum Programming Google's Cirq lets you program quantum computers in Python — like writing a recipe for the world's weirdest kitchen
- Python Pennylane Quantum Ml How PennyLane mixes quantum computing and AI together — like teaching a magical calculator to learn from its mistakes
- Python Qiskit Quantum Circuits How IBM's Qiskit lets you build quantum computer programs in Python — like snapping together LEGO blocks that follow alien physics
- Python Quantum Annealing Python How quantum annealing finds the best solution by shaking problems until the answer falls out — and how D-Wave lets you try it in Python
- Python Quantum Error Correction Why quantum computers make so many mistakes and how Python helps fix them — like spell-check for the universe's tiniest computers