##############################
## IMPORTS ##
##############################
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
import numpy as np
from qiskit.quantum_info import Statevector
Quantum Key Distribution
A First Attempt
################################
## SEND A MESSAGE WITH NO SPY ##
################################
# Create a message to be passed, in this case the color purple
# Red contributon, green contribution, purple contribution
= "10011101" + "00000000" + "11111111"
purple_binary # Get the length of the string
= len(purple_binary)
n
# Create a quantum circuit with one qubit and one bit per digit in
# the message to be passed
= QuantumRegister(n)
q = ClassicalRegister(n)
c = QuantumCircuit(q,c)
qc
# Encode the message in the circuit by adding NOT gates to represent ones
for i in range(n):
if purple_binary[i] == "1":
qc.x(i)
# Add Hadamard gates to create a superposition
range(n))
qc.h(
# Create a barrier to represent the message being sent
qc.barrier()= Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
# Use Hadamard gates to collapse the superposition
range(n))
qc.h(
= Statevector(qc)
statevector = 'latex'))
display(statevector.draw(output
# Measure the results of the circuit
range(n), range(n))
qc.measure(
# Draw the circuit
="mpl", fold=-1)
qc.draw(output
\[0.0002441406 |000000000000000000000000\rangle-0.0002441406 |000000000000000000000001\rangle+0.0002441406 |000000000000000000000010\rangle-0.0002441406 |000000000000000000000011\rangle+0.0002441406 |000000000000000000000100\rangle-0.0002441406 |000000000000000000000101\rangle + \ldots -0.0002441406 |111111111111111111111011\rangle+0.0002441406 |111111111111111111111100\rangle-0.0002441406 |111111111111111111111101\rangle+0.0002441406 |111111111111111111111110\rangle-0.0002441406 |111111111111111111111111\rangle\]
\[ |111111110000000010111001\rangle\]
####################################
## SEND A MESSAGE WITH NO SPY ##
## SIMULATE RECIEVING THE MESSAGE ##
####################################
# Simulate running the circuit and compare the top result to the secret message
= AerSimulator()
simulator = simulator.run(qc).result().get_counts()
results # Sort the returned dictionary in order of counts of each result
= sorted(results, key=results.get, reverse=True)
sorted_results print("Number of Results:", len(sorted_results))
print("Messsage Correct?", sorted_results[0][::-1] == purple_binary)
Number of Results: 1
Messsage Correct? True
#############################
## SEND A MESSAGE WITH SPY ##
#############################
# Create a message to be passed, in this case the color purple
# Red contributon, green contribution, purple contribution
= "10011101" + "00000000" + "11111111"
purple_binary # Get the length of the string
= len(purple_binary)
n
# Create a quantum circuit with one qubit and one bit per digit in
# the message to be passed
= QuantumRegister(n)
q = ClassicalRegister(n)
c = QuantumCircuit(q,c)
qc
# Encode the message in the circuit by adding NOT gates to represent ones
for i in range(n):
if purple_binary[i] == "1":
qc.x(i)
# Add Hadamard gates to create a superposition
range(n))
qc.h(
# Create a barrier to represent the message being sent
qc.barrier()
# Assume that someone looks at (measures) the message during transmission
range(4), range(4))
qc.measure(#qc.measure(range(4), range(4))
qc.barrier()
# Use Hadamard gates to collapse the superposition
range(n))
qc.h(
#qc.remove_final_measurements()
#statevector = Statevector(qc)
#display(statevector.draw(output = 'latex'))
print(qc.draw())
# Measure the results of the circuit
range(n), range(n))
qc.measure(
# Draw the circuit
="mpl", fold=-1) qc.draw(output
┌───┐┌───┐ ░ ┌─┐ ░ ┌───┐
q17_0: ┤ X ├┤ H ├─░─┤M├──────────░─┤ H ├
├───┤└───┘ ░ └╥┘┌─┐ ░ ├───┤
q17_1: ┤ H ├──────░──╫─┤M├───────░─┤ H ├
├───┤ ░ ║ └╥┘┌─┐ ░ ├───┤
q17_2: ┤ H ├──────░──╫──╫─┤M├────░─┤ H ├
├───┤┌───┐ ░ ║ ║ └╥┘┌─┐ ░ ├───┤
q17_3: ┤ X ├┤ H ├─░──╫──╫──╫─┤M├─░─┤ H ├
├───┤├───┤ ░ ║ ║ ║ └╥┘ ░ ├───┤
q17_4: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_5: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤└───┘ ░ ║ ║ ║ ║ ░ ├───┤
q17_6: ┤ H ├──────░──╫──╫──╫──╫──░─┤ H ├
├───┤┌───┐ ░ ║ ║ ║ ║ ░ ├───┤
q17_7: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤└───┘ ░ ║ ║ ║ ║ ░ ├───┤
q17_8: ┤ H ├──────░──╫──╫──╫──╫──░─┤ H ├
├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_9: ┤ H ├──────░──╫──╫──╫──╫──░─┤ H ├
├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_10: ┤ H ├──────░──╫──╫──╫──╫──░─┤ H ├
├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_11: ┤ H ├──────░──╫──╫──╫──╫──░─┤ H ├
├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_12: ┤ H ├──────░──╫──╫──╫──╫──░─┤ H ├
├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_13: ┤ H ├──────░──╫──╫──╫──╫──░─┤ H ├
├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_14: ┤ H ├──────░──╫──╫──╫──╫──░─┤ H ├
├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_15: ┤ H ├──────░──╫──╫──╫──╫──░─┤ H ├
├───┤┌───┐ ░ ║ ║ ║ ║ ░ ├───┤
q17_16: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_17: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_18: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_19: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_20: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_21: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_22: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
├───┤├───┤ ░ ║ ║ ║ ║ ░ ├───┤
q17_23: ┤ X ├┤ H ├─░──╫──╫──╫──╫──░─┤ H ├
└───┘└───┘ ░ ║ ║ ║ ║ ░ └───┘
c16: 24/══════════════╩══╩══╩══╩═════════
0 1 2 3
####################################
## SEND A MESSAGE WITH SPY ##
## SIMULATE RECIEVING THE MESSAGE ##
####################################
# Simulate running the circuit and compare the top result to the secret message
= AerSimulator()
simulator = simulator.run(qc).result().get_counts()
results # Sort the returned dictionary in order of counts of each result
= sorted(results, key=results.get, reverse=True)
sorted_results print("Number of Results:", len(sorted_results))
print("Messsage Correct?", sorted_results[0][::-1] == purple_binary)
Number of Results: 16
Messsage Correct? False
A Better Attempt
####################################
## RANDOM BINARY STRING ##
####################################
def random_binary_string (n):
"""
Creates a random binary string (list) of a
specified length.
Inputs:
n (an int): the length of the binary string
to be created
Returns:
Unnamed (a Numpy Array): the random binary
string where each digit is a separate
element of the array.
"""
return np.random.randint(2, size=n)
####################################
## F1 ##
####################################
def F1 (bit):
"""
Applies the F1 filter to the specified binary
digit. The F1 filter does not apply any gates
outside those needed to create the qubit in the
specified state.
Inputs:
bit (an int): a binary digit
Returns:
qc (a qiskit circuit): the quantum circuit
correspondig to the F1 filter being applied
to the specified bit.
"""
if bit == 0:
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc else:
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc 0)
qc.x(return qc
####################################
## F2 ##
####################################
def F2 (bit):
"""
Applies the F2 filter to the specified binary
digit. The F2 filter applies a Hadamard gate
after the circuit has been created in the specified
state.
Inputs:
bit (an int): a binary digit
Returns:
qc (a qiskit circuit): the quantum circuit
correspondig to the F2 filter being applied
to the specified bit.
"""
if bit == 0:
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc 0)
qc.h(else:
= QuantumRegister(1)
q = ClassicalRegister(1)
c = QuantumCircuit(q,c)
qc 0)
qc.x(0)
qc.h(return qc
####################################
## RANDOM GATE ORDER ##
####################################
def random_gate_order (n):
"""
Creates a random array of 1's and 2's to
determine the order the filters (gates)
should be applied.
Inputs:
n (an int): the length of the binary
string
Returns:
Unnamed (a Numpy Array): 1's and 2's which
are used to determine which filter is
applied to which digit in the binary
string.
"""
return np.random.randint(2, size=n)+1
####################################
## APPLY GATES ##
####################################
def apply_gates(string, gate_order):
"""
Applies one gate in the gate_order array to the
corresponding digit in the the string array.
Results in a list of quantum circuits.
Inputs:
string (a Numpy array): represents the string
to be sent
gate_order (a Numpy array): reprsents the order
the filters should be applied to the digits
of the string
Returns:
message (a list of qiskit circuits): the quantum
message to be transmitted.
"""
= []
message for i in range(len(string)):
if gate_order[i] == 1:
message.append(F1(string[i]))else:
message.append(F2(string[i]))return message
####################################
## UNAPPLY GATES ##
####################################
def unapply_gates (message, gate_order):
"""
Decrypts the message sent via quantum key distribution
using a random order of filters.
Inputs:
message (a list of qiskit circuit): The encoded messsage
gate order (a list): The order to apply the filters to
decode the message
Returns:
string (a list): The decoded string, where each element of
the list is a digit in the string
"""
= []
string for i in range(len(message)):
= message[i]
qc if gate_order[i] == 1:
0,0)
qc.measure(= simulator.run(qc, shots=1).result().get_counts()
results # Sort the returned dictionary in order of counts of each result
= sorted(results, key=results.get, reverse=True)[0]
result
string.append(result)else:
0)
qc.h(0,0)
qc.measure(= simulator.run(qc, shots=1).result().get_counts()
results # Sort the returned dictionary in order of counts of each result
= sorted(results, key=results.get, reverse=True)[0]
result str(result))
string.append(return string
################################################
## SIMULATE QUANTUM KEY DISTRIBUTION ##
## ASSUME NO SPY IS WATCHING THE TRANSMISSION ##
################################################
# Define the number of digits to be in the potential secret key
= 1000
n # Define the number of digits to share at the end of the process
= 100
share # String for Person 1
= random_binary_string(n)
string1
# Order of gates to be applied for Person 1
= random_gate_order(n)
gate_order1
# Person 1 creates the message by applying the gates
# to the string of binary digits
= apply_gates(string1, gate_order1)
message
# Person 2 chooses which gates to apply to which digit
# when recieving the secret message
= random_gate_order(n)
gate_order2
# Person 2 uses the random order of gates to decrypt the message
= unapply_gates(message, gate_order2)
string2
# Person 1 and Person 2 compare the order in which they applied
# the gates. Find the indicies where the gates differ
= gate_order1 == gate_order2
compare = np.where(compare==False)[0].tolist()
remove_indices
# If the list of indices to be removed is not empty
# remove the digits at those indices
if remove_indices:
= np.delete(string1, remove_indices)
string1 = [str(i) for i in string1]
string1 = ''.join(string1)
string1
= np.delete(string2, remove_indices)
string2 = [str(i) for i in string2]
string2 = ''.join(string2)
string2
# Now compare the digits in the share length. If the shared
# strings match, remove those digits from the string and create
# the secret key. If they do not match then there is a hacker
if string1[:share] == string2[:share]:
= string1[share:]
secret_key print(secret_key)
else:
print("HACKER")
111111001010011110000110001110111110000010010100001010110100111100110101000100010010111110100111011001101110011110111010101111101110110000001001111110000101100000110100010100100010010001100011100000100011110000000110110111100101011000011111011011111000000001000111011011010100001100111101011100111000011111000110101111001000010010111111110111011000111001011001000010110011111101111110101101100
#############################################
## SIMULATE QUANTUM KEY DISTRIBUTION ##
## ASSUME SPY IS WATCHING THE TRANSMISSION ##
## CASE 1: SPY LOOKS AT THE MESSAGE ##
#############################################
# Define the number of digits to be in the potential secret key
= 1000
n # Define the number of digits to share at the end of the process
= 100
share # String for Person 1
= random_binary_string(n)
string1
# Order of gates to be applied for Person 1
= random_gate_order(n)
gate_order1
# Person 1 creates the message by applying the gates
# to the string of binary digits
= apply_gates(string1, gate_order1)
message
# The spy just measures all of the transmitted circuits
# to determine their value
= []
spy_string for m in message:
0,0)
m.measure(= simulator.run(m, shots=1).result().get_counts()
results # Sort the returned dictionary in order of counts of each result
= sorted(results, key=results.get, reverse=True)[0]
result str(result))
spy_string.append(
# Person 2 chooses which gates to apply to which digit
# when recieving the secret message
= random_gate_order(n)
gate_order2
# Person 2 uses the random order of gates to decrypt the message
= unapply_gates(message, gate_order2)
string2
# Person 1 and Person 2 compare the order in which they applied
# the gates. Find the indicies where the gates differ
= gate_order1 == gate_order2
compare = np.where(compare==False)[0].tolist()
remove_indices
# If the list of indices to be removed is not empty
# remove the digits at those indices
if remove_indices:
= np.delete(string1, remove_indices)
string1 = [str(i) for i in string1]
string1 = ''.join(string1)
string1
= np.delete(string2, remove_indices)
string2 = [str(i) for i in string2]
string2 = ''.join(string2)
string2
# Now compare the digits in the share length. If the shared
# strings match, remove those digits from the string and create
# the secret key. If they do not match then there is a hacker
if string1[:share] == string2[:share]:
= string1[share:]
secret_key print(secret_key)
else:
print("HACKER")
print("SECRET STRINGS DO NOT MATCH")
HACKER
SECRET STRINGS DO NOT MATCH