Javaを使ってヴィジュネル暗号をクリブアタック

こんにちは。Qiitaで初投稿です。
ヴィジュネル暗号の解読手法の1つであるクリブアタックに関して、解説しているWebサイトが見当たらなかったので、備忘録も兼ねて記事を書きます。
言語はJavaを使用しています。

#ヴィジュネル暗号とは
まず、ヴィシュネル暗号とは、ヴィジュネル方陣と呼ばれるアルファベットの表を使って暗号化をする暗号化方式です。

`ヴィジュネル方陣`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/901094/f4df97a4-15fb-ad5e-bc50-ac4a71a710c0.png)



暗号化する際は、縦が鍵、横が平文となります。
具体的にどのように暗号化すると言いますと、例えば平文が「IAMASTUDENT」、鍵が「INFO」であるならば、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/901094/6f7d3bfb-16e0-4c21-89df-e2b5b6739830.png)

このような方陣を使います。
平文の1文字目「I」と鍵の1文字目「I」に対応する「Q」が暗号文の1文字目となり、平文の2文字目「A」と鍵の2文字目「N」に対応する「N」が暗号文の2文字目となり...と続けていきます。
鍵は「INFOINFOIN...」と、平文の暗号化が終わるまでループして対応させます。
そうしていくと、「QNROAGZRMAY」という暗号文が出来るという手法です。

#クリブアタックとは
さて、本題のクリブアタックというのがどのような解読手法かと言いますと、平文に入っているであろう文字列を推測して、それを元に鍵を探す手法です。
先ほどの例では、まず、「QNROAGZRMAY」の中に「STUDENT」という単語が入っていると推測します。
縦に推測される文字列を入れて、文字列の1文字目「S」のヴィジュネル方陣「STUVWX...」の中で暗号文の1文字目「Q」に対応する文字「Y」を方陣に入れて、暗号文の2文字目「N」に対応する文字「V」を入れて...を繰り返し、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/901094/41ddfa20-bc9e-58ce-3bc6-3d790ce2300c.png)


このような方陣を作ります。
こうして出来た方陣の中に、推測される文字列がある部分に鍵が出現します。
この方陣の中に、正解の鍵である「INFO」が出現していることが分かります。
今回は綺麗に最初の文字から出現しましたが、「FOINFOIN...」のようにずれて出現することもあります。
気合で探しましょう。

手作業で方陣を作るのは結構手間がかかるので、Javaでコードを書いてみました。

```java:decoding.java
import java.util.Scanner;

public class decoding{
	@SuppressWarnings("resource")
	public static void main(String[]args) {
		int A;
		
		
		//ヴィジュネル方陣の作成
		String tabledata = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
		String table[]= tabledata.split("", 0);
		int tableN[] = new int[table.length];
		
		//AからZを数字に置き換え
		for(int i = 0; i<table.length; i++) {
			tableN[i] = i;
		}

		
		//暗号文の入力受付
		System.out.println("暗号化された文字列を入力してください");
		Scanner inputscan = new Scanner(System.in);
		String inputtext = inputscan.next();
		String input[]= inputtext.split("",0);
		int inputtextnum[]=new int[input.length];
		
		//入力文字列(暗号文)を数字に置き換え
		for(int i=0;i<input.length; i++) {
			for(int j=0; j<table.length; j++) {
				if(input[i].equals(table[j])) {
					inputtextnum[i]=j;
				}
			}
		}

		
		//候補文字列の入力受付
		System.out.println("暗号文に入っている文字列の入力");
		Scanner keyscan = new Scanner(System.in);
		String key = keyscan.next();
		String keywords[] =key.split("",0);
		int keywordsnum[]= new int[keywords.length];

		//入力文字列(候補文字列)を数字に置き換え
		for(int i=0; i<keywords.length; i++) {
			for(int j=0; j<table.length; j++) {
				if(keywords[i].equals(table[j])) {
					keywordsnum[i] = j;
				}
			}
		}


		System.out.println();
		System.out.println("  "+inputtext);

		//クリブアタックの表作成
		for(int i=0;i<keywordsnum.length;i++) {
			A = 0;
			System.out.print(keywords[i]+" ");
			for(int j=0; j<inputtextnum.length;j++) {
				if(keywordsnum[i]>inputtextnum[j]) {
					A = table.length-(keywordsnum[i] - inputtextnum[j]);
				}else {
					A = inputtextnum[j] - keywordsnum[i];
				}
				System.out.print(table[A]);
			}
			System.out.println();
		}
		
		
		//鍵の入力受付
		System.out.println();
		System.out.println("鍵の入力");
		Scanner truekeyscan = new Scanner(System.in);
		String truekey = truekeyscan.next();
		String truekeyword[] = truekey.split("",0);
		int truekeywordnum[]= new int[truekeyword.length];

		//鍵を数字に置き換え
		for(int i=0; i<truekeyword.length;i++) {
			for(int j=0; j<table.length; j++) {
				if(truekeyword[i].equals(table[j])) {
					truekeywordnum[i] = j;
				}
			}
		}

		
		//入力された鍵を使って複合
		System.out.println();
		int j=0;
		for(int i=0; i<input.length; i++) {
			if(truekeywordnum[j]>inputtextnum[i]) {
				A = table.length-(truekeywordnum[j] - inputtextnum[i]);
			}else {
				A = inputtextnum[i] - truekeywordnum[j];
			}
			System.out.print(table[A]);
			j++;
			if(j>=truekeyword.length) {
				j=0;
			}
		}
	}
}
```

#コードの解説
まず、A~Zまでのアルファベットを入れた配列`table[]`と、そのアルファベットを0～25までの数字に置き換えた配列`tableN[]`を作成します。

```java:
		String tabledata = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
		String table[]= tabledata.split("", 0);
		int tableN[] = new int[table.length];

		for(int i = 0; i<table.length; i++) {
			tableN[i] = i;
		}
```

次に、暗号文の入力を`inputscan`で受け付け、一文字ずつ配列`input[]`に入れ、先ほど作成した配列`table[]`を参照し、それぞれの文字を数字に置き換えて配列`inputtextnum[]`に入れます。

```java:
		System.out.println("暗号化された文字列を入力してください");
		Scanner inputscan = new Scanner(System.in);
		String inputtext = inputscan.next();
		String input[]= inputtext.split("",0);
		int inputtextnum[]=new int[input.length];

		for(int i=0;i<input.length; i++) {
			for(int j=0; j<table.length; j++) {
				if(input[i].equals(table[j])) {
					inputtextnum[i]=j;
				}
			}
		}
```

同じく推測される文字列を`keyscan`で受け付け、一文字ずつ配列`keywords[]`に入れ、配列`table[]`を参照し、文字を数字に置き換えて配列`keywordsnum[]`に入れます。

```java:
		System.out.println("暗号文に入っている文字列の入力");
		Scanner keyscan = new Scanner(System.in);
		String key = keyscan.next();
		String keywords[] =key.split("",0);
		int keywordsnum[]= new int[keywords.length];

		for(int i=0; i<keywords.length; i++) {
			for(int j=0; j<table.length; j++) {
				if(keywords[i].equals(table[j])) {
					keywordsnum[i] = j;
				}
			}
		}
```

そして、クリブアタックで鍵を探すための表を作成します。
推測される文字列を1文字ずつ暗号文の文字に参照するため、外側に`keywordnum[]`の長さだけ繰り返すfor文を、内側に`inputtextnum[]`の長さだけ繰り返すfor文を二重for文で書きます。
`keywordnum[i]`が`inputtextnum[j]`より大きい場合、つまり推測される文字列の文字が暗号文の文字よりアルファベット順で後ろに来る場合は、その差の大きさをアルファベットの数26文字(25)から引くと表に追加するべき文字の大きさが求められます。
`keywordnum[i]`が`inputtextnum[j]`より小さい場合は、そのまま`keywordnum[i]`から`inputtextnum[j]`を引けば追加するべき文字の大きさが求められます。

```java:
		for(int i=0;i<keywordsnum.length;i++) {
			A = 0;
			System.out.print(keywords[i]+" ");
			for(int j=0; j<inputtextnum.length;j++) {
				if(keywordsnum[i]>inputtextnum[j]) {
					A = table.length-(keywordsnum[i] - inputtextnum[j]);
				}else {
					A = inputtextnum[j] - keywordsnum[i];
				}
				System.out.print(table[A]);
			}
			System.out.println();
		}
```

こうして、クリブアタックの表が作成されます。
ここから気合で鍵を見つけましょう。

鍵を見つけられたら、鍵を`truekeyscan`で受け付け、一文字ずつ`truekeyword[]`に入れ、数字に置き換えたものを`truekeywordnum[]`に入れます。

```java:
		System.out.println();
		System.out.println("鍵の入力");
		Scanner truekeyscan = new Scanner(System.in);
		String truekey = truekeyscan.next();
		String truekeyword[] = truekey.split("",0);
		int truekeywordnum[]= new int[truekeyword.length];

		for(int i=0; i<truekeyword.length;i++) {
			for(int j=0; j<table.length; j++) {
				if(truekeyword[i].equals(table[j])) {
					truekeywordnum[i] = j;
				}
			}
		}
```

鍵を使って複合するには、`input[]`の長さだけ繰り返すfor文を書き、そのfor文の外に`int j`を初期値0で定義します。
そして、for文の中で先ほどクリブアタックの表を作成するときと同様の式を、`keywordsnum[]`を`truekeywordnum[]`に置き換え、iとjをそれぞれ入れ替えることで複合が出来ます。
暗号文の長さだけ鍵を繰り返したいので、jをforループごとに加算し、jが`truekeyword[]`の長さ以上になったら0に初期化することにより、`truekeyword[]`を繰り返し使うことが出来ます。

```java:
		int j=0;
		for(int i=0; i<input.length; i++) {
			if(truekeywordnum[j]>inputtextnum[i]) {
				A = table.length-(truekeywordnum[j] - inputtextnum[i]);
			}else {
				A = inputtextnum[i] - truekeywordnum[j];
			}
			System.out.print(table[A]);
			j++;
			if(j>=truekeyword.length) {
				j=0;
			}
		}
```

#実行結果

```
暗号化された文字列を入力してください
QNROAGZRMAY
暗号文に入っている文字列の入力
STUDENT

  QNROAGZRMAY
S YVZWIOHZUIG
T XUYVHNGYTHF
U WTXUGMFXSGE
D NKOLXDWOJXV
E MJNKWCVNIWU
N DAEBNTMEZNL
T XUYVHNGYTHF

鍵の入力
INFO

IAMASTUDENT
```

#終わりに
この例では鍵が可読性のある単語だったので比較的簡単に鍵を見つけることが出来ましたが、鍵がランダムな文字列だった場合、とたんに解読が難しくなります。
また、この手法で解読できるのは、暗号文がヴィジュネル暗号を使っていることが分かっていて、平文に入っている文字列を推測出来る場合に限られます。
ヴィジュネル暗号を使うときは、可読性のある鍵を使わないようにしましょう。