quantum-bb84¶
Use the "Run" button to execute the code.
In [1]:
# Import function from other file
import os
import sys
module_path = os.path.abspath(os.path.join('..'))
if module_path not in sys.path:
sys.path.append(module_path)
from utils import state_to_bloch_vector, draw_and_plot_state, console_print, filter_none, int_array_to_str
----------------------------------------------------------------- $ Console ['print', 'test OK']
In [2]:
%matplotlib inline
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister, Aer, execute
from qiskit.tools.visualization import plot_bloch_vector, plot_histogram
from qiskit.quantum_info import random_statevector, Statevector
from numpy import pi
import numpy
import jovian
sv_simulator = Aer.get_backend('statevector_simulator')
DEBUG = False
DEFAULT_TIMES = 100
Create random base and bit pairs
In [3]:
# Random Bit circuit
qr = QuantumRegister(1)
cr = ClassicalRegister(1)
rdm_circuit = QuantumCircuit(qr,cr)
rdm_circuit.h(qr[0])
rdm_circuit.measure(qr, cr)
rdm_circuit.draw(output='mpl')
Out[3]:
In [4]:
def measure_Z(circuit, shots=1):
return execute(circuit, backend=sv_simulator, shots=shots).result()
# TESTS -----------------------------------------------------------------
measure_Z(rdm_circuit, 1000).get_counts()
Out[4]:
{'1': 484, '0': 516}
In [5]:
def get_measure(circuit, qubit_to_measure, times=DEFAULT_TIMES):
measure = measure_Z(circuit, times)
prob_list = measure.get_counts()
#if DEBUG == True: print(measure)
#if DEBUG == True: print(measure.get_statevector().probabilities_dict([qubit_to_measure]))
#if DEBUG == True: draw_and_plot_state(circuit)
prob_list = measure.get_statevector().probabilities_dict([qubit_to_measure])
prob0 = prob_list['0'] if '0' in prob_list else None
prob1 = prob_list['1'] if '1' in prob_list else None
return(prob0, prob1)
# TESTS -----------------------------------------------------------------
def test_get_measure_should_be_always_1():
measure_circuit = QuantumCircuit(1, 1)
measure_circuit.x(0)
measure_circuit.measure(0,0)
return get_measure(measure_circuit, 0)
def test_get_measure_should_be_always_0():
measure_circuit = QuantumCircuit(3, 3)
measure_circuit.x(0)
measure_circuit.x(1)
measure_circuit.x(2)
measure_circuit.x(2)
measure_circuit.barrier()
measure_circuit.measure(0,0)
measure_circuit.measure(1,1)
return get_measure(measure_circuit, 2)
display(test_get_measure_should_be_always_1())
display(test_get_measure_should_be_always_0())
(None, 1.0)
(1.0, None)
In [6]:
def get_random_bit ():
counts = measure_Z(rdm_circuit).get_counts()
bit = list(counts.keys())[0]
return bit
# TESTS -----------------------------------------------------------------
get_random_bit()
Out[6]:
'0'
In [7]:
def get_random_basis ():
if get_random_bit() == '0':
return 'Z'
else:
return 'X'
# TESTS -----------------------------------------------------------------
get_random_basis()
Out[7]:
'Z'
In [8]:
def get_pair():
base = get_random_basis()
state = get_random_bit()
return (base, int(state))
# TESTS -----------------------------------------------------------------
get_pair()
Out[8]:
('Z', 0)
Based on the pair it returns either a |1>,|0>,|+>,|-> qubit
In [9]:
def pair_to_state_id(pair):
if pair[0]=='Z' and pair[1]==0:
return '0'
elif pair[0]=='Z' and pair[1]==1:
return '1'
elif pair[0]=='X' and pair[1]==0:
return '+'
elif pair[0]=='X' and pair[1]==1:
return '-'
# TESTS -----------------------------------------------------------------
pair_to_state_id(('X', 1))
Out[9]:
'-'
In [10]:
# It should return an array of qubit state identifiers
def to_state_id(array):
qubits_id = []
for pair in array:
if pair is None:
qubits_id.append(None)
continue
qubits_id.append(pair_to_state_id(pair))
return qubits_id
# TESTS -----------------------------------------------------------------
to_state_id([('Z', 0), ('Z', 1), ('X', 0), ('X', 1)])
Out[10]:
['0', '1', '+', '-']
Calls both functions to get an array of qubits
In [11]:
def get_basis_bit_pairs(nb_pairs):
basis_bit_pairs = []
for i in range (0, nb_pairs):
basis_bit_pairs.append(get_pair())
qub = to_state_id(basis_bit_pairs)
return qub, basis_bit_pairs
# TESTS -----------------------------------------------------------------
get_basis_bit_pairs(10)
Out[11]:
(['-', '1', '+', '+', '+', '+', '-', '0', '0', '0'],
[('X', 1),
('Z', 1),
('X', 0),
('X', 0),
('X', 0),
('X', 0),
('X', 1),
('Z', 0),
('Z', 0),
('Z', 0)])
In [12]:
def create_tp_circuit_registers():
qr = QuantumRegister(3)
b_x_tp_reg = ClassicalRegister(1, 'x')
b_z_tp_reg = ClassicalRegister(1, 'z')
b_v_tp_reg = ClassicalRegister(1, 'teleported')
return QuantumCircuit(qr, b_x_tp_reg, b_z_tp_reg, b_v_tp_reg)
# TESTS -----------------------------------------------------------------
create_tp_circuit_registers().draw(output='mpl')
Out[12]:
In [13]:
def add_teleport_gates(teleport_circuit):
teleport_circuit.barrier()
teleport_circuit.h(1)
teleport_circuit.cx(1,2)
teleport_circuit.cx(0,1)
teleport_circuit.h(0)
teleport_circuit.barrier()
teleport_circuit.measure(1,0)
teleport_circuit.measure(0,1)
teleport_circuit.barrier()
return teleport_circuit
# TESTS -----------------------------------------------------------------
def test_add_teleport_gates():
test_circuit = add_teleport_gates(create_tp_circuit_registers())
display(test_circuit.draw(output='mpl'))
test_add_teleport_gates()
Bob gets alice's array and chooses a random base and state (base, state) and creates his own array
In [14]:
def teleport_qubit(teleport_circuit, qubit_id):
# Init signal to send
if qubit_id == '0':
teleport_circuit.reset(0)
elif qubit_id == '1':
teleport_circuit.x(0)
elif qubit_id == '+':
teleport_circuit.h(0)
elif qubit_id == '-':
teleport_circuit.h(0)
teleport_circuit.z(0)
circuit = add_teleport_gates(teleport_circuit)
return circuit
# TESTS -----------------------------------------------------------------
def test_teleport_qubit():
test_circuit = teleport_qubit(create_tp_circuit_registers(), '+')
draw_and_plot_state(test_circuit)
test_teleport_qubit()
In [17]:
def apply_correction(circuit):
circuit.x(2).c_if(0,1)
circuit.z(2).c_if(1,1)
return circuit
# TESTS -----------------------------------------------------------------
def test_apply_correction(q0_value, q1_value):
q_reg = QuantumRegister(3,'q')
c_reg = ClassicalRegister(3, 'c')
test_circuit = QuantumCircuit(q_reg, c_reg)
if q0_value == 1 : test_circuit.x(0)
if q1_value == 1 : test_circuit.x(1)
test_circuit.barrier()
display(draw_and_plot_state(test_circuit))
test_circuit.measure(0, 0)
test_circuit.measure(1, 1)
apply_correction(test_circuit)
display(draw_and_plot_state(test_circuit))
test_apply_correction(0, 1)
None
None
In [18]:
THRESHOLD = 0.98
def measure_with_random_basis(circuit, basis_bit_pairs, qubit_to_measure):
basis = get_random_basis()
if basis == 'X':
circuit.h(qubit_to_measure) # switch measurement in X basis
(prob0, prob1) = get_measure(circuit, qubit_to_measure)
bit = None
if prob0 != None and prob0 >= THRESHOLD:
bit = 0
if prob1 != None and prob1 >= THRESHOLD:
bit = 1
basis_bit_pairs.append((basis, bit))
return basis_bit_pairs
# TESTS -----------------------------------------------------------------
def get_circuit():
q_reg = QuantumRegister(3, 'q')
c_z_reg = ClassicalRegister(1, 'z')
c_x_reg = ClassicalRegister(1, 'x')
c_value_reg = ClassicalRegister(1, 'value')
measure_circuit = QuantumCircuit(q_reg, c_z_reg, c_x_reg, c_value_reg)
return measure_circuit
def test_z_basis_should_be_1():
measure_circuit = QuantumCircuit(1,1)
measure_circuit.x(0)
print(measure_with_random_basis(measure_circuit, [], 0))
if DEBUG == True: draw_and_plot_state(measure_circuit)
def test_x_basis_should_be_1():
measure_circuit = get_circuit()
measure_circuit.h(0)
print(measure_with_random_basis(measure_circuit, [], 0))
if DEBUG == True: draw_and_plot_state(measure_circuit)
test_z_basis_should_be_1()
test_x_basis_should_be_1()
[('X', None)]
[('Z', None)]
In [33]:
# Eve reads the bit and replaces the circuit with an other on
def eve_read_and_replace(circuit, eve_basis_bit_pairs, index):
apply_correction(circuit)
# save alice bit in eve_measure_pair
eve_measure_pair = []
measure_with_random_basis(circuit, eve_measure_pair, 2)
eve_basis_bit_pairs[index] = eve_measure_pair[0]
(_, bit) = eve_measure_pair[0]
# Create a new basis bit pair
# using a random basis
# and the bit read from alice or a random bit if nothing was found
state_id = pair_to_state_id((get_random_basis(), bit or get_random_bit()))
# teleport an other state
circuit.clear()
malicious_teleport_circuit = teleport_qubit(circuit, state_id)
return malicious_teleport_circuit
# TESTS -----------------------------------------------------------------
def test_eve_read_and_replace():
bob_test_circuit = teleport_qubit(create_tp_circuit_registers(), '0')
draw_and_plot_state(bob_test_circuit)
malicious_circuit = eve_read_and_replace(bob_test_circuit, [None], 0)
draw_and_plot_state(malicious_circuit)
test_eve_read_and_replace()
We discard elements on the arrays based on the bases that bob chose and the array of qubits that alice passed
In [20]:
# fill in alice_same_basis_pairs and bob_same_basis_pairs with pairs having the same basis
# input : alice array of pairs [('Z', 1), ['X', 0]], bob array of pairs [('Z', 1), ['X', 0]]
# output : alice array of pairs [('Z', 1), ['X', 0]], alice array of pairs [('Z', 1), ['X', 0]]
def keep_same_basis_pairs(basis_pairs_1, basis_pairs_2):
result = []
for i, pair in enumerate(basis_pairs_1):
bit2 = basis_pairs_2[i][1]
if bit2 is None:
result.append(None)
continue
basis1 = basis_pairs_1[i][0]
basis2 = basis_pairs_2[i][0]
if basis1 == basis2:
result.append(basis_pairs_2[i])
else:
result.append(None)
return result
# TESTS -----------------------------------------------------------------
def test_keep_same_basis_pairs():
test_id1, test_basis_pairs_1 = get_basis_bit_pairs(5)
test_id2, test_basis_pairs_2 = get_basis_bit_pairs(5)
print(test_basis_pairs_1)
print(test_basis_pairs_2)
return keep_same_basis_pairs(test_basis_pairs_1, test_basis_pairs_2)
test_keep_same_basis_pairs()
[('Z', 0), ('X', 0), ('Z', 1), ('Z', 1), ('Z', 1)]
[('X', 1), ('Z', 1), ('X', 0), ('Z', 1), ('Z', 1)]
Out[20]:
[None, None, None, ('Z', 1), ('Z', 1)]
In [21]:
def match_alice_bob_pairs(alice_pairs, bob_pairs):
bob_no_none = []
alice_no_none = []
for i in range (0, len(alice_pairs)):
if bob_pairs[i] != None:
bob_no_none.append(bob_pairs[i])
alice_no_none.append(alice_pairs[i])
return alice_no_none, bob_no_none
# TESTS -----------------------------------------------------------------
match_alice_bob_pairs([('X', 0), ('Z', 1), ('Z', 0)], [None, ('X', 1), None])
Out[21]:
([('Z', 1)], [('X', 1)])
We take a random subset from arrays to see if both of the arrays (alice's and bob's) are the same
In [22]:
# randomly choose SIFTING_LENGTH integers between 0 and length of alice_same_basis_pairs and put them in sifted_indexes
# input : max_length, sifting_length
# output : array of integers
def get_sifting_indexes(array_to_sift, sifting_length):
integer_array = numpy.random.choice(len(array_to_sift), sifting_length, replace=False)
integer_array = numpy.sort(integer_array)
return integer_array
# TESTS -----------------------------------------------------------------
get_sifting_indexes([('X', 1), ('Z', 1), ('Z', 1), ('Z', 1), ('Z', 1), ('Z', 0), ('X', 1)], 3)
Out[22]:
array([2, 3, 6])
In [23]:
# copy bits from same_basis_pairs matching the sifted_indexes in sifted_bits
# used by alice and bob
# input : array of pairs [('Z', 1), ['X', 0]], array of integers indexes
# output : array of bits
def copy_bits_at_indexes(pair_array, integer_array):
sifted_bits = []
for i in integer_array:
sifted_bits.append(pair_array[i][1])
return sifted_bits
# TESTS -----------------------------------------------------------------
def test_copy_bits_at_indexes():
pair_array = [ ('X',1), ('Y',0), ('Y',1), ('X',0), ('Y',0) ]
test_integer_array = [0,1,2]
print(copy_bits_at_indexes(pair_array,test_integer_array))
test_copy_bits = copy_bits_at_indexes(pair_array,test_integer_array)
alice_key = numpy.array(list(zip(test_integer_array, test_copy_bits))) #merge 2 list
matrix = [[0, 1], [1, 0], [2, 1]]
bob_key = numpy.array(matrix)
return numpy.array_equal(alice_key, bob_key)
test_copy_bits_at_indexes()
[1, 0, 1]
Out[23]:
True
In [24]:
# copy bits with index NOT in sifted_indexes from same_basis_pairs into secret_key
# used by alice and bob
# input : array of pairs [('Z', 1), ['X', 0]], array of integers indexes
# output : array of bits
def copy_bit_not_at_indexes(pairs_array, indexes):
bits = []
for index in range (0, len(pairs_array)):
if index not in indexes:
bits.append(pairs_array[index][1])
return bits
# TESTS -----------------------------------------------------------------
def test_copy_bit_not_at_indexes():
indexes_to_exclude = [2, 4]
return copy_bit_not_at_indexes([('X',1), ('Y',0), ('Y',1), ('X',0), ('Y',0)], indexes_to_exclude)
test_copy_bit_not_at_indexes()
Out[24]:
[1, 0, 0]
Key Generation¶
In [25]:
def execute_bb84_protocol(message_length, sifting_length, eve_rate = 0):
KEY_LENGTH = message_length * 3 + sifting_length
# Data
# Secret keys
bob_secret_key = []
alice_secret_key = []
alice_qubit_identifiers = []
# quantum registers
alice_qubit_reg = []
bob_qubit_reg = []
# arrays of basis and classical bit pairs
alice_basis_bit_pairs = []
bob_basis_bit_pairs = []
eve_basis_bit_pairs = [None] * KEY_LENGTH
# arrays of basis and classical bit pairs having the same basis
alice_same_basis_pairs = []
bob_same_basis_pairs = []
alice_no_none_pairs = []
bob_no_none_pairs = []
# sifting
sifted_indexes = []
alice_sifted_bits = []
bob_sifted_bits = []
# generate alice key with random basis
alice_qubit_identifiers, alice_basis_bit_pairs = get_basis_bit_pairs(KEY_LENGTH)
console_print('alice requests a key for a message of ', message_length, 'bits')
console_print('generating key on alice side :', len(alice_qubit_identifiers), 'pairs')
if DEBUG == True: console_print('alice had those pairs:', alice_basis_bit_pairs)
if DEBUG == True: console_print('alice sends:', alice_qubit_identifiers)
# create TP circuit
tp_circuit = create_tp_circuit_registers()
# send each qubit
qubit_position = 0
for qubit_identifier in alice_qubit_identifiers:
#=========== ALICE ===========#
tp_circuit = teleport_qubit(tp_circuit, qubit_identifier)
#=========== EVE ===========#
# compute a probability of eve intervention
does_eve_spies_on_that_qubit = numpy.random.randint(0, 100) <= eve_rate
if eve_rate > 0 and does_eve_spies_on_that_qubit == True:
tp_circuit = eve_read_and_replace(tp_circuit, eve_basis_bit_pairs, qubit_position)
#=========== BOB ===========#
apply_correction(tp_circuit)
measure_with_random_basis(tp_circuit, bob_basis_bit_pairs, 2)
# reset circuit for next communication
tp_circuit.clear()
qubit_position = qubit_position + 1
print('sending qubits to bob [', qubit_position, '/', len(alice_qubit_identifiers), ']', end = '\r')
# endfor
console_print('bob received', len(filter_none(bob_basis_bit_pairs)), 'pairs')
if DEBUG == True: console_print('bob pairs :', to_state_id(bob_basis_bit_pairs))
bob_same_basis_pairs = keep_same_basis_pairs(alice_basis_bit_pairs, bob_basis_bit_pairs)
console_print('bob keeps', len(filter_none(bob_same_basis_pairs)), 'pairs measured with the same basis as alice')
alice_no_none_pairs, bob_no_none_pairs = match_alice_bob_pairs(alice_basis_bit_pairs, bob_same_basis_pairs)
console_print('prepare to sift', sifting_length, 'pairs')
sifted_indexes = get_sifting_indexes(bob_no_none_pairs, sifting_length)
if DEBUG == True: console_print('sifted indexes', sifted_indexes)
alice_sifted_bits = copy_bits_at_indexes(alice_no_none_pairs, sifted_indexes)
bob_sifted_bits = copy_bits_at_indexes(bob_no_none_pairs, sifted_indexes)
if DEBUG == True: console_print('alice sifted bits', alice_sifted_bits)
if DEBUG == True: console_print('bob sifted bits', bob_sifted_bits)
if eve_rate > 0: console_print('Eve spied, at a', eve_rate,'% rate. Has she been caught ? She read', len(filter_none(eve_basis_bit_pairs)), 'pairs')
if eve_rate > 0 and DEBUG == True: console_print('This is what she read:', filter_none(eve_basis_bit_pairs))
if alice_sifted_bits == bob_sifted_bits:
alice_secret_key = copy_bit_not_at_indexes(alice_no_none_pairs, sifted_indexes)
bob_secret_key = copy_bit_not_at_indexes(bob_no_none_pairs, sifted_indexes)
console_print('No Eve detected ! Here\'s the secret key, shhh...')
if DEBUG == True: console_print('Alice key :', alice_secret_key)
if DEBUG == True: console_print('Bob key : ', bob_secret_key)
return int_array_to_str(alice_secret_key) # same as bob_secret_key
else:
console_print('Alert ! Key is compromised : Eve has been spotted')
#Show difference
console_print('Alice and Bob sifted bits are different :\n', alice_sifted_bits, '\n', bob_sifted_bits)
return None
# Parameters
MESSAGE_LENGTH = 50
SIFTING_LENGTH = 10
EVE_RATE = 1 # Between 0 and 100, is the rate of qubits intercepted by eve
print(execute_bb84_protocol(MESSAGE_LENGTH, SIFTING_LENGTH, EVE_RATE))
----------------------------------------------------------------- $ alice requests a key for a message of 50 bits ----------------------------------------------------------------- $ generating key on alice side : 160 pairs sending qubits to bob [ 160 / 160 ] ----------------------------------------------------------------- $ bob received 160 pairs ----------------------------------------------------------------- $ bob keeps 75 pairs measured with the same basis as alice ----------------------------------------------------------------- $ prepare to sift 10 pairs ----------------------------------------------------------------- $ Eve spied, at a 1 % rate. Has she been caught ? She read 3 pairs ----------------------------------------------------------------- $ No Eve detected ! Here's the secret key, shhh... 10111101111010000000010000111111101111101111010000010010111100110
One Time Pad implementation¶
In [26]:
from otpUtils import text_to_binary, encode, decode, get_string_hash, split_message_hash, validate_hash
'b10a8db'
'b10a8db164e0754105b7a99be72e3fe5'
True
True
In [29]:
# Parameters
SIFITING_RATE = 0
EVE_RATE = 90
message = 'Lorem ipsum dolor sit amet'
ENABLE_HASH = True
In [ ]:
def send_secure_message(alice_message, enable_hash=True):
hash_length = 0
message_to_send = alice_message
console_print('sending alice message:', alice_message)
if enable_hash == True:
hash_string = get_string_hash(alice_message)
hash_length = len(hash_string)
message_to_send = alice_message + hash_string
console_print('Securing message integrity with hash:', hash_string)
binary_message = text_to_binary(message_to_send)
sifting_length = int(len(binary_message) * (SIFITING_RATE / 100))
full_key = execute_bb84_protocol(len(binary_message), sifting_length, EVE_RATE)
if full_key == None:
console_print('Eve has been spotted, the key is compromised, try again later !')
return
key = full_key[:len(binary_message)]
encoded_message = encode(binary_message, key)
console_print('sending encoded message', encoded_message)
# alice sends the message to bob somehow
console_print('message arrived: decoding on bob side...')
decoded_string = decode(encoded_message, key)
decoded_message, decoded_hash_string = split_message_hash(decoded_string, hash_length)
if enable_hash == True and validate_hash(decoded_message, decoded_hash_string, hash_length) != True:
console_print('WARNING : message corrupted ! The hash and the message do not match', decoded_message)
console_print('bob received:', decoded_message)
if enable_hash == True: console_print('with hash:', decoded_hash_string)
send_secure_message(message)
In [32]:
def hey():
return None or 'hey'
hey()
Out[32]:
'hey'