##############################
## IMPORTS ##
##############################
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
import numpy as np
from qiskit.quantum_info import StatevectorQuantum 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
purple_binary = "10011101" + "00000000" + "11111111"
# Get the length of the string
n = len(purple_binary)
# Create a quantum circuit with one qubit and one bit per digit in
# the message to be passed
q = QuantumRegister(n)
c = ClassicalRegister(n)
qc = QuantumCircuit(q,c)
# 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
qc.h(range(n))
# Create a barrier to represent the message being sent
qc.barrier()
statevector = Statevector(qc)
display(statevector.draw(output = 'latex'))
# Use Hadamard gates to collapse the superposition
qc.h(range(n))
statevector = Statevector(qc)
display(statevector.draw(output = 'latex'))
# Measure the results of the circuit
qc.measure(range(n), range(n))
# Draw the circuit
qc.draw(output="mpl", fold=-1)
\[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
simulator = AerSimulator()
results = simulator.run(qc).result().get_counts()
# Sort the returned dictionary in order of counts of each result
sorted_results = sorted(results, key=results.get, reverse=True)
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
purple_binary = "10011101" + "00000000" + "11111111"
# Get the length of the string
n = len(purple_binary)
# Create a quantum circuit with one qubit and one bit per digit in
# the message to be passed
q = QuantumRegister(n)
c = ClassicalRegister(n)
qc = QuantumCircuit(q,c)
# 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
qc.h(range(n))
# Create a barrier to represent the message being sent
qc.barrier()
# Assume that someone looks at (measures) the message during transmission
qc.measure(range(4), range(4))
#qc.measure(range(4), range(4))
qc.barrier()
# Use Hadamard gates to collapse the superposition
qc.h(range(n))
#qc.remove_final_measurements()
#statevector = Statevector(qc)
#display(statevector.draw(output = 'latex'))
print(qc.draw())
# Measure the results of the circuit
qc.measure(range(n), range(n))
# Draw the circuit
qc.draw(output="mpl", fold=-1) ┌───┐┌───┐ ░ ┌─┐ ░ ┌───┐
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
simulator = AerSimulator()
results = simulator.run(qc).result().get_counts()
# Sort the returned dictionary in order of counts of each result
sorted_results = sorted(results, key=results.get, reverse=True)
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:
q = QuantumRegister(1)
c = ClassicalRegister(1)
qc = QuantumCircuit(q,c)
else:
q = QuantumRegister(1)
c = ClassicalRegister(1)
qc = QuantumCircuit(q,c)
qc.x(0)
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:
q = QuantumRegister(1)
c = ClassicalRegister(1)
qc = QuantumCircuit(q,c)
qc.h(0)
else:
q = QuantumRegister(1)
c = ClassicalRegister(1)
qc = QuantumCircuit(q,c)
qc.x(0)
qc.h(0)
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)):
qc = message[i]
if gate_order[i] == 1:
qc.measure(0,0)
results = simulator.run(qc, shots=1).result().get_counts()
# Sort the returned dictionary in order of counts of each result
result = sorted(results, key=results.get, reverse=True)[0]
string.append(result)
else:
qc.h(0)
qc.measure(0,0)
results = simulator.run(qc, shots=1).result().get_counts()
# Sort the returned dictionary in order of counts of each result
result = sorted(results, key=results.get, reverse=True)[0]
string.append(str(result))
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
n = 1000
# Define the number of digits to share at the end of the process
share = 100
# String for Person 1
string1 = random_binary_string(n)
# Order of gates to be applied for Person 1
gate_order1 = random_gate_order(n)
# Person 1 creates the message by applying the gates
# to the string of binary digits
message = apply_gates(string1, gate_order1)
# Person 2 chooses which gates to apply to which digit
# when recieving the secret message
gate_order2 = random_gate_order(n)
# Person 2 uses the random order of gates to decrypt the message
string2 = unapply_gates(message, gate_order2)
# Person 1 and Person 2 compare the order in which they applied
# the gates. Find the indicies where the gates differ
compare = gate_order1 == gate_order2
remove_indices = np.where(compare==False)[0].tolist()
# If the list of indices to be removed is not empty
# remove the digits at those indices
if remove_indices:
string1 = np.delete(string1, remove_indices)
string1 = [str(i) for i in string1]
string1 = ''.join(string1)
string2 = np.delete(string2, remove_indices)
string2 = [str(i) for i in string2]
string2 = ''.join(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]:
secret_key = string1[share:]
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
n = 1000
# Define the number of digits to share at the end of the process
share = 100
# String for Person 1
string1 = random_binary_string(n)
# Order of gates to be applied for Person 1
gate_order1 = random_gate_order(n)
# Person 1 creates the message by applying the gates
# to the string of binary digits
message = apply_gates(string1, gate_order1)
# The spy just measures all of the transmitted circuits
# to determine their value
spy_string = []
for m in message:
m.measure(0,0)
results = simulator.run(m, shots=1).result().get_counts()
# Sort the returned dictionary in order of counts of each result
result = sorted(results, key=results.get, reverse=True)[0]
spy_string.append(str(result))
# Person 2 chooses which gates to apply to which digit
# when recieving the secret message
gate_order2 = random_gate_order(n)
# Person 2 uses the random order of gates to decrypt the message
string2 = unapply_gates(message, gate_order2)
# Person 1 and Person 2 compare the order in which they applied
# the gates. Find the indicies where the gates differ
compare = gate_order1 == gate_order2
remove_indices = np.where(compare==False)[0].tolist()
# If the list of indices to be removed is not empty
# remove the digits at those indices
if remove_indices:
string1 = np.delete(string1, remove_indices)
string1 = [str(i) for i in string1]
string1 = ''.join(string1)
string2 = np.delete(string2, remove_indices)
string2 = [str(i) for i in string2]
string2 = ''.join(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]:
secret_key = string1[share:]
print(secret_key)
else:
print("HACKER")
print("SECRET STRINGS DO NOT MATCH")
HACKER
SECRET STRINGS DO NOT MATCH