バーナム暗号をPythonで実装してみた

#はじめに
IPUSIRONさんの書籍『暗号技術のすべて』で暗号理論を勉強していた際、バーナム暗号がとてもシンプルにも関わらずその高い安全性に面白いと感じ、バーナム暗号をPythonで実装しようと思い立ちました。
私自身はコーディングセンスがあまりなく、「その書き方ダサくない？」っていうのがある気がするので、そういった点に気づかれた方はコメントで指摘して頂くと大変助かります。

#バーナム暗号
ある文字列pをビット列化したmの長さがnビットだとします。
この時、nビットのランダムなビット列kを生成し、mとkの排他的論理和の結果が暗号文となるのがバーナム暗号です。
バーナム暗号はとてもシンプルで簡単ですが、秘密鍵がランダムで使い捨てな限り解読不能です。
しかし、鍵の長さや、使い捨てという面から効率が悪いのであまり使われていません。

###具体例
"A"をバーナム暗号で暗号化します。
Aのアスキーコード（2進数）は100 0001です。これは7ビットなので、鍵の長さも7ビットとなります。
ランダムに生成した7ビットの鍵が例えば101 1100だったとします。
この時、100 0001と101 1100の排他的論理和は以下のようになります。

```
  100 0001
^ 101 1100
￣￣￣￣￣￣
  001 1101
```

よって、"A"をバーナム暗号で暗号化した場合001 1101となります。
復号する場合、暗号文と鍵の排他的論理和が平文のアスキーコードとなるので、そこから復号できます。

#実装してみよう
###文字列からビット列化
引数として平文の文字列が与えられたとき、それを2進数のアスキーコードに変換する関数を作ります。
ordメソッドで10進のアスキーコードに変換し、それからbinメソッドで2進数に変換するという作業を1文字ずつ行います。
この時、いちいちbinメソッドで変換した2進文字列の最初に"0b"がつくと邪魔なので、下記のコードでは一旦[2:]でスライスして"0b"を取り除いています。

```python
def binary_conversion(plaintext):
	binary_text = ""
	for i in plaintext:
		binary_text += str(bin(ord(i)))[2:]
	return "0b" + binary_text
```

###鍵生成
指定された長さ分だけランダムなビット列を生成するモジュールやら組み込みメソッドが見つからなかった（もっと探したらあったかも）ので、指定された回数だけランダムに"0"と"1"を選択し文字列にくっつけるというやり方をしました。
もっと良い方法あるよって人はコメントで教えていただけると大変助かります。

```python
def random_binary(binary_text):
	binary_len = len(binary_text[2:])
	binary = ["0", "1"]
	key = ""
	for i in range(binary_len):
		key += random.choice(binary)
	return "0b" + key
```

###暗号化
平文の2進文字列と鍵の2進文字列を一旦10進の整数化（intメソッド使用）し、排他的論理和をとります（^演算子）。
その結果をまたbinメソッドで2進文字列化することで暗号文を生成します。

```python
def xor(binary_text, key):
	cipher = bin(int(binary_text, 0) ^ int(key, 0))
	return cipher
```

###復号
とてもめんどくさかったです。なぜめんどくさいかと言うと、暗号文が文字ごとに区切られてないので、7ビット（アスキーコードは7ビットなので）ずつに区切ったあとそれぞれを文字列化しないといけなかったからです。

暗号文を7ビットごとに区切りリストにいれ、それぞれを10進にしたあとchrメソッドで文字化します。

```python
def decryption(cipher, key):
	binary_text = bin(int(cipher, 0) ^ int(key, 0))[2:]
	text_list = []
	plaintext = ""

	for i in range(len(binary_text)):
		i += 1
		if i % 7 == 0:
			text_list.append("0b" + binary_text[i-7:i])

	for i in text_list:
		plaintext += chr(int(i, 0))
	
	return plaintext
```

###コード全体
長いです。

```python
import random

# 文字列をビット列化
def binary_conversion(plaintext):
	binary_text = ""
	for i in plaintext:
		binary_text += str(bin(ord(i)))[2:]
	return "0b" + binary_text

# 鍵生成
def random_binary(binary_text):
	binary_len = len(binary_text[2:])
	binary = ["0", "1"]
	key = ""
	for i in range(binary_len):
		key += random.choice(binary)
	return "0b" + key

# 暗号化
def xor(binary_text, key):
	cipher = bin(int(binary_text, 0) ^ int(key, 0))
	return cipher

# 復号
def decryption(cipher, key):
	binary_text = bin(int(cipher, 0) ^ int(key, 0))[2:]
	text_list = []
	plaintext = ""

	for i in range(len(binary_text)):
		i += 1
		if i % 7 == 0:
			text_list.append("0b" + binary_text[i-7:i])

	for i in text_list:
		plaintext += chr(int(i, 0))
	
	return plaintext

def main():
	enc_or_dec = input("暗号化：0, 復号：1→")

	if enc_or_dec == "0":
		plaintext = input("平文：")
		binary_text = binary_conversion(plaintext)
		key = random_binary(binary_text)
		cipher = xor(binary_text, key)
		print("鍵：{}".format(key))
		print("暗号文：{}".format(cipher))
	elif enc_or_dec == "1":
		cipher = input("暗号文：")
		key = input("鍵：")
		print(decryption(cipher, key))
	else:
		print("正しい値を入力してください。")

main()
```

#検証
上記のコードでちゃんと暗号、復号化できるか確かめます。

まず普通に実行すると最初に以下が表示されます。
何かを暗号化したい場合は0、暗号を復号したい場合は1を入力します。

```
暗号化：0, 復号：1→
```

###暗号化
さきほどの場面で0を入力したら、次は暗号化したい文字列を入力します。
今回は"ANGO"という文字を暗号化することにします。

```
平文：ANGO
```

すると以下のように表示されます。

```
鍵：0b1000110100011011101100110101
暗号文：0b111000100001100011111010
```

鍵はランダムなので、暗号文も実行のたびに変わります。

復号できなければ暗号とは言えません。次は、この暗号がちゃんと復号できるか確かめましょう。

###復号
コードを実行した最初の場面で1を入力します。
すると鍵と暗号文の入力を求められるので、先ほどの実行結果をそのままコピペしてみます。

```
暗号文：0b111000100001100011111010
鍵：0b1000110100011011101100110101
```

すると以下のように表示されました。

```
ANGO
```

復号成功です。

今回は以上です。最後まで見ていただきありがとうございました。