20201122のJavaに関する記事は4件です。

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

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

ヴィジュネル暗号とは

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

ヴィジュネル方陣
image.png

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

image.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

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

手作業で方陣を作るのは結構手間がかかるので、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[]を作成します。

        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

終わりに

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Spring Boot】ポートが使用されてたときの対応

最近Spring Bootを使用して開発をしているのですが、基礎的な部分が抜け落ちているので、参考書を見ながら進めています。

そんな中で、アプリケーションを起動した際にエラーが起きたので、その時の対応をメモしておきます。

エラー内容

Web server failed to start. Port 8080 was already in use.

スクリーンショット 2020-11-21 11.46.45.png

対応方法

使用しているポート番号のPIDを検索

$ lsof -i:[ポート番号]

稼働しているポートを止める

$ kill [PIDの番号]

再度ポート番号の使用状況を確認

$ lsof -i:[ポート番号] 

何も表示されなければ、成功です。
この後は再度Springのアプリケーション起動してもらえれば大丈夫なはずです。

スクリーンショット 2020-11-21 11.50.53.png

終わりに

今回のエラーは使用していないdocker環境を8080ポートで起動していたのが原因でした。
開発の環境と試験的に動かしてる環境を整理したほうがよさそうです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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
Latitude.java
package 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.java
package 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");
    }
}

実行時エラーでミスを未然に防げますね〜
実行時.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

minecraft1.7.10左右を入れ替えたときに別のアイテムを出すクラフトレシピを登録したい

環境

minecraft 1.7.10

多分他のバージョンでも同じ。それこそ1.2.5とかでも

背景

例えばしたの2つの例を見てほしい。このように左右逆に並べた時に別々のアイテムを作るレシピを登録したいとする。

image.png image.png

何も考えずに
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-251

    public 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.java
package 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.addShapedRecipeNonReversedShapedRecipe.addShapedRecipeに置き換えるだけでよい。

image.png
image.png

ちゃんと左右を入れ替えたときに別のアイテムを出すことができた

謝辞

この記事は解析を担当した某Discord鯖のE氏を始めとする方々の成果を私が記事としてまとめたものです。サンプルコードが動くことは自力で再現していますがそれ以上のことは私は行っていません。解析に感謝します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む