A Practical Guide to Quantum Key Distribution: The BB84 Protocol Explained

Jefrin Jabez
6 min readJan 24, 2024

--

Have you ever wondered about how secure communication works? Just try thinking about it - What is to stop any random person from reading the signals that you’re sending out from your devices? You’ve probably heard of cryptography… but how does it work?

If we were living in an ideal world, people would not be constantly not trying to get hold of each other’s information. Unfortunately, we don’t. The next best thing is cryptography.

🔐 Cryptography

The two most important elements of cryptography are the channel and the secret key.

📺 The Channel

The channel is the medium through which you would send information to the receiver. This could be anything from optical fibers to letters.

There are two kinds of channels. Public and private. Public channels can be accessed by anyone, while access to private channels are restricted with a secret key.

A public channel can be made private, simply by encrypting your messages with a secret key.

Now, wait… what is a secret key?

🔑 The Secret Key

The secret key restricts access to information, ideally by leveraging bedrock mathematical laws.

This would probably be a set of numbers. It can be of any length — the longer, the better. Longer secret keys are harder to guess.

How does cryptography work?

Cryptography protocols almost always follow these steps:

  1. We encrypt our information using a secret key.
  2. We send our encrypted messages over a channel.
  3. The recipient decrypts the message with the same key.

Sounds simple right?

It’s not.

🦠 The Problem

In order to encrypt and decrypt the information being sent on private channels, both parties would have to have the same secret key.

Sending the key without encryption would make it easy for someone to intercept it and use it to eavesdrop. Sending it with encryption would require us to have another secret key. How would you share that one?

Doesn’t look solvableat first glance

🗝️ Public Key Cryptography

There are various ways to do this classically. One of them involves using something called a public key. As it is obvious from the name, the public key is publicly visible to anyone on the channel.

The public key is used to encrypt the message. But it CANNOT be used to decrypt the message. It can only be decrypted by the two different private keys that each of the parties have.

There is no need to share anything! NOBODY — not even the sender can view the message without the private key of the receiver.

🪽Quantum Key Distribution

But classical is boring…

Quantum Key Distribution (QKD) is another way to securely distribute secret keys.

If you’re familiar with quantum physics, you probably know that measurement alters the states of quantum objects. In this case, qubits.

It would be to your advantage to remember this fact while reading through this because it is critical to understanding what makes QKD so secure.

🚨FULL DISCLAIMER🚨

I’m simplifying things here. A LOT.

I built my own implementation of quantum key distribution. I’m including this in bits and pieces across this article in relevant places. Here’s a small snippet, importing the necessary modules and doing some preliminary setup (setting up the number of bits in the secret key and etc.)

!pip install cirq — quiet
import cirq
from random import choices

encode_gates = {0: cirq.I, 1: cirq.X}
basis_gates = {'Z': cirq.I, 'X': cirq.H}

bits = 15
qubits = cirq.NamedQubit.range(bits, prefix = 'q')

Generating and sending the qubits!

To begin with, the sender generates their key — which would be a series of random zeroes and ones.

sender_key = choices([0,1], k=bits)P

The sender then randomly chooses whether or not to apply a hadamard gate to each of the qubits.

sender_bases = choices(['Z', 'X'], k=bits)

sender_circuit = cirq.Circuit()

for bit in range(bits):
qubit = qubits[bit]
sender_circuit.append(encode_gates[sender_key[bit]](qubit))
sender_circuit.append(basis_gates[sender_bases[bit]](qubit))

Hadamard Gates

Hadamard gates perform a specific kind of operation on qubits — it causes the qubit to attain a special state. The qubit would now be in a superposition.

When we measure qubits that are in a superposition, they collapse into a one state 50% of the time and a zero state 50% of the time.

When a Hadamard gate is applied on a qubit that is in the zero state, then it would turn into a plus state.

If the qubit is in the one state when a hadamard gate is applied, then it would turn into a minus state.

Just so you know, both the plus and the minus states would result in a one state half the time and a zero state half the time. They are both in ‘equalsuperposition.

Bonus: here’s a fancy way of representing these states — |1>, |0>, |->, |+> this is called the Dirac notation.

After doing this, the receiver has something like this: |-> |1> |+> |0> |-> |+> |-> |1> |1> |0>. These qubits are then created and sent to a receiver over a public channel.

Yes, you read that right — over a public channel!

Receiving and measuring the qubits!

Now, the receiver randomly decides whether or not to apply a hadamard gate to each qubit.

receiver_bases = choices(['Z', 'X'], k=bits)

receiver_circuit = cirq.Circuit()

for bit in range(bits):
qubit = qubits[bit]
receiver_circuit.append(basis_gates[sender_bases[bit]](qubit))

Doesn’t seem very useful, does it?

Well, wait for it.

The receiver then measures all of the qubits. The receiver then creates a key based on his own measurement results. This is obviously going to be very different from the key that the receiver had.

receiver_circuit.append(cirq.measure(qubits, key='Receiver Key'))

protocol_circuit = sender_circuit + receiver_circuit

simulator = cirq.Simulator()
results = simulator.run(protocol_circuit)
receiver_key = results.measurements['Receiver Key'][0]

Does it work? (Spoiler: YES!)

The sender and the receiver share whether or not they applied a hadamard gate to each of the qubits. If they did not do the same thing to the qubit, they remove that bit from their key.

final_sender_key = []
final_receiver_key = []

for bit in range(bits):
if sender_bases[bit] == receiver_bases[bit]:
final_sender_key.append(sender_key[bit])
final_receiver_key.append(receiver_key[bit])

Looks good, but won’t the eavesdropper be able to just figure out the key when the sender and the receiver share whether or not they applied a hadamard gate to each of the qubits?

Well, turns out not. As long as the eavesdropper does not know what state each qubit was in, they would not be able to figure out the key.

This is because of the fact that the bits were also randomly chosen in the beginning. Applying the hadamard gate on the 1 state would result in a different state than when applying it on the 0 state.

Sounds good, but what is to stop the interceptor to play the role of the receiver and simply measure the qubits while they are passing through the channel?

Well, turns out that it would not work! Why? Measuring the state of the qubits would alter their states and would result in the sender and receiver having different keys.

Both the sender and receiver share a PART of their keys to ensure that they are both the same and that there were no eavesdroppers before they start communicating.

if final_sender_key[0]==final_receiver_key[0]:
final_sender_key = final_sender_key[1:]
final_receiver_key = final_receiver_key[1:]
print("No eavesdroppers. The keys can be used.")
else:
print("There was an evesdropper! The keys cannot be used!")

If their keys are different, they know that an eavesdropper had been listening and do not communicate.

They can simply wait and share their keys until they match — when an eavesdropper is not making measurements.

Conclusions

This is simply one way to implement quantum key distribution. It’s one of the most popular ones — called BB84.

It has been extensively studied and implemented in various settings. It is secure.

Below is the full version of the code. If you’re familiar with Python and/or Cirq, try pasting this into Google Colabs and play around!

!pip install cirq --quiet
import cirq

from random import choices

encode_gates = {0: cirq.I, 1: cirq.X}
basis_gates = {'Z': cirq.I, 'X': cirq.H}

bits = 15
qubits = cirq.NamedQubit.range(bits, prefix = 'q')

sender_key = choices([0,1], k=bits)

sender_bases = choices(['Z', 'X'], k=bits)

sender_circuit = cirq.Circuit()

for bit in range(bits):
qubit = qubits[bit]
sender_circuit.append(encode_gates[sender_key[bit]](qubit))
sender_circuit.append(basis_gates[sender_bases[bit]](qubit))

receiver_bases = choices(['Z', 'X'], k=bits)

receiver_circuit = cirq.Circuit()

for bit in range(bits):
qubit = qubits[bit]
receiver_circuit.append(basis_gates[sender_bases[bit]](qubit))

receiver_circuit.append(cirq.measure(qubits, key='Receiver Key'))

protocol_circuit = sender_circuit + receiver_circuit
simulator = cirq.Simulator()
results = simulator.run(protocol_circuit)
receiver_key = results.measurements['Receiver Key'][0]

final_sender_key = []
final_receiver_key = []

for bit in range(bits):
if sender_bases[bit] == receiver_bases[bit]:
final_sender_key.append(sender_key[bit])
final_receiver_key.append(receiver_key[bit])

if final_sender_key[0]==final_receiver_key[0]:
final_sender_key = final_sender_key[1:]
final_receiver_key = final_receiver_key[1:]
print("No eavesdroppers. The keys can be used.")
else:
print("There was an evesdropper! The keys cannot be used!")

Looking for a challenge? Try simulating what would happen if there was an eavesdropper using the above code.

Thanks a lot for reading through! If you got anything to say, hit me up on any of my socials here. Feel free to tag me on Twitter with questions you may have too!

--

--

Jefrin Jabez

Student. Hacker. Web Dev. Aspiring Quantum Physicist. Mechatronics Enthusiast. Philosopher.