- 投稿日:2020-11-22T23:38:24+09:00
Javaを使ってヴィジュネル暗号をクリブアタック
こんにちは。Qiitaで初投稿です。
ヴィジュネル暗号の解読手法の1つであるクリブアタックに関して、解説しているWebサイトが見当たらなかったので、備忘録も兼ねて記事を書きます。
言語はJavaを使用しています。ヴィジュネル暗号とは
まず、ヴィシュネル暗号とは、ヴィジュネル方陣と呼ばれるアルファベットの表を使って暗号化をする暗号化方式です。
暗号化する際は、縦が鍵、横が平文となります。
具体的にどのように暗号化すると言いますと、例えば平文が「IAMASTUDENT」、鍵が「INFO」であるならば、このような方陣を使います。
平文の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」を入れて...を繰り返し、
このような方陣を作ります。
こうして出来た方陣の中に、推測される文字列がある部分に鍵が出現します。
この方陣の中に、正解の鍵である「INFO」が出現していることが分かります。
今回は綺麗に最初の文字から出現しましたが、「FOINFOIN...」のようにずれて出現することもあります。
気合で探しましょう。手作業で方陣を作るのは結構手間がかかるので、Javaでコードを書いてみました。
decoding.javaimport 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[]
を作成します。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[]
に入れます。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[]
に入れます。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]
を引けば追加するべき文字の大きさが求められます。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[]
に入れます。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[]
を繰り返し使うことが出来ます。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終わりに
この例では鍵が可読性のある単語だったので比較的簡単に鍵を見つけることが出来ましたが、鍵がランダムな文字列だった場合、とたんに解読が難しくなります。
また、この手法で解読できるのは、暗号文がヴィジュネル暗号を使っていることが分かっていて、平文に入っている文字列を推測出来る場合に限られます。
ヴィジュネル暗号を使うときは、可読性のある鍵を使わないようにしましょう。
- 投稿日:2020-11-22T17:43:18+09:00
【Spring Boot】ポートが使用されてたときの対応
最近Spring Bootを使用して開発をしているのですが、基礎的な部分が抜け落ちているので、参考書を見ながら進めています。
そんな中で、アプリケーションを起動した際にエラーが起きたので、その時の対応をメモしておきます。
エラー内容
Web server failed to start. Port 8080 was already in use.対応方法
使用しているポート番号のPIDを検索
$ lsof -i:[ポート番号]稼働しているポートを止める
$ kill [PIDの番号]再度ポート番号の使用状況を確認
$ lsof -i:[ポート番号]何も表示されなければ、成功です。
この後は再度Springのアプリケーション起動してもらえれば大丈夫なはずです。終わりに
今回のエラーは使用していないdocker環境を8080ポートで起動していたのが原因でした。
開発の環境と試験的に動かしてる環境を整理したほうがよさそうです。
- 投稿日:2020-11-22T11:01:42+09:00
【Java】変更に強いコード追加編 - Validation 機能
もっと!変更に強いコードを目指す
- 前回の 【Java】変更に強いコード実践編 - 値オブジェクト2(Value Object) では、
Valueオブジェクトを使ってタイポがない、凡ミスがないコードにしようo(^^)o- ということで、建物の緯度経度を表すBuildeingクラスを作りました
- Valueオブジェクトで型を指定することで、Buildingに渡す引数の順番間違え(Latitude、Longitudeの順番)を防止できました?
Validation機能追加
- さらに堅牢なコードを目指し、LatitudeにLongitudeの値(139.761102f)を入れてしまうことを防ぎたい。。。
- 値オブジェクトの初期化時に間違った実値を渡してしまう事を抑止するのはValidation機能で行うことができます
new Latitude(139.761102f), new Longitude(35.663114f)
- Latitudeクラスを以下のように変更したら、範囲外の値を入力した時に実行時例外を出すことができるので、さらに堅牢なコードになりますね⭐️o(^o^)o
- 構成は以下のようになっています
Project Root └─src └─ main └─ java └─ Main └─ values └─ Building └─ Building2 └─ BuildingName └─ Longitude └─ Latitude └─ Main
- 他の全コードは前回の
【Java】変更に強いコード実践編 - 値オブジェクト2(Value Object)
参考にしてくださいLatitude.javapackage values; public class Latitude { private float value; public Latitude(float value) throws Exception { if(value < -90 || 90 < value ){ throw new Exception("Out of Range"); } this.value = value; } public float getValue() { return value; } }Main.javapackage values; public class Main { public static void main(String[] args) throws Exception { Building mitaka = new Building( new BuildingName ("三鷹の森"), //fつけないとリテラルとして認識されない new Latitude(35.6962303f), new Longitude(139.5704895f) ); /*緯度と経度の値を逆にしてしまった場合にValidationが効いて実行時エラー Exception in thread "main" java.lang.Exception: Out of Range */ Building totoro = new Building( new BuildingName ("トトロの森"), new Latitude(139.4219994f), new Longitude(35.7818004f) ); /*緯度と経度の引数順を間違えてしまった場合はコンパイルエラー Building totoro = new Building( new BuildingName ("トトロの森"), new Longitude(139.4219994f), new Latitude(35.7818004f) ); */ /*コンパイルエラーにならない例 Building2 totoro2 = new Building2( "トトロの森", 139.4219994f, 35.7818004f ); */ System.out.println("pause"); } }
- 投稿日:2020-11-22T02:08:17+09:00
minecraft1.7.10左右を入れ替えたときに別のアイテムを出すクラフトレシピを登録したい
環境
minecraft 1.7.10
多分他のバージョンでも同じ。
それこそ1.2.5とかでも背景
例えばしたの2つの例を見てほしい。このように左右逆に並べた時に別々のアイテムを作るレシピを登録したいとする。
何も考えずに
1.7のレシピ追加 - Minecraft Modding Wiki
を真似してpackage com.example.examplemod; import net.minecraft.init.Blocks; import net.minecraft.item.ItemStack; import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.Mod.EventHandler; import cpw.mods.fml.common.event.FMLInitializationEvent; import cpw.mods.fml.common.registry.GameRegistry; @Mod(modid = ExampleMod.MODID, version = ExampleMod.VERSION) public class ExampleMod { public static final String MODID = "examplemod"; public static final String VERSION = "1.0"; @EventHandler public void init(FMLInitializationEvent event) { GameRegistry.addShapedRecipe(new ItemStack(Blocks.stone), "WA", 'A',Blocks.dirt, 'W',Blocks.grass); GameRegistry.addShapedRecipe(new ItemStack(Blocks.cobblestone), "AW", 'A',Blocks.dirt, 'W',Blocks.grass); } }のようにすると思う。このとき、予想に反して両方とも焼石が生成されてしまう。
原因
左右反転レシピが自動的に登録される仕様がMinecraftにはある。このよけいなおせっかい機能が原因である。
そもそも
GameRegistry.addShapedRecipe
がどう実装されているかというと
https://gitlab.zveronline.ru/Mirrors/Minecraft/MinecraftForge/-/blob/1.7.10/fml/src/main/java/cpw/mods/fml/common/registry/GameRegistry.java#L248-251public static IRecipe addShapedRecipe(ItemStack output, Object... params) { return CraftingManager.getInstance().addRecipe(output, params); }
net.minecraft.item.crafting.CraftingManager
に丸投げされている。これをデコンパイルして追跡した先人たち曰く、内部でいろいろ処理された後、
net.minecraft.item.crafting.ShapedRecipes
のインスタンスが作られるのだそうだ。この中にmatches
というメソッドがある。でコンパイルした結果を読むと、matches
が呼び出しているcheckMatch
というprivate methodの第4引数にtrueが渡るときとfalseが渡るときがあり、先にtrueのときが処理されている。反転が先に判定される状態だ。これを書き換えることでなんとかできそうだ。そこで
ShapedRecipes
をoverrideしてmatches
を書き換えることにする。ただ、checkMatch
がprivate methodで呼び出せないので仕方ないからデコンパイルしたものを持ってきた。解決方法
デコンパイルしたコードを結構引っ張ってきたようなコードとなる
NonReversedShapedRecipe.javapackage com.example.examplemod; import java.util.HashMap; import net.minecraft.block.Block; import net.minecraft.inventory.InventoryCrafting; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.crafting.ShapedRecipes; import net.minecraft.world.World; import cpw.mods.fml.common.registry.GameRegistry; public class NonReversedShapedRecipe { public static ShapedRecipes addShapedRecipe(ItemStack output, Object... params) { String s = ""; int i = 0; int j = 0; int k = 0; if (params[i] instanceof String[]) { String[] astring = (String[]) ((String[]) params[i++]); for (int l = 0; l < astring.length; ++l) { String s1 = astring[l]; ++k; j = s1.length(); s = s + s1; } } else { while (params[i] instanceof String) { String s2 = (String) params[i++]; ++k; j = s2.length(); s = s + s2; } } HashMap<Character, ItemStack> hashmap; for (hashmap = new HashMap<>(); i < params.length; i += 2) { Character character = (Character) params[i]; ItemStack itemstack1 = null; if (params[i + 1] instanceof Item) { itemstack1 = new ItemStack((Item) params[i + 1]); } else if (params[i + 1] instanceof Block) { itemstack1 = new ItemStack((Block) params[i + 1], 1, 32767); } else if (params[i + 1] instanceof ItemStack) { itemstack1 = (ItemStack) params[i + 1]; } hashmap.put(character, itemstack1); } ItemStack[] aitemstack = new ItemStack[j * k]; for (int i1 = 0; i1 < j * k; ++i1) { char c0 = s.charAt(i1); if (hashmap.containsKey(Character.valueOf(c0))) { aitemstack[i1] = ((ItemStack) hashmap.get(Character.valueOf(c0))).copy(); } else { aitemstack[i1] = null; } } ShapedRecipes shapedrecipes = new ShapedRecipes(j, k, aitemstack, output) { @Override public boolean matches(InventoryCrafting inv, World worldIn) { for (int i = 0; i <= 3 - this.recipeWidth; ++i) { for (int j = 0; j <= 3 - this.recipeHeight; ++j) { // When you uncomment this, you can get vanilla's behavior. // if (this.checkMatch(inv, i, j, true)) { // return true; // } if (this.checkMatch(inv, i, j, false)) { return true; } } } return false; } private boolean checkMatch(InventoryCrafting inv, int i, int j, boolean p_77573_4_) { for (int k = 0; k < 3; ++k) { for (int l = 0; l < 3; ++l) { int i1 = k - i; int j1 = l - j; ItemStack itemstack = null; if (i1 >= 0 && j1 >= 0 && i1 < this.recipeWidth && j1 < this.recipeHeight) { if (p_77573_4_) { itemstack = this.recipeItems[this.recipeWidth - i1 - 1 + j1 * this.recipeWidth]; } else { itemstack = this.recipeItems[i1 + j1 * this.recipeWidth]; } } ItemStack itemstack1 = inv.getStackInRowAndColumn(k, l); if (itemstack1 != null || itemstack != null) { if (itemstack1 == null && itemstack != null || itemstack1 != null && itemstack == null) { return false; } if (itemstack.getItem() != itemstack1.getItem()) { return false; } if (itemstack.getItemDamage() != 32767 && itemstack.getItemDamage() != itemstack1.getItemDamage()) { return false; } } } } return true; } }; GameRegistry.addRecipe(shapedrecipes); return shapedrecipes; } }なんとこの記事を書いている今日、はじめてまともにJavaに触ったのだが、
ShapedRecipes shapedrecipes = new ShapedRecipes(j, k, aitemstack, output) {
以下の部分は匿名(無名)クラスという機能らしい。ここでは無名のShapedRecipesを継承したクラス作ってることになる。あとは
GameRegistry.addShapedRecipe
をNonReversedShapedRecipe.addShapedRecipe
に置き換えるだけでよい。ちゃんと左右を入れ替えたときに別のアイテムを出すことができた
謝辞
この記事は解析を担当した某Discord鯖のE氏を始めとする方々の成果を私が記事としてまとめたものです。サンプルコードが動くことは自力で再現していますがそれ以上のことは私は行っていません。解析に感謝します。