CirqでBB84を実装してみた
量子プログラミングのライブラリCirqを使って、量子鍵配送プロトコルBB84を実装してみました。
Cirq
Googleが公開している量子プログラミングのライブラリです。(https://cirq.readthedocs.io/en/stable/)
以前Cirqでフーリエ変換を実装したことがあるので、興味のある方はそちらもどうぞ。(http://tech.innovation.co.jp/2019/03/16/Cirq.html)
BB84
量子鍵配送のプロトコルです。(https://ja.wikipedia.org/wiki/BB84)
BB84をざっくり説明するとこんな感じです(間違ってたら教えてください)
- 送信者はランダムにビット列を生成する
- それぞれのビットをqubitにエンコードして受信者に送信する。その際、X基底・H基底のどちらをランダムに選択してエンコードする
- 受信者はそれぞれのqubitをデコードして値を読み取る。その際、X基底・H基底のどちらをランダムに選択してデコードする
- 送信者と受信者は何番目のビットをどちらの基底でエンコード・デコードしたか教え合う(この通信は量子以外の、通常の通信でよい)
- 送信者は基底が一致したビットを最初に生成したビット列から抽出し、秘密鍵とする
- 受信者は基底が一致したビットを読み取ったビット列から抽出し、秘密鍵とする
このプロトコルは、量子の通信経路・古典の通信経路のどちらを盗聴されても安全に鍵を配送できるように設計されています。
実装
これをCirqで普通に実装すると

こんな感じの回路がずらっと並ぶだけで、いまいち面白みに欠けます。そこで、量子テレポーテーションで送るように実装してみました。
できたもの
main.py で全体の処理を実行します。alice.py が送信者で bob.py が受信者になります。network.py が量子テレポーテーションで送信する部分です。
| import cirq | |
| from alice import Alice | |
| from bob import Bob | |
| from network import Network | |
| def main(): | |
| # 使用するbit数を決める | |
| bit_count = int(input('Bit count : ')) | |
| alice = Alice(bit_count) | |
| bob = Bob() | |
| network = Network() | |
| for index in range(bit_count): | |
| # ビットの初期値を決め、ランダムな基底でエンコードする | |
| circuit, qubits = network.send(alice.encode_function(index)) | |
| # アリスの手元のビットを観測する | |
| alice.measure_qubits(circuit, qubits[0:2]) | |
| # ボブに送ったビットに反映させる | |
| network.reflect_measure_results(circuit, qubits) | |
| # ランダムな基底でデコードする | |
| bob.measure_qubits(circuit, qubits[2]) | |
| # アリスとボブは互いに何番目のビットをどちらの基底でエンコード・デコードしたか教え合う | |
| # 基底が一致したビットを抽出し、秘密鍵とする | |
| alice.set_secret_key(bob.decode_gates) | |
| bob.set_secret_key(alice.encode_gates) | |
| if __name__ == '__main__': | |
| main() |
| import cirq | |
| import random | |
| class Alice: | |
| def __init__(self, bit_count): | |
| self._initial_bits = [random.choice([0, 1]) for _ in range(bit_count)] | |
| self.encode_gates = [random.choice([0, 1]) for _ in range(bit_count)] | |
| self._secret_key = '' | |
| def encode_function(self, index): | |
| def encode_function(qubit): | |
| initial_bit = self._initial_bits[index] | |
| if initial_bit == 1: | |
| yield cirq.X(qubit) | |
| gate = self.encode_gates[index] | |
| # 0:X基底、1:H基底 | |
| if gate == 0: | |
| yield cirq.X(qubit) | |
| else: | |
| yield cirq.H(qubit) | |
| return encode_function | |
| def measure_qubits(self, circuit, qubits): | |
| # qubitを測定する | |
| circuit.append(cirq.measure(*[qubits[0], qubits[1]], key='alice')) | |
| def set_secret_key(self, decode_gates): | |
| self._secret_key = '' | |
| for index, gate in enumerate(self.encode_gates): | |
| # エンコードしたゲートとデコードしたゲートが同じであれば、 | |
| # 対応するinitial_bitを秘密鍵のbitに追加 | |
| if gate == decode_gates[index]: | |
| self._secret_key += str(self._initial_bits[index]) | |
| print('alice_secret_key : ' + self._secret_key) |
| import cirq | |
| import random | |
| class Bob: | |
| def __init__(self): | |
| self._result_bits = [] | |
| self.decode_gates = [] | |
| self._secret_key = '' | |
| def measure_qubits(self, circuit, qubit): | |
| # 0:X基底、1:H基底 | |
| bit = random.choice([0, 1]) | |
| self.decode_gates.append(bit) | |
| if bit == 0: | |
| circuit.append(cirq.X(qubit)) | |
| else: | |
| circuit.append(cirq.H(qubit)) | |
| # qubitを測定する | |
| circuit.append(cirq.measure(*[qubit], key='bob')) | |
| result = cirq.Simulator().run(circuit) | |
| self._result_bits.append(result.measurements['bob'][0][0]) | |
| print(circuit) | |
| def set_secret_key(self, encode_gates): | |
| self._secret_key = '' | |
| for index, gate in enumerate(self.decode_gates): | |
| # エンコードしたゲートとデコードしたゲートが同じであれば、 | |
| # 対応するresult_bitを秘密鍵のbitに追加 | |
| if gate == encode_gates[index]: | |
| self._secret_key += str(self._result_bits[index]) | |
| print('bob_secret_key : ' + self._secret_key) |
| import cirq | |
| class Network: | |
| def send(self, encode_function): | |
| qubits = [cirq.LineQubit(i) for i in range(3)] | |
| circuit = cirq.Circuit( | |
| encode_function(qubits[0]), | |
| cirq.H(qubits[1]), | |
| cirq.CNOT(qubits[1], qubits[2]), | |
| cirq.CNOT(qubits[0], qubits[1]), | |
| cirq.H(qubits[0]) | |
| ) | |
| return circuit, qubits | |
| def reflect_measure_results(self, circuit, qubits): | |
| # 測定結果が1ならボブのqubitにXゲートを追加 | |
| circuit.append([ | |
| cirq.CX(qubits[1], qubits[2]), | |
| ]) | |
| # 測定結果が1ならボブのqubitにZゲートを追加 | |
| circuit.append([ | |
| cirq.CZ(qubits[0], qubits[2]), | |
| ]) |
main.py を実行し、鍵生成に使用するビット数を入力します。

試しに4ビットを使用して鍵を生成すると、以下のようになりました。

生成された鍵が表示されています。元のビット数が少ないので、できた鍵が1ビットしかありません。
以下の部分で1ビット分の情報をAliceからBobに送信しています。複雑に見えますが、殆どがテレポーテーションの為のゲートです。

256ビットを使って生成してみました。

このビット数でも互いの鍵が一致しているので、正常に動いているようです。
通信を盗聴された場合
量子の通信経路を盗聴者Eveによって盗聴された場合、以下のような出力になります。

このように互いの鍵がバラバラになります。これはEveがqubitを観測した時に、ビットの状態が確定したためです。これによって盗聴されたことを検知することができます。
そのため、「鍵を使用する前に先頭の数ビットをお互いに公開し、一致してなければその鍵を使うのをやめる、一致していれば公開していない部分を鍵として通信を行う」という決まりを設けておけば、安全に通信ができます。
おわりに
CirqでBB84を実装してみました。回路自体は単純なので、思ったより簡単に実装できました。今後はもう少し難しい回路にも挑戦してみたいと思います。