Want to change the parameters? Run the live demo on Binder →

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]:
No description has been provided for this image
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]:
No description has been provided for this image
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()
    
No description has been provided for this image

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()
No description has been provided for this image
No description has been provided for this image
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)
No description has been provided for this image
No description has been provided for this image
None
No description has been provided for this image
No description has been provided for this image
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()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

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'