20190501のJavaに関する記事は16件です。

数値(int)⇔文字列(String)変換

数値 ⇒ 文字列

String s = String.valueOf(i);

文字列 ⇒ 数値

int i = Integer.parseInt(s);

よく使い、よく忘れるのでメモ。

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

いろはちゃんコンテスト Day2 - A-Cまで

いろはちゃんコンテスト Day2

とりあえず、解けたところまで。。。
終了後は、解けた問題の考察があっていたかの確認
解けなかった問題をeditorialを読んで考察。。。しても理解不能。。。考察力を磨くとは(ry

もう少しレベルアップしたら残りの問題をやりに戻ってくる予定

A問題

    private void solveA() {
        String s1 = next();
        String s2 = next();
        if (s1.isEmpty() || s2.isEmpty()) {
            out.println(0);
            return;
        }
        int l1 = s1.length();
        int l2 = s2.length();

        int[][] dp = new int[l1 + 1][l2 + 1];

        for (int i = 1; i <= s1.length(); i++) {
            for (int j = 1; j <= s2.length(); j++) {
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Integer.max(dp[i][j - 1], dp[i - 1][j]);
                }
            }
        }

        out.println(dp[l1][l2] + 1);

    }

B問題

 - ベクトルの外積って、記憶にないわーー。ということでこの問題は解けませんでした。。。
- 各種場合分けに失敗したので解くのあきらめてCに移りました。

    /**
     * ベクトルの外積
     * 外積の向き
     */
    private void solveB() {
        int x = nextInt();
        int y = nextInt();
        int a = nextInt();
        int b = nextInt();

        int[] s = IntStream.range(0, 2).map(i -> nextInt()).toArray();
        int[] t = IntStream.range(0, 2).map(i -> nextInt()).toArray();

        boolean vect1 = s[0] * (b - a) - (s[1] - a) * x > 0;
        boolean vect2 = t[0] * (b - a) - (t[1] - a) * x > 0;

        out.println(vect1 == vect2 ? "No" : "Yes");
    }

C問題

  • 座標圧縮だよねこれ
    private void solveC() {
        int numN = nextInt();
        int[] wk = new int[numN];
        Set<Integer> wkL = new TreeSet<Integer>();
        for (int i = 0; i < wk.length; i++) {
            wk[i] = nextInt();
            wkL.add(wk[i]);
        }

        List<Integer> tmp = new ArrayList<Integer>();
        tmp.addAll(wkL);
        Collections.sort(tmp);

        for (int i = 0; i < wk.length; i++) {
            int position = Collections.binarySearch(tmp, wk[i]);
            position = position >= 0 ? position : ~position;
            out.println(position + 1);
        }
    }

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

いろはちゃんコンテスト Day1 - A-Fまで

いろはちゃんコンテスト Day1

とりあえず、解けたところまで。。。
コンテスト中に解けない問題はeditorialみても解けませんっすわ。。。

A問題

  • 最初の1文字を出力する
    private void solveA() {
        String s = next();

        out.println(s.charAt(0));
    }

B問題

  • 「先頭の文字を末尾に移動する」という操作を$K回$行った結果
  • この手の問題、きちんとローテすると絶対時間足りないんで
    • ローテしたかの様に、後ろに文字を足していく
    • 生成できた文字列のk番目から元の文字の長さを切り出す

入力例2:
 JKrolling
 99

生成後文字列(ここの99番目から108番目までを切り出して出力)
JKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrolling

    private void solveB() {
        String s = next();
        int k = nextInt();

        List<String> wk = new ArrayList<String>();

        for (int i = 0; i < s.length(); i++) {
            wk.add(s.substring(i, i + 1));
        }
        for (int i = 0; i < k; i++) {
            wk.add(wk.get(i));
            //          wk.remove(0);
        }
        StringBuilder builder = new StringBuilder();
        for (int i = k; i < wk.size(); i++) {
            builder.append(wk.get(i));
        }
        out.println(builder.toString());
    }

C問題

  • 7日前から当日までというのに注意
    private void solveC() {
        int numN = nextInt();

        int start = numN - 7;
        for (int i = start; i <= numN; i++) {
            out.println(i);
        }

    }

D問題

  • 最善の手段なので
    • 入力値をソートして
      • 後ろから、高橋 -> 青木 の順で取っていく
    private void solveD() {
        int n = nextInt();
        int x = nextInt();
        int y = nextInt();
        int[] wk = IntStream.range(0, n).map(i -> nextInt()).toArray();

        Arrays.sort(wk);

        int tak = 0;
        int aok = 0;
        for (int i = 0; i < wk.length; i++) {
            if ((i & 1) == 1) {
                aok += wk[wk.length - 1 - i];
            } else {
                tak += wk[wk.length - 1 - i];
            }
        }
        tak += x;
        aok += y;
        if (aok > tak) {
            out.println("Aoki");
        } else if (aok < tak) {
            out.println("Takahashi");
        } else {
            out.println("Draw");
        }

    }

E問題

  • TLEになったので書き直したversion

  • 1日もデートに行かない場合は$精進可能日-精進可能日/休暇$で精進日がわかる

  • 記念日が0日、1日は分岐

  • 記念日2日以上のループの最終日は分岐

    • 通常の処理に加え、最後の記念から最終日までの処理を追加
    private void solveE() {
        long n = nextLong();
        long a = nextLong();
        long b = nextLong();
        List<Long> day = new ArrayList<Long>();
        for (int i = 0; i < b; i++) {
            day.add(nextLong());
        }

        long res = 0;

        /*
         * 一日も記念日がない
         */
        if (day.size() == 0) {
            long d = n - 1;
            long val = d - d / a;
            res += val;
            out.println(res);
            return;
        }
        /*
         * 1日だけ記念日
         */
        if (day.size() == 1) {
            long d = 0;
            long val = 0;
            d = day.get(0) - 1;
            val = d - d / a;
            res += val;

            d = n - day.get(0);
            val = d - d / a;
            res += val;
            out.println(res);
            return;
        }

        Collections.sort(day);
        long d = 0;
        for (int wkCnt = 0; wkCnt <= n; wkCnt++) {

            if (wkCnt == day.size() - 1) {
                /*
                 * 最終の時は、
                 * ・休日の処理
                 * ・最後の日(n)から最後の休日までの処理
                 * の二つを実施
                 */
                d = day.get(wkCnt) - day.get(wkCnt - 1) - 1;
                long val = d - d / a;
                res += val;

                d = n - day.get(wkCnt);
                val = d - d / a;
                res += val;
                break;
            } else if (wkCnt == 0) {
                /*
                 * 最初は
                 */
                d = (day.get(wkCnt) - 1);
            } else if (wkCnt < day.size() - 1) {
                /*
                 * 休日の真ん中のみ取得するため、
                 * 次の休日-今回の休日 のあと、追加で-1
                 */
                d = day.get(wkCnt) - day.get(wkCnt - 1) - 1;
            }
            /*
             * d/aがa周期の休日
             */
            long val = d - d / a;
            res += val;
        }
        out.println(res);

    }

E問題:TLE

愚直に実装したけど、まぁTLE

  • デート日をsetに詰めておく
  • 毎日以下の判定を行う
    • 記念日かどうか
      • 記念日だった場合、連続日数のカウントをリセット
    • デート日かどうか
      • デート日の場合、連続日数のカウントをリセット
    • 精進したかどうか
      • 精進したらres++
    /**
     * TLE
     */
    private void solveE2() {
        long n = nextLong();
        long a = nextLong();
        long b = nextLong();
        Set<Long> day = new HashSet<Long>();
        for (int i = 0; i < b; i++) {
            day.add(nextLong());
        }

        //      StringBuilder builder = new StringBuilder();
        long dayCnt = 0;
        long res = 0;
        //      List<Long> add = new ArrayList<Long>();
        long cnt = 1;
        while (cnt <= n) {
            dayCnt++;
            if (dayCnt == a) {
                dayCnt = 0;
                //              builder.append("D");
            } else if (day.contains(cnt)) {
                dayCnt = 0;
                //              builder.append("D");
            } else {
                res++;
                //              add.add(i);
                //              builder.append("P");
            }
            cnt++;
        }

        out.println(res);
    }

F問題

  • 素因数分解
    • kと同じならソートしてそのまま出力
    • kより小さいなら-1
    • kより大きいなら、k番目以降の値を全て乗算して1つにまとめる
    private void solveF() {
        long n = nextLong();
        long k = nextLong();

        List<Integer> wk = new ArrayList<Integer>();
        for (int i = 2; i <= n; i++) {
            while (n % i == 0) {
                wk.add(i);
                n /= i;
            }
        }
        StringBuilder builder = new StringBuilder();
        if (wk.size() == k) {
            for (Integer integer : wk) {
                builder.append(integer + " ");
            }
        } else if (wk.size() < k) {
            builder.append(-1);
        } else {
            int cnt = 1;
            for (int i = 0; i < wk.size(); i++) {
                if (i < k - 1) {
                    builder.append(wk.get(i) + " ");
                } else {
                    cnt *= wk.get(i);
                }
            }
            builder.append(cnt);
        }
        out.println(builder.toString());
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】JavaGoldが解説するPythonの基礎 パート1

概要

今回の3人のプロジェクトでPythonを使うため1から勉強したので
今回学んだ内容をJavaとの違いを含めて皆様にシェアできればと思います!!
このパート1ではJavaとの違い&概要を、パート2で基本的な構文(if文&例外処理等)を説明していきます。

ちなみに仕事ではJavaしか使う機会がなく、1年前の3年目の夏にJava SE8 Goldを取得しました。
(本日から令和が始まりましたね!!)
_106250427_japanera-2.jpg

アジェンダ

・Javaとは
・Pythonとは
・JavaとPythonの違い
・データ型
  1. 数値型
  2. 文字列型
  3. リスト型
  4. 辞書型
  5. タプル型
  6. 集合型(セット型)
  7. None
・最後に
・参考文献

Javaとは

Java仮想マシン(JVM)さえ動作すればJavaはどんなコンピュータ上でも利用することが出来るオブジェクト指向言語。コンパイル言語。
OSを入れ替えてもプログラムはそのまま使用可能。Write once, run anywhere(一度書いたらどこでも動く)。
Javaは、携帯電話やスマートフォンなどで使われる小規模なアプリケーションから、銀行で使われるような大規模な業務システムを開発する際にも用いられる。

javaで開発できるもの

①業務システム ex.運送会社の配送システム, 金融の取引システム
②Androidアプリ
③WEBアプリケーション ex. Twitter
④ゲーム ex. Minecraft
⑤その他 ex.家電製品や従来の携帯電話、Blu-rayプレーヤーなどに搭載されているソフトウェアなど

Tips

Google三大言語(「Java」「C++」「Python」)
Javaと同じような言語形式を持っているのは、C、C♯、C++などのC系と呼ばれるプログラミング言語。
そもそもJavaは、C++のバグを生みやすい仕様を削除し代わりにガーベジコレクションなどの新しい機能を取り入れた言語として設計されている。

Pythonとは

シンプルなコード、豊富なライブラリ、汎用性の高さなどが特徴のオブジェクト指向言語。スクリプト言語。
何か決まったものを作るための特化した言語ではなく、Web、ゲーム、データ解析、GUIアプリなど、なんでも作ることができる非常に汎用性の高い言語。
最近では、ビッグデータ処理、統計、機械学習、AIなどの分野に適したライブラリが充実しており、利用されることが多くなってきている。

Pythonで開発できるもの

①WEBアプリケーション ex. Dropbox, Instagram, Youtube, Evernote
②デスクトップアプリ
③業務効率化 VBAの様に単純作業を自動化させるツール作成が可能
④組み込みアプリ
⑤機械学習・統計解析系アプリ
⑥ゲーム

Tips

組み込みアプリの分野では、機械が理解できる形により近く、動作が高速なC言語やC++がよく使われるが、PythonはC言語やC++と親和性が高く、PythonでC言語等の処理を呼び出すことが可能。
Pythonのソースコードは、他のプログラミング言語よりもシンプルに書ける。
 →書かなくてはならないソースコードの量が少ないこと、書き方が限られているため。文法がシンプルで必要最低限のものしか用意されていない。(*オフサイドルールなど)

*オフサイドルール(Off-side Rule)
ブロックが{}ではなく、インデントで指定する。インデントはコードの見やすさのためではなく文法として意味を持っている。

JavaとPythonの違い

Javaではデータ型というものを最初に固定で決める形式(静的型付)を取る。
 →プログラムを実行する前にコンピュータが実行できる形式に翻訳する(コンパイル)。
一方、*Pythonではデータ型がプログラムが実行するときに決まる(動的型付)という形式をとる。
 →コンパイルを必要とせずにプログラムを実行できる。
 →スクリプト起動時に、Pythonインタープリタがスクリプトを中間コード(仮想マシン語)にコンパイルしてから実行する。

*詳細は、shiracamusさんをコメントをご参照下さい。

またPythonでは、

・for文などで使うブロックが{}ではなく、インデントで指定する。
・変数を宣言する際に何らかのキーワードを指定する必要はない。(JavaのStringなどの型やJavascriptのvarなど)
・定数という概念は存在しない。
・『else if』が「elif」
・switch~case文がない。その代わり「in」キーワードで同様の実装が可能。*詳細はパート2で
・for文は、Javaのforeach文に相当。range関数と組み合わせて使うことが多い。また、do-while文は存在しない。*詳細はパート2で
・例外処理の構文が、「try~except~else~finally」*詳細はパート2で
・何もしないことを明示的に表すpass文がある。*詳細はパート2で

データ型

Pythonでは、大きく分けて7種類の型がある。
1. 数値型
2. 文字列型
3. リスト型
4. 辞書型
5. タプル型
6. 集合型(セット型)
7. None

1.数値型

①整数型 
→Javaなどの他の言語と変わりなし。
→Javaなど他の言語は固定長整数。
Pythonの整数型は可変長整数で、文字列と同様に可変長データなので、値の上限がないのが特徴。大きな値ほど、メモリを多く消費する。

②浮動小数点型
除算(/)の場合は、整数と整数の演算でも結果が浮動小数点(Float)になる。切り捨て除算(//)だと結果は整数(Integer)に。

③複素数型
→複素数が利用できる。(バージョン3.4以降)。数値に添字(j)を付けると複素数リテラルとして扱うことができる。Javaではない。

④真偽値
→Falseは『0』、Trueは「1」として定義される。そのため、これらの値を数値と直接演算することも可能。ちなみに先頭を大文字にしないと真偽値として認識されない。

2.文字列型

文字列は4種類の記述ができる。3重のクォートを使うと複数行にまたがる文字列(ヒアドキュメント)を定義することが可能。

 ①値をシングルクォートで囲う
 ②値をダブルクォートで囲う
 ③値を3重のシングルクォートで囲う
 ④値を3重のダブルクォートで囲う

3.リスト型

Javaで言う配列。内容の書き換えが可能で、かつシーケンシャルに扱うことができる。含める値の型は一致している必要はない。
データはブラケット[ ] の中に値をカンマで区切って宣言する。引数には負数を指定することができ末尾から表示できる。

lst=['test', 10, False]
print(lst[1])  #10
lst[1]=1000
print(lst[1])  #1000
lst[-1]=False

4.辞書型

Javaで言うMap。内容の書き換えが可能で、キーと値のセットでデータを管理。
データは波括弧 { } の中にキーと値をセットにしたデータをカンマで区切って宣言する。
キーに対する値の型は辞書内で一致しなくても良い。

directory={'key1': 'value1', 'key2': 'value2'}
print(directory['key1'])  #value1

5.タプル型

Javaで言うfinal宣言した配列。リスト型の様にシーケンシャルを持つが要素の変更はできない。
データはカッコ ( )の中に値をカンマで区切って宣言する。含める値の型は一致していなくて良い。
タプルに含める値が一つだけの場合は末尾にカンマを付与する必要がある。
辞書のキーとして使用することも可能。

tuple1=('test', 10, True)
print(tuple[0]) #test
tuple2=('test', ) #タプルに含める値が一つ

6.集合型

Javaで言うSet。シーケンシャルではなく重複した値を持たない。そのため各々の出力結果は常に例の通りになるとは限らない。
データは波括弧 { } の中に値をカンマで区切って宣言するか、setという関数に値を渡して生成する。
「set」関数は引数としてリスト型の値か文字列を受け取り、文字列を受け取った場合は一文字ずつバラして管理。

sets1={'test',999,True}
print(sets1)# {True, ‘test’, 999}#
print(sets1[0])# 順番を持たないためこれはNG

sets3=set('hogehoge')
print(sets3) # {'e', 'h', 'o', 'g'} #重複は排除される

7.None

Null値を表す。

最後に

さすがシンプルなコードと謳っているだけあって、Javaのクラス宣言など書かずに済み、ソース量が少ないですね♪Java経験があると理解しやすそう!
if文や例外処理などの基本的な構文はパート2で説明します。
近日、公開します^^

参考文献

・【2017年】JavaとPython徹底比較を専門用語ゼロで解説
https://www.sejuku.net/blog/36782

・JavaプログラマがPythonを勉強してみた。(型について)
https://qiita.com/riekure/items/57f306500636727bc125

・PythonとJavaのクラス、インスタンス、スコープの違いを比較
http://kkv.hatenablog.com/entry/2015/04/12/164817

・The 2018 Top Programming Languages - IEEE Spectrum Rankin
https://spectrum.ieee.org/at-work/innovation/the-2018-top-programming-languages

・現役エンジニアがよく使う!Python機械学習ライブラリ厳選9選
https://www.sejuku.net/blog/11551

・2018年大注目のPython!WEBフレームワーク3つを徹底比較
https://www.sejuku.net/blog/3713

・AmadaShirou. Programing Keikensya No Tameno Python Saisoku Nyumon (Japanese Edition) Kindle 版

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

WebSphere LibertyでHTTPSを有効化する

やりたいこと

自作のWebアプリケーションをGoogleのLighthouseにかけてみようと思ったらHTTPS化することが必須のようだったので、LibertyでHTTPSを使う方法を試してみた。

SANが必要

自己署名証明書を利用してHTTPS通信をすれば良いので難しくないと思ったのだが、昔はCommon Name(CN)が設定されていればOKだったが、Chrome 58以降は証明書にSubject Alternative Name(SAN)が登録されていることを要求するため、SANを含む証明書を作る必要がある。
スクリーンショット 2019-05-01 17.56.12.png

試した環境

  • AdoptOpenJDK HotSpot 11.0.3
  • WebSphere Liberty 19.0.0.4
  • macOS Mojave
  • Chrome 74

デフォルトで生成される証明書はどうなってる?

ssl-1.0フィーチャーを有効化したWebSphere Libertyを起動するとuser/servers/(server-name)/resources/securitykey.p12というファイルが生成される。このファイルはPKCS12という形式のファイルで中に秘密鍵と公開鍵証明書が保存されている。ファイルにはパスワードがかかっていてserver.xml<keyStore>要素でパスワードを指定する。

内容の確認方法は以下の通り(パスワードは<keyStore>で指定したもの)。default という別名(alias)で証明書が登録されていることがわかる。

keytool -v -list -keystore key.p12

SAN付き証明書の作成手順

秘密鍵を入手してそこからSAN付きの証明書を作っていく。秘密鍵はデフォルトで生成されるp12ファイルから取り出すこともできるようだが(後述)、ここではゼロから作ってみる。iTerm.appだとopensslコマンドの実行時にパスワードを求められた場合のキーボード入力がうまくいかないのでTerminal.appを使った。

# 秘密鍵を作成する
openssl genrsa 2048 > default.key

# Certificate Signing Request(CSR, 証明書署名要求)を作成する
# Country Nameなどを聞かれる。最低限Common Name(CN)はlocalhostとする必要がある
openssl req -new -key default.key > default.csr

# SANがlocalhostとなるように入力ファイルを作成する
echo subjectAltName=DNS:localhost > default-san.ext

# 証明書を作成する
openssl x509 -days 3650 -sha256 -req -signkey default.key < default.csr > default.crt -extfile default-san.ext

# PKCS12形式の鍵ストアを作成する
openssl pkcs12 -export -in default.crt -inkey default.key -out default.p12 -name default

# 作成したp12ファイル内の証明書にSubjectAlternativeNameが記載されていることを確認する
keytool -v -list -keystore key.p12

Macのキーチェーンに証明書を登録する

キーチェーンアクセス.appを起動して、左ペインから「ログイン」、「証明書」を開く。右のペインに証明書の一覧が表示されるのでここに上記の手順で作成したdefault.crtをドラッグ&ドロップで追加する。追加したらSSLの部分を「常に信頼する」に変更する。

スクリーンショット 2019-05-01 17.58.00.png

Chromeでアクセスし直すと、自己署名証明書利用時の警告メッセージなくアクセスできる。
スクリーンショット 2019-05-01 19.36.31.png

参考

p12ファイルからの秘密鍵、証明書の取り出し方

以下のコマンドで取り出せる。前述の通りiTerm.appだとパスワード入力がうまくいかないのでTerminal.appを使った。

# 秘密鍵を取り出す
openssl pkcs12 -in key.p12 -nocerts -nodes -out privatekey
# 証明書を取り出す
openssl pkcs12 -in key.p12 -clcerts -nokeys -out default2.crt

p12かjksか

昔はp12ではなくjks形式だった気がすると思ってOpenLibertyのIssueをのぞいてみたところ19.0.0.3から変わったのかもしれない。一般的にp12が推奨されているっぽい。
- https://github.com/OpenLiberty/open-liberty/issues/7041

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

JAVAについて知っているとよいことかどうかわからない11項目

JAVAについての相談は、1995年頃から約24年間、定常的にいただいている。
2001年頃までは、JAVAのプログラミング研修を6年ほど実施していた。

当時の経験からすれば、現状のよくわからない情報は、当初のよくわからない情報と同じで、たいした違いは感じていない。

ポインタ不要

JAVAの一番の画期的なところは、C言語の記述で一番難しいポインタ処理を排除したことである。
この点以外に、C++に対する優位点は、その後の様々な改良の成果だと理解している。
当初設計でC/C++に対する優位性は、C言語のように書けるが、ポインタ処理をしなくても必要な機能を実現できることだ。

文字コード対応

JAVAは当初から国際対応していて文字コード対応が万全だと言う方がいた。

日本語の表現は、1バイト2バイト混在のデータを文字コードを切り替えて表現する。
画面で左から右に表示している時に、右端から逆に辿ると、現在位置の文字コードはわからない。

この状態を回避するには、いくつか方法がある。
JAVAのシステムで文字コード対応がそれなりに安定したのは1−2年経過してからのような気がする。
これは、気がするだけである。
実際にどうだったかは、過去の資料を紐解いてみる。

画面周りの変更

描画用の仕組みは、当初からすこしづつ改定されている。

1.1 におけるAWT 、1.2におけるSwing など、目まぐるしく変化した。
画面は、さまざまな道具類の設計の変更により、どのような構造が適しているか変化していくものだという理解をしている。

Visual J++

MicrosoftがVisual J++を発表した時には、すぐにセミナを開催した。
Visual J++のよいところは、GUIで記述したソースコードを生成するところだったと思う。
逆に、使うのをやめることにしたのは、生成したコードが読みにくいことだった。

ちょうど、C++がで始めた頃、C++のソースコードをCのコードに変換するプリプロセッサ(C Pre ProcessorでCPPという)を購入したことがある。使うのをやめたのは、マイクロソフト、ボーランンドから高速なC++が発売されたことと、生成されたCソースコードが読みにくいことだった。

国際規格投票でアメリカが反対

JAVAを国際規格になれば、write once move anywaereが実現できるはずだった。

アメリカがSUNのライセンスに対する不透明さから国際規格に反対投票し、結果としてJAVAの国際規格が否決された。
当時、別の国際規格のeditorをしていた。
ニュースを聞いて小踊りした。

本当にWrite Once move anywareなら、国際規格に誰も反対しない。
どこでも動くような状況を作るために、JAVAの技術者は、いろいろなCPUにVMを作られ、すごく努力された。
SUNが公開でこれらの努力にもっと経緯を払っていれば、アメリカが反対したとしても、圧倒的多数で国際規格になったはずである。

JAVAの閉鎖性は当初から、一貫して見え隠れしている。

DBミドルウェア

JAVAプログラミング教育をしていて、
JAVAが一定の規模で利用されるようになり、一番大きな分野がネットワークを介したDB利用であった。

しかし、DBのミドルウェアは高価なものが次々に出てきて、
当方が教育から撤退したのは、有償のミドルウェアが購入できないため。

DBまわりの発展が、一時的とはいえ、高価な道具類を使わないと仕事にならなかったのは残念だった。

これ以降、JAVAの教育から撤退している。
それまでとは、JAVAに関して積極的に情報取得、情報発信は半分以下になった。

Vertical Machineの淘汰

いろいろなVertical Machineが増えた。
小ささ、高速さをうたうものもあった。

Vertial Machineの興隆を計ってこそJAVAの発展が望めたはずである。

MacintoshでJAVAのVMを同梱しなくなった時点で、JAVAの理想が終わったと思った。

C#の登場

C#は、Windows処理ではJAVAよりも便利な記述ができるところを売りにしていたのだろうか。

標準化も、ECMA, JISと順調に進んでいった。

Windows以外のOSへの展開に時間がかかっていて、当方が積極的に参入することができなかった。

CLIであるmonoの導入など、進捗を知るためにも何度か挑戦をしようとした。

HAVAからCOBOLへの移行

COBOLからJAVAへの移行案件は、何度か小耳にはさんだ。
半分くらいはJAVAに移行したらしい。残りはまたCOBOLに戻ったと聞いている。

そこで、JAVAで書いたプログラムをCOBOLに移行する方法を検討する。

COBOLはもともとオブジェクト指向言語であり、英語そのままでプログラムが動く。

Open JDK

http://openjdk.java.net

Oracle JDK 8 の公開アップデート終了

https://www.oracle.com/technetwork/jp/java/javase/overview/index.html

オラクルは2019年1月を最後に、公開されたダウンロードサイトにおける商用利用向けのJava SE 8アップデートリリースを終了します。Java SE 8やそれ以前のバージョンに対する、バグ修正やセキュリティ修正などメンテナンスされたアップデートリリースが引き続き今後も必要な場合、Oracle Java SE Subscription または Oracle Java SE Desktop Subscriptionで提供される長期サポートで入手・利用が可能となります。Oracle JDK 8 の長期サポートに関するより詳しい情報は、Java SE サポート・ロードマップをご確認ください。

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

Windows Command Line で Java Module Programing

Command Line で Java Module Programing

目的

  • JDK9以降、Module Progaramingが求められる。
  • JDK11以降、JavaFXがJDKから分離され、Comannd Lineで書かないといけないpathがやたら複雑に。前はjavacとjavaで簡単だったが。
  • Module Programing の開発開始から、最終package化まで、必要なすべてのComannd Lineを開発環境(module, package, Class)に合わせて事前に出力し、copy&pasteで開発を進める。

前提となるJDKとJFXの配置

(現在の最新状態)

//openjdkと言うディレクトリにJDKとJFXを集める。
c:\Program Files\openjdk 
          ├─javafx-jmods-12
          ├─javafx-sdk-12
          └─jdk-12.0.1

前提となる開発環境ディレクトリ構造

(root)
├─mods // Classファイルが入ります
│ ├─(module1)
│ │ └─(package1)
│ ├─(module2)
│ │ └─(package2)


├─out //アプリのJREが作られます
│ ├─(Class名1)
│ │ ├─bin
│ │ ├─conf
│ │ ├─include
│ │ ├─legal
│ │ └─lib
│ ├─(Class名2)
│ │ ├─bin
│ │ ├─conf
│ │ ├─include
│ │ ├─legal
│ │ └─lib

├─src // ソースコード(.java)を入れます
│ ├─(module1)
│ │ └─(package1)
│ ├─(module2)
│ │ └─(package2)

└─txt // javacの時に使うソース一覧の入ったtxtファイル

開発環境を整えるbatファイル

置き場所: c:\Users(someone) //someoneはユーザー名
 (cmd.exeを立ち上げた時にこのディレクトリ基準なので)

javaopen.bat
cd (開発root 絶対path)
set path=%PATH%;C:\Program Files\openjdk\jdk-12.0.1\bin
set jfxpath="C:\Program Files\openjdk\javafx-sdk-12\lib"
set jfxmod="C:\Program Files\openjdk\javafx-jmods-12"
set jdkpath="C:\Program Files\openjdk\jdk-12\jmods"
set CLASSPATH=.;(開発root 絶対path)

開発root 絶対path は、上に書いた開発環境ディレクトリ構造の(root)の絶対pathになります。

[説明]
1.開発rootを起点にします。
2.JDKのbinにあるコマンドを使えるように。
3.JFXのsdkのlibのpath略号をjfxpathと命名
4.JFXのjmodsのpath略号をjfxmodと命名(jlinkで使う)
5.JDKのjmodsのpath略号をjdkpathと命名(jlinkで使う)
6.CLASSPATHを設定

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

Command Line で Java Module Programing

Command Line で Java Module Programing

目的

  • JDK9以降、Module Progaramingが求められる。
  • JDK11以降、JavaFXがJDKから分離され、Comannd Lineで書かないといけないpathがやたら複雑に。前はjavacとjavaで簡単だったが。
  • Module Programing の開発開始から、最終package化まで、必要なすべてのComannd Lineを開発環境(module, package, Class)に合わせて事前に出力し、copy&pasteで開発を進める。

前提となるディレクトリ構造

(root)
├─mods // Classファイルが入ります
│ ├─(module1)
│ │ └─(package1)
│ ├─(module2)
│ │ └─(package2)


├─out //アプリのJREが作られます
│ ├─(Class名1)
│ │ ├─bin
│ │ ├─conf
│ │ ├─include
│ │ ├─legal
│ │ └─lib
│ ├─(Class名2)
│ │ ├─bin
│ │ ├─conf
│ │ ├─include
│ │ ├─legal
│ │ └─lib

├─src // ソースコード(java)を入れます
│ ├─(module1)
│ │ └─(package1)
│ ├─(module2)
│ │ └─(package2)

└─txt // javacの時に使うソース一覧の入ったtxtファイル

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

Rubyを使っていたJava初心者がSpringの学習でつまづいたこと、解決の為にしたこと

Springを始めよう

初めまして。
私は今大学生で、来年から都内のIT企業で働くことになった学生です。
私はずっとRubyを使ってきたのですが、会社ではJavaを使うということでそのためのトレーニングをすることになりました。
そこで初めてSpringを使ったのですが、今までRubyとRailsを使って開発をしていたのでつまるところがあり、できればこれを共有したいと今回書くことにしたわけです。
レベル感でいうと、本当にJavaの基礎について、Webの基礎についての本を読んで少し手を動かした程度です。

何に詰まったか

何に詰まったかというと、MVCの形がRailsと違った為に、どのような形で実装すればいいかわからなくなったこと。
これはSpringをやってから気づいたんですが、RailsのMVCってかなり簡単に実装できるように作られているんだなぁと感じました。
まさにMVCそれぞれ、ModelとView、Controllerがそれぞれ三つだけに別れているので、何を書いていけばいいかが割と簡単にわかる。すごく楽ですね。
一方、SpringはMVCの実現がやや違う形になります。
Controller、Service、Entity、Repositoryに別れると。
Controllerの役割についてはRailsとそれほど変わらないのでわかりましたが、それ以外は何?どんなことを書いていけばいいんじゃ?となったわけです。

何を実装するか知る旅に出よう

ここから大いなる旅が始まりました。
どうやらRepositoryはDBとのやりとりをするとはわかったものの、Serviceも割と近くね?Entityって何?となっていきました。
とにかくここがわからないと、作ったとて、学びにならない!ととにかく色々調べます。
ここで難しかったのはそれぞれの役割が割と抽象的に表現されていたこと。
例えばServiceはビジネスロジックを実装するとか。いやわかりにくい。
そのビジネスロジックってそもそもなんなんですかということに。
そうこうしていくうちに、自分の知りたいことの乗っているサイトに出会いました。
Spring bootでweb 基本編
Slide Shareのもので、とにかくわかりやすいです。
ここでようやく自分なりに答えが出ました。自分の勉強のため、一応ここに書いておきます。

各名称 役割
Controller コントローラー。ブラウザ、そしてServiceとやりとり。
Service DBに対して何をしたいかを書いた部分。保存だったり、更新だったり、具体的なことを書いていくところ。
Repository Serviceから処理要求をもらって、DBとの直接のやりとりをする。抽象化して使う。データ取得、更新など。
Entity データの入れ物。保管するための箱とみたいなもの。

自分の中ではまとまっていますが、結構わかりにくいかもしれないです。
注釈を入れていくと、RepositoryはServiceの処理の実現のための抽象的なものだと解釈すればいいでしょう。
Service行うことを実装するためのものです。
で、EntityはDBとやりとりするのに値を保存する必要があるので、その時に使う箱。
数字を入れたりだとか。そういった役割をします。
ここでは自分にわかりやすいように書いていますが、もしわからない時は、先ほどのリンクできっちり見てみるといいかもしれません。

終わりに

先ほどのサイトのおかげで、結構Springでの役者の役割がわかりました。
結構行き詰まる人もいそうだというのが私の感想です。

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

JDBC URL(Oracle Database, Thin)の作り方

JDBC URL(Oracle Database, Thin)の作り方を書いてみるやで彡(゚)(゚)

1. JDBCマニュアルの記述

マニュアルの記述は以下の通り。

8.2 データベースURLとデータベース指定子
https://docs.oracle.com/cd/E96517_01/jjdbc/data-sources-and-URLs.html#GUID-C4F2CA86-0F68-400C-95DA-30171C9FB8F0
データベースURLは文字列です。完全なURL構文は、次のとおりです。
jdbc:oracle:driver_type:[username/password]@database_specifier
 

8.2.4 Thin形式のサービス名の構文
https://docs.oracle.com/cd/E96517_01/jjdbc/data-sources-and-URLs.html#GUID-EF07727C-50AB-4DCE-8EDC-57F0927FF61A
Thin形式のサービス名は、JDBC Thinドライバでのみサポートされます。構文は次のとおりです。
@//host_name:port_number/service_name

なお上記の記述は簡易接続(EZCONNECT)そのものなので、簡易接続のマニュアルもリンクしとく彡(゚)(゚)

8.1 簡易接続ネーミング・メソッドの理解
https://docs.oracle.com/cd/E96517_01/netag/configuring-naming-methods.html#GUID-B0437826-43C1-49EC-A94D-B650B6A4A6EE
CONNECT username@[//]host[:port][/service_name][:server][/instance_name]

原則としてホスト名/ポート番号/サービス名の3つを指定すれば、JDBC URLを作成できます。
これらを管理するのはリスナーなので、まずリスナーの役割を簡単に解説。

2. リスナーの役割

リスナーはサーバー上に常駐するプロセスで、クライアントからの接続要求を
リスニングして、Oracle Database への接続を管理/許可するプロセスです。

専用サーバー接続の例ですが、リスナーの動作は下記の記事が解り易いです。

コネクションとは?
https://www.oracle.com/technetwork/jp/articles/chapter5-1-101584-ja.html#p01b

リスナーは1組以上のホスト名(IPアドレス)/ポート番号が定義されていて、
そのリスナーにデータベース・サービスが動的に登録されます。

lsnrctl status <リスナー名>コマンドを実行すると、ホスト名(IPアドレス)/ポート番号や
登録されているデータベース・サービスが確認できます。以下はサンプル彡(゚)(゚)

$ lsnrctl status LISTENER

LSNRCTL for Linux: Version 18.0.0.0.0 - Production on 30-APR-2019 23:39:27
:
Listening Endpoints Summary...
  (DESCRIPTION=(ADDRESS=(PROTOCOL=ipc)(KEY=EXTPROC1)))
  (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=0.0.0.0)(PORT=1521))) ★ホスト名とポート番号
  (DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=8081))(Presentation=HTTP)(Session=RAW))
:
Services Summary...
Service "64a52f53a7683286e053cda9e80aed76" has 1 instance(s). ★リスナーに登録されたサービス
  Instance "orclcdb", status READY, has 1 handler(s) for this service...
Service "784ac9d638bb5f59e0530100007f6047" has 1 instance(s). ★リスナーに登録されたサービス
  Instance "orclcdb", status READY, has 1 handler(s) for this service...
Service "AYSTEST" has 1 instance(s).                          ★リスナーに登録されたサービス
  Instance "orclcdb", status READY, has 1 handler(s) for this service...
Service "orcl" has 1 instance(s).                             ★リスナーに登録されたサービス
  Instance "orclcdb", status READY, has 1 handler(s) for this service...
Service "orclcdb" has 2 instance(s).                          ★リスナーに登録されたサービス
  Instance "orclcdb", status UNKNOWN, has 1 handler(s) for this service...
  Instance "orclcdb", status READY, has 1 handler(s) for this service...
Service "orclcdbXDB" has 1 instance(s).                       ★リスナーに登録されたサービス
  Instance "orclcdb", status READY, has 1 handler(s) for this service...
The command completed successfully

3. データベース・サービスとは?

データベース・サービスとは、Oracle Database のワークロード(負荷)を識別し易くするために、
論理的な別名を付与したものとなります。1つの Oracle Database環境に複数サービスを作成可能です。

以下の記事が分かり易いですやで彡(゚)(゚)

第3回 ネットワーク経由で接続
https://www.oracle.com/technetwork/jp/database/articles/kusakabe/kusakabe-3-4490049-ja.html
4.2 サービス
Oracle8までは…(中略)…Oracleインスタンス識別子であるSIDでした …(中略)…
Oracle Database 10gではサービスの概念が拡張され、ワークロードを抽象化する概念となりました …(中略)…
追加されたサービスは、動的サービス登録の仕組みによってOracleリスナーに登録されます。

JDBC URL/tnsnames.ora/簡易接続(EZCONNECT)など、接続先としてこのデータベース・サービス名を記述します。

4. サンプル1:sqlclでJDBC URLを指定しつつ接続確認

sqlcl(SQL Developerのコマンドライン版)で、JDBC URLのサンプルを書いてみるやで彡(゚)(゚)
上記 2. の AYTESTサービスに接続してみます。このケースの JDBC URL は下記の通り

jdbc:oracle:driver_type:[username/password]@//host_name:port_number/service_name

jdbc:oracle:thin:@//0.0.0.0:1521/AYTEST

実行サンプルを下記に示します。CONNECTコマンドのユーザ名直後の@マーク以降がJDBC URLです。
sqlcl の SHOW JDBCコマンドでも JDBC URL は確認できます。

cd /home/oracle/sqldeveloper/sqldeveloper/bin
./sql /nolog
CONNECT AYSHIBAT@jdbc:oracle:thin:@//0.0.0.0:1521/AYSTEST
SHOW JDBC

SQLcl: Release 18.3 Production on Wed May 01 01:06:43 2019

Copyright (c) 1982, 2019, Oracle.  All rights reserved.
Password? (**********?) ********
Connected.

-- Database Info --
Database Product Name: Oracle
:
-- Driver Info --
Driver Name: Oracle JDBC driver
Driver Version: 18.3.0.0.0
Driver Major Version: 18
Driver Minor Version: 3
Driver URL: jdbc:oracle:thin:@//0.0.0.0:1521/AYSTEST JDBC URL
:

5. サンプル2:OCI DB(DBaaS)のCDBにsqlclでJDBC URLを指定しつつ接続

OCI(Oracle Cloud Infrastructure) DB(DBaaS) の CDB に sqlcl JDBC URLで接続するサンプルを書いてみます彡(゚)(゚)
下記記事のサンプルを流用してみるやで。

OCI Database(DBaaS) の PDB に sqlplus で接続してみる。(Oracle Cloud Infrastructure)
https://gonsuke777.hatenablog.com/entry/2019/02/19/211953

管理サービス(CDB)への接続文字列(簡易接続):
dbname.subnetname.vcnname.oraclevcn.com:1521/dbname_cdb32r.subnetname.vcnname.oraclevcn.com

上記ケースの JDBC URL は 下記の通りとなります。

jdbc:oracle:driver_type:[username/password]@//host_name:port_number/service_name

jdbc:oracle:thin:@//dbname.subnetname.vcnname.oraclevcn.com:1521/dbname_cdb32r.subnetname.vcnname.oraclevcn.com

sqlclによる接続サンプルは下記の通り彡(゚)(゚)

./sql /nolog
CONNECT SYSTEM@jdbc:oracle:thin:@//dbname.subnetname.vcnname.oraclevcn.com:1521/dbname_iad32r.subnetname.vcnname.oraclevcn.com
SHOW JDBC;

Password? (**********?) ********
Connected.

-- Database Info --
Database Product Name: Oracle
:
-- Driver Info --
Driver Name: Oracle JDBC driver
Driver Version: 12.2.0.1.0
Driver Major Version: 12
Driver Minor Version: 2
Driver URL: jdbc:oracle:thin:@//dbname.subnetname.vcnname.oraclevcn.com:1521/dbname_iad32r.subnetname.vcnname.oraclevcn.com JDBC URL
:

6. サンプル3:Autonomous DB(ATP/ADW)の場合のJDBC URL

Autonomous DB(ADW/ATP)の場合のJDBC URLは下記記事を参照彡(゚)(゚)

Autonomous DB(ADW/ATP) に Java の JDBC Thin Driver で接続してみる。(OCI, Oracle Cloud Infrastructure)
https://gonsuke777.hatenablog.com/entry/2019/02/26/023534

何かしらの方法でウォレットの格納場所をTNS_ADMINに指定します。
JDBC URL に TNS_ADMIN を直接記述するやり方だと、下記の通り彡(゚)(゚)

  1. 方法1:ウォレットのパス(TNS_ADMIN) を JDBC URL に記述 https://gonsuke777.hatenablog.com/entry/2019/02/26/023534#4-%E6%96%B9%E6%B3%951%E3%82%A6%E3%82%A9%E3%83%AC%E3%83%83%E3%83%88%E3%81%AE%E3%83%91%E3%82%B9TNS_ADMIN-%E3%82%92-JDBC-URL-%E3%81%AB%E8%A8%98%E8%BF%B0

    ★下記がJDBC URL
    jdbc:oracle:thin:@xxxxxx_high?TNS_ADMIN=/home/opc/app/opc/product/18.0.0/client_1/network/admin

結果は記事を見てね。

7. サンプル4:tnsnames.oraっぽい書き方でJDBC URLを記述

マニュアルにも記載が有るとおり、
JDBC URLはtnsnames.oraっぽく書くこともできる。下記はそのサンプル彡(゚)(゚)

※実際には1行で記述
jdbc:oracle:thin:@
(DESCRIPTION_LIST=
  (DESCRIPTION=
    (ADDRESS=(PROTOCOL=tcp)(HOST=dbname.subnetname.vcnname.oraclevcn.com)(PORT=1521))
    (CONNECT_DATA=(SERVICE_NAME=dbname_iad32r.subnetname.vcnname.oraclevcn.com))
  )
  (DESCRIPTION=
    (ADDRESS=(PROTOCOL=tcp)(HOST=dbname-scan.subnetname.vcnname.oraclevcn.com)(PORT=1521))
    (CONNECT_DATA=(SERVICE_NAME=dbname_iad32r.subnetname.vcnname.oraclevcn.com))
  )
)

sqlclでの接続サンプルは下記の通り彡(゚)(゚) コマンドながーい。

./sql /nolog
CONNECT SYSTEM@jdbc:oracle:thin:@(DESCRIPTION_LIST=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=dbname_iad32r.subnetname.vcnname.oraclevcn.com)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=aysdb121_iad1rn.sub12070931430.vcnname.oraclevcn.com)))(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=dbname-scan.subnetname.vcnname.oraclevcn.com)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=dbname_iad32r.subnetname.vcnname.oraclevcn.com))))
SHOW JDBC

SQLcl: Release 19.1 Production on Wed May 01 06:20:39 2019

Copyright (c) 1982, 2019, Oracle.  All rights reserved.
Password? (**********?) ***************
Connected.

-- Database Info --
Database Product Name: Oracle
:
-- Driver Info --
Driver Name: Oracle JDBC driver
Driver Version: 18.3.0.0.0
Driver Major Version: 18
Driver Minor Version: 3
Driver URL: jdbc:oracle:thin:@(DESCRIPTION_LIST=(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=dbname_iad32r.subnetname.vcnname.oraclevcn.com)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=aysdb121_iad1rn.sub12070931430.vcnname.oraclevcn.com)))(DESCRIPTION=(ADDRESS=(PROTOCOL=tcp)(HOST=dbname-scan.subnetname.vcnname.oraclevcn.com)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=dbname_iad32r.subnetname.vcnname.oraclevcn.com))))
:

8. まとめ

上記 7. みたいな書き方が出来ちゃうから、評判が悪いのかしら彡(゚)(゚)

ここら辺はOracle MAA(Maximum Availability Architecture)の一環、
接続時フェイルオーバーとかクライアント・サイド・ロードバランスとかで
こうなっていると認識しているので、ご理解頂くよう要努力ですかね(゚ε゚ )

とまれ、簡易接続(EZCONNECT)さえ理解してれば恐れるに足らず!
どんどん接続(?)してくれやで彡(^)(^)

おまけ. サンプル:Autonomous DB(ATP/ADW) に sqlclで接続

sqlclの場合はset cloudconfigコマンドでウォレットを指定します。下記が参考になります。

Oracle Cloud:Autonomous DatabaseにSQLcl接続してみてみた
https://qiita.com/shirok/items/86355be72a47a840d10e

set cloudconfigした後の JDBC URL は下記の通り、あら何か複雑彡(゚)(゚)

./sql /nolog
set cloudconfig /home/opc/app/opc/product/18.0.0/client_1/network/admin/Wallet_aysatp01.zip
CONNECT ADMIN@aysatp01_low
SHOW JDBC


SQLcl: Release 19.1 Production on Wed May 01 05:58:28 2019

Copyright (c) 1982, 2019, Oracle.  All rights reserved.

Operation is successfully completed.
Operation is successfully completed.
Using temp directory:/tmp/oracle_cloud_config4416493815228189719
Password? (**********?) *************
Connected.

-- Database Info --
Database Product Name: Oracle
:
-- Driver Info --
Driver Name: Oracle JDBC driver
Driver Version: 18.3.0.0.0
Driver Major Version: 18
Driver Minor Version: 3
Driver URL: jdbc:oracle:thin:@(description= (address=(protocol=tcps)(port=1522)(host=xxxx.xxxx.xxxxxxxx.com))(connect_data=(service_name=xxxxxx_low.xxxx.xxxxxxxx.com))(security=(ssl_server_cert_dn="CN=xxxx.xxxx.xxxxxxxx.com,OU=…,O=…,L=…,ST=…,C=…"))   )
:
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Seasar2 Formのsetter/getterのパラメータ型が異なると認識しなくなる

今更Seasar2の内容になりますが、ActionFormが自分の思った通りに動かなかったので、今後の自分のためにフレームワークの内容を調べてまとめて見ました。

実行環境

  • Java 1.8
  • Seasar 2.4.46
  • SAStruts 1.0.4-sp9
  • Jackson 2.9.8

やりたかったこと

json形式のパラメータをbeanに詰め替えたいです。

Springであればメソッドのパラメータにjsonを想定した引数を指定してあげれば、spring側で自動的に詰めてくれるので、それと同じようなことをSeasarでも実現したいです。

springの場合
String getName(@RequestBody PersonForm personForm) {
    return personForm.getFirstName() + personForm.getLastName();
}

しかしSeasarではFormの引数にオブジェクトを設定してもjson形式を自動でセットはしてくれないです。

Seasarでやりたいけど出来ない
public void setPerson(PersonForm personForm) {
    this.personForm = personForm;
}

ではそのためにどうしたか。その結果どうなったかというのが今回の内容です。

Formのsetter/getterでパラメータ型を変える(失敗します)

先ほどの通り、setの引数にオブジェクトを指定しても認識してくれないので、引数にはString型を指定しました。ここにjsonが格納されます。
setterメソッドの中で、jsonからBeanオブジェクトに詰め替えています。
getterメソッドでは、setter内で作成したBeanオブジェクトを返却するようにしています。

具体的なコードは以下のイメージです。

ExampleForm
public class ExampleForm {

    private PersonForm personForm;

    public void setPerson(String person) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            personForm = mapper.readValue(person, PersonForm.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public PersonForm getPerson() {
        return this.personForm;
    }
}

このコードに対して以下のようなjsonファイルをリクエストしても、PersonFormには値がセットされません。

リクエストするjson
{
  "firstName": "太郎",
  "lastName": "田中"
}

原因としては、setPersonの引数がString型、getPersonがPersonForm型と別の型になっていることで、Seasar側でset出来ないという理由からです。

SeasarではForm内のsetter・getterをリクエストごとに認識してリクエストパラメータ内の値を自動的にsetする仕様になっているのですが、その中でパラメータの型が異なるとエラーとなるようにしているからです。

詳細について興味がある方はSeasarの挙動について後述しますのでご覧ください。

対処法

私の考える対処法としては、メソッド名を変更することです。
以下のようなコードになります。

ExampleForm
public class ExampleForm {

    private PersonForm personForm;

    public void setPerson(String person) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            personForm = mapper.readValue(person, PersonForm.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public PersonForm getPersonForm() {
        return this.personForm;
    }
}

おまけ:Seasar2のActionForm操作

おまけになりますが、Seasar2でどのようにActionFormを操作しているのかを確認しましたので記載しておきます。

Seasar2でActionFormをセットするまでのスタックトレース
javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:432)
org.apache.struts.action.ActionServlet.process(ActionServlet.java:1196)
org.seasar.struts.action.S2RequestProcessor.process(S2RequestProcessor.java:104)
org.seasar.struts.action.S2RequestProcessor.processPopulate(S2RequestProcessor.java:290)
org.seasar.struts.action.S2RequestProcessor.setProperty(S2RequestProcessor.java:398)
org.seasar.struts.action.S2RequestProcessor.setSimpleProperty(S2RequestProcessor.java:482)
org.seasar.framework.beans.impl.PropertyDescImpl.setValue(PropertyDescImpl.java:251)
org.seasar.framework.util.MethodUtil.invoke(MethodUtil.java:96)
java.lang.reflect.Method.invoke(Method.java:498)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
org.seasar.sastruts.example.form.ExampleForm.setPerson(ExampleForm.java:13)

ActionFormの設定をしている中で、今回問題となったロジックがある箇所は、S2RequestProcessorクラスのsetSimplePropertyメソッドです。

このメソッドの処理の流れとしては、BeanDescとPropertyDescを生成して、PropertyDesc(変数名:pd)がisWritableであれば、setValueメソッドによってActionFormのフィールドにリクエストの内容をセットするとなっています。

BeanDescクラスはActionFrom自体を管理するクラス、PropertyDescクラスはActionForm内のフィールド(setter/getterなど)を管理するメソッドというイメージになります。

原因となったロジックとしては、BeanDescImplクラス(BeanDescクラスの具象クラス)の中にあります。

BeanDescImplクラス
private void setupPropertyDescs() {
    Method[] methods = beanClass.getMethods();
    for (int i = 0; i < methods.length; i++) {
        Method m = methods[i];
        if (MethodUtil.isBridgeMethod(m) || MethodUtil.isSyntheticMethod(m)) {
            continue;
        }
        String methodName = m.getName();
        if (methodName.startsWith("get")) {
            if (m.getParameterTypes().length != 0
                    || methodName.equals("getClass")
                    || m.getReturnType() == void.class) {
                continue;
            }
            String propertyName = decapitalizePropertyName(methodName
                    .substring(3));
            setupReadMethod(m, propertyName);
        } else if (methodName.startsWith("is")) {
            if (m.getParameterTypes().length != 0
                    || !m.getReturnType().equals(Boolean.TYPE)
                    && !m.getReturnType().equals(Boolean.class)) {
                continue;
            }
            String propertyName = decapitalizePropertyName(methodName
                    .substring(2));
            setupReadMethod(m, propertyName);
        } else if (methodName.startsWith("set")) {
            if (m.getParameterTypes().length != 1
                    || methodName.equals("setClass")
                    || m.getReturnType() != void.class) {
                continue;
            }
            String propertyName = decapitalizePropertyName(methodName
                    .substring(3));
            setupWriteMethod(m, propertyName);
        }
    }
    for (Iterator i = invalidPropertyNames.iterator(); i.hasNext();) {
        propertyDescCache.remove(i.next());
    }
    invalidPropertyNames.clear();
}

private void setupReadMethod(Method readMethod, String propertyName) {
    Class propertyType = readMethod.getReturnType();
    PropertyDesc propDesc = getPropertyDesc0(propertyName);
    if (propDesc != null) {
        if (!propDesc.getPropertyType().equals(propertyType)) {
            invalidPropertyNames.add(propertyName);
        } else {
            propDesc.setReadMethod(readMethod);
        }
    } else {
        propDesc = new PropertyDescImpl(propertyName, propertyType,
                readMethod, null, null, this);
        addPropertyDesc(propDesc);
    }
}

このロジックをご覧いただければわかると思うのですが、メソッド名の先頭3文字が"get"や"set"の時には、setupReadMethod、setupWriteMethodが呼び出されてPropertyDescImplクラスを生成するという流れになっています。

PropertyDescImplクラスでは、propertyName(ActionFormのget/setを除いたメソッド名)がキー項目となっています。
ここで注目すべきは、setupReadMethodメソッド内の以下記述になります。

setupReadMethod
if (!propDesc.getPropertyType().equals(propertyType)) {
    invalidPropertyNames.add(propertyName);

すでにPropertyDescImplが登録されている場合かつ、プロパティタイプ(引数の型・戻り値の型)が異なる場合は、invalidPropertyNamesに登録されるという流れになります。

以上のことから、Seasar2ではsetterの引数の型とgetterの戻り値の型が異なる場合には、プロパティの管理対象から除外されてしまい、自動でsetされないということになります。

その他参考になる記事

上記の内容はFormでのjson操作でしたが、Springと同じように上位階層でjsonをオブジェクトに詰めることが出来るようになる方法は以下の記事を参考にしてください。
SAStrutsとJSON @shienaさん

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

Calendar.MONTH

備忘録です。

// 現在日時
private Calendar now = Calendar.getInstance();
year = now.get(Calendar.YEAR);
monthOfYear = now.get(Calendar.MONTH);
dayOfMonth = now.get(Calendar.DAY_OF_MONTH);

てやっても1か月前にさかのぼってしまいます

Calendar.MONTH はどうやら0から始まるようです。

なので

// 現在日時
private Calendar now = Calendar.getInstance();
year = now.get(Calendar.YEAR);
monthOfYear = now.get(Calendar.MONTH) + 1;
dayOfMonth = now.get(Calendar.DAY_OF_MONTH);

と書きましょう。

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

CLIで理解するJavaのコンパイルと実行

Javaプログラミングをする場合にIDEを使うことがほとんどだと思います。Javaプログラミングの学習を始める場合でもIDEの準備から求められることが大半です。しかし初学者の方がIDEでのJavaプログラミングから学習を初めると、IDE上でコードは書けるようになってもIDEがサポートしてくれる部分を深く理解できずにいたり、IDEに依存した作業しかできない状態から成長できません。
この記事は簡単なJavaのプログラムを、IDEを使わずにCLIで実行し理解を深める初学者向けの記事となります。プログラムの詳細な読み方や書き方までは言及しません。途中初学者には難しい言葉や概念などが登場するかもしれませんが、すぐに理解する必要はないので読み流しつつ徐々に理解していってください。

環境

今回作業するOSや利用するJavaのversionになります。本記事ではCentOSで作業していますが、vagrantの導入などができない場合はMacでも問題ないですし、UNIXコマンドを置き換えればWindowsのコマンドプロンプトでも作業可能だと思います(検証はしてません)。

OS

こちらのBOXを使ってVirtualBox上にvagrantでVMを起動し、その中で作業します。

$ cat /etc/os-release
NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"

Java

こちらのOpenJDKをyumでインストールして使います。

$ java -version
java 10.0.2 2018-07-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.2+13)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.2+13, mixed mode)

$ javac -version
javac 10.0.2

Javaプログラム

Javaのプログラムは通常、実行前にjavacツールで.javaから.classというバイトコード(中間コードとも言う)にコンパイルします。そしてプログラム実行時にJVM上でバイトコードをインタプリタ方式で実行、もしくはJITコンパイラによってマシンコードに再コンパイルして実行されます。JITコンパイラはJRE(Javaランタイム環境)のコンポーネントの一つで、プログラムメソッドのマシンコードを最適化しパフォーマンス向上をする仕組みです。

ここからはCLIで実際にJavaプログラムのコンパイルと実行、アーカイブの作成までの作業を行います。

基本となるコンパイルと実行

まずはHello world.と出力するプログラムを書いて実行してみます。実行するプログラムファイルは下記となるのでviなどで用意してください。

App.java
class App {
    public static void main(String[] args) {
        System.out.println("Hello world.");
    }
}

これをjavacでコンパイルします。

$ ls
App.java

$ javac App.java

$ ls
App.class  App.java

.classファイルが生成されたことが確認できます。その後javaで実行します。引数はファイル名ではなくクラス名となるので注意してください。

$ java App 
Hello world.

簡単でしたが、コンパイルとプログラムの実行をしました。ここまでは問題ないですね。

引数ありの実行

プログラム実行時に引数を渡してみます。先ほどのプログラムファイルに下記の修正を加えます。

App.java
class App {
    public static void main(String[] args) {
        for (String arg: args) {
            System.out.println(String.format("Hello %s.", arg));
        }
    }
}

コンパイルして実行します。先ほどとは違い、実行時に引数を渡してみます。

$ java App Alice Bob Carol
Hello Alice.
Hello Bob.
Hello Carol.

引数を受け取れていることが確認できますね。

他クラスの利用

プログラム内で他のクラスにアクセスしてみます。まず他のクラスとして人間を表現するHumanクラスを別のファイルで作ります。

Human.java
class Human {
    String name;
    Human(String name) {
            this.name = name;
    }
    void introduceMyself() {
            System.out.println(String.format("My name is %s.", this.name));
    }
}

先ほどのAppクラスのmainメソッド内でHumanクラスをインスタンス化します。

App.java
class App {
    public static void main(String[] args) {
        for (String arg: args) {
            Human human = new Human(arg);
            human.introduceMyself();
        }
    }
}

ではコンパイルしてみます。

$ ls
App.java  Human.java

$ javac App.java

$ ls
App.class  App.java  Human.class  Human.java

Appのコンパイルと一緒にHumanもコンパイルされたことが確認できます。

$ java App Alice Bob Carol
My name is Alice.
My name is Bob.
My name is Carol.

引数の数だけHumanインスタンスを生成しメソッドが実行されたことを確認しました。

パッケージ管理

パッケージ名を付与して、別々のパッケージのプログラムとしてコンパイルしてみます。まずはHumanクラスにパッケージ名を付与します。また、このクラスは別のパッケージからアクセスされるクラスなので、各修飾子を正しく付与しました。

Human.java
package jp.co.sample.lib;

public class Human {
    private String name;
    public Human(String name) {
        this.name = name;
    }
    public void introduceMyself() {
        System.out.println(String.format("My name is %s.", this.name));
    }
}

続いてAppクラスですが、パッケージ名を付与すると共にHumanクラスにアクセスするためにimportも記述します。

App.java
package jp.co.sample;

import jp.co.sample.lib.Human;

class App {
    public static void main(String[] args) {
        for (String arg: args) {
            Human human = new Human(arg);
            human.introduceMyself();
        }
    }
}

パッケージ名の付与は完了しましたが、このままではコンパイルができません。Javaではパッケージ名と同様のディレクトリ構成にしてファイルを配置する必要があります。なのでディレクトリを下記のように作りファイルを移動させてください。

$ tree
.
└── jp
    └── co
        └── sample
            ├── App.java
            └── lib
                └── Human.java

4 directories, 2 files

ファイルを移動させたらコンパイルして実行してみます。

$ javac jp/co/sample/App.java

$ tree
.
└── jp
    └── co
        └── sample
            ├── App.class
            ├── App.java
            └── lib
                ├── Human.class
                └── Human.java

4 directories, 4 files

$ java jp.co.sample.App Alice Bob Carol
My name is Alice.
My name is Bob.
My name is Carol.

.javaファイルと同階層に.classファイルが作成され、プログラムが実行できたことを確認しました。

JARファイルの作成

作成した.classファイルを.jarにまとめアーカイブを作成します。.javaファイルは.jarには含めないので、srcディレクトリとして分けます。

$ tree
.
└── src
    └── jp
        └── co
            └── sample
                ├── App.java
                └── lib
                    └── Human.java

5 directories, 2 files

コンパイルをしてclassesディレクトリに.classファイルを出力します。パッケージ起点となるsrcディレクトリでない場所で実行する場合は-sourcepathオプションでパッケージ起点を指定する必要があります。

$ javac -sourcepath src -d classes src/jp/co/sample/App.java 

$ tree
.
├── classes
│   └── jp
│       └── co
│           └── sample
│               ├── App.class
│               └── lib
│                   └── Human.class
└── src
    └── jp
        └── co
            └── sample
                ├── App.java
                └── lib
                    └── Human.java

10 directories, 4 files

classesディレクトリが作成され、その下に.classファイルがパッケージと同じディレクトリ構成で生成されたことが確認できます。
ちなみに実行時にパッケージ起点でない場所で実行する場合は-classpathオプションでパッケージ起点を指定する必要があります。

$ java -classpath classes jp.co.sample.App Alice Bob Carol
My name is Alice.
My name is Bob.
My name is Carol.

続いて.jar作成のためにMANIFESTファイルが必要となるので、下記のファイルを作成します。ここにはmainメソッドを持つクラス名をパッケージ名含め記載しておきます。最終行に空行を一行入れないとMANIFESTファイルとして認識してくれないのでお忘れなく。

manifest.mf
Main-Class: jp.co.sample.App

MANIFESTファイルを用意したらjarで.jarファイルを作成します。

$ jar cvfm sample.jar manifest.mf -C classes .
マニフェストが追加されました
jp/を追加中です(=0)(=0)(0%格納されました)
jp/co/を追加中です(=0)(=0)(0%格納されました)
jp/co/sample/を追加中です(=0)(=0)(0%格納されました)
jp/co/sample/App.classを追加中です(=469)(=343)(26%収縮されました)
jp/co/sample/lib/を追加中です(=0)(=0)(0%格納されました)
jp/co/sample/lib/Human.classを追加中です(=595)(=382)(35%収縮されました)

$ ls
classes  manifest.mf  sample.jar  src

$ jar -tf sample.jar 
META-INF/
META-INF/MANIFEST.MF
jp/
jp/co/
jp/co/sample/
jp/co/sample/App.class
jp/co/sample/lib/
jp/co/sample/lib/Human.class

.jarファイルの中にMANIFESTファイルと.classファイルを内包していることが確認できます。
では最後に.jarファイルを実行してみます。

$ java -jar sample.jar Alice Bob Carol
My name is Alice.
My name is Bob.
My name is Carol.

まとめ

本記事ではプログラムを書いてコンパイルし、その後.jarファイルを作成し実行するところまで作業してみました。IDEでの作業とCLIでコマンドを叩いてコンパイルや実行をするのとでは作業内容が大きく違うと感じたことでしょう。UIのあるIDEでは直感的に作業ができるのに対し、CLIでの作業は一つ一つコマンドを理解して実行する必要があると思います。本記事の内容を理解することが、IDEでの作業理解にも活きてくると思います。

おまけ

OpenJDKには、コンパイルした.classファイルを逆アセンブルできるツールが内包されており、プログラムの詳細な命令文を追うことができるので、余裕がある方は見てみると良いでしょう。

$ javap -v -classpath classes jp.co.sample.App
Classfile /home/vagrant/java_test/classes/jp/co/sample/App.class
  Last modified 2019/04/30; size 469 bytes
  MD5 checksum 7ad6f96dd09200ac12a4c48cadb71ea8
  Compiled from "App.java"
class jp.co.sample.App
  minor version: 0
  major version: 54
  flags: (0x0020) ACC_SUPER
  this_class: #5                          // jp/co/sample/App
  super_class: #6                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #6.#17         // java/lang/Object."<init>":()V
   #2 = Class              #18            // jp/co/sample/lib/Human
   #3 = Methodref          #2.#19         // jp/co/sample/lib/Human."<init>":(Ljava/lang/String;)V
   #4 = Methodref          #2.#20         // jp/co/sample/lib/Human.introduceMyself:()V
   #5 = Class              #21            // jp/co/sample/App
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               StackMapTable
  #14 = Class              #23            // "[Ljava/lang/String;"
  #15 = Utf8               SourceFile
  #16 = Utf8               App.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Utf8               jp/co/sample/lib/Human
  #19 = NameAndType        #7:#24         // "<init>":(Ljava/lang/String;)V
  #20 = NameAndType        #25:#8         // introduceMyself:()V
  #21 = Utf8               jp/co/sample/App
  #22 = Utf8               java/lang/Object
  #23 = Utf8               [Ljava/lang/String;
  #24 = Utf8               (Ljava/lang/String;)V
  #25 = Utf8               introduceMyself
{
  jp.co.sample.App();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0

  public static void main(java.lang.String[]); 
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=6, args_size=1
         0: aload_0
         1: astore_1
         2: aload_1
         3: arraylength
         4: istore_2
         5: iconst_0
         6: istore_3
         7: iload_3
         8: iload_2
         9: if_icmpge     39
        12: aload_1
        13: iload_3
        14: aaload
        15: astore        4
        17: new           #2                  // class jp/co/sample/lib/Human
        20: dup
        21: aload         4
        23: invokespecial #3                  // Method jp/co/sample/lib/Human."<init>":(Ljava/lang/String;)V
        26: astore        5
        28: aload         5
        30: invokevirtual #4                  // Method jp/co/sample/lib/Human.introduceMyself:()V
        33: iinc          3, 1
        36: goto          7
        39: return
      LineNumberTable:
        line 7: 0
        line 8: 17
        line 9: 28
        line 7: 33
        line 11: 39
      StackMapTable: number_of_entries = 2
        frame_type = 254 /* append */
          offset_delta = 7
          locals = [ class "[Ljava/lang/String;", int, int ]
        frame_type = 248 /* chop */
          offset_delta = 31
}
SourceFile: "App.java"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Java を使うWeb アプリの文字コードを Shift_JIS から UTF-8 に変更する場合の考慮点

概要

昔からあるWebアプリケーションのHTML文字コード設定をUTF-8に変える時のポイントをまとめてみる。

前提

  • Webページの文字コード設定をShift_JISからUTF-8へ変更する
  • 利用したソフトウェアは以下
  • Java, Liberty, ChromeはmacOS、Db2はコンテナで稼働させる

Db2環境構築

env_list という名前で以下の内容のファイルを作っておく

LICENSE=accept
DB2INSTANCE=db2inst1
DB2INST1_PASSWORD=password
DBNAME=testdb
BLU=false
ENABLE_ORACLE_COMPATIBILITY=false
UPDATEAVAIL=NO
TO_CREATE_SAMPLEDB=false
REPODB=false
IS_OSXFS=false
PERSISTENT_HOME=true
HADR_ENABLED=false
ETCD_ENDPOINT=
ETCD_USERNAME=
ETCD_PASSWORD=
# Docker環境の構築
brew install docker docker-machine
docker-machine create --driver virtualbox default
eval $(docker-machine env)
docker-machine ip

# Db2コンテナの稼働
docker login
docker pull store/ibmcorp/db2_developer_c:11.1.4.4-x86_64
docker run -h db2server --name db2server --detach --privileged=true -p 50000:50000 -p 55000:55000 --env-file env_list store/ibmcorp/db2_developer_c:11.1.4.4-x86_64
docker exec -it db2server bash

# ibm-943コードページのデータベース作成
su - db2inst1
db2 create db ibm943db using codeset ibm-943 territory jp collate using identity

HTMLの変更箇所

<!doctype html>
  <html lang="en">
  <head>
    <meta charset="utf-8">
    <title>title</title>
  </head>
  <body>
    ...

JSPの変更箇所

HTTPレスポンスのcontent-typeとJSPファイル自体の文字コードは以下のように指定する

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" %>

WebページをUTF-8に変えると何が起きるか

ここからが本題。Webページを変更した時にサーバーサイドではどんな対処が必要なのかまとめてみる。

ブラウザからサーバーへ送信されるデータの文字参照が変わる

ユーザーが入力した文字によっては、ブラウザによって数値文字参照へ変換されて送信される。Shift_JISを利用する場合と、UTF-8を利用する場合で数値文字参照へ変換される文字が異なる。例えば、?という絵文字はWebページがShift_JISの場合、&#128512;に変換されるが、UTF-8の場合は?がそのまま送信される。以下の例では、¢、?などで数値文字参照が利用される。

ユーザーの入力 Unicodeコードポイント Formエンコードデータ(Shift_JIS)の場合 Formエンコードデータ(UTF-8)の場合 備考
a U+0061 a a
& U+0026 &(%26) &(%26)
¢ U+00A2 &#162;(%26%23162%3B) ¢(%C2%A2) CENT SIGN. Shift_JIS に存在しない文字
U+FFE0 ¢(%81%91) ¢(%EF%BF%A0) FULLWIDTH CENT SIGN
U+2014 &#8212;(%26%238212%3B) —(%E2%80%94) EM DASH
U+2015 ―(%81%5C) ―(%E2%80%95) HORIZONTAL BAR
U+FF0D -(%81%7C) -(%EF%BC%8D) FULLWIDTH HYPHEN-MINUS
U+2212 −(%81%7C) −(%E2%88%92) MINUS SIGN(上記も%81%7Cとなっていることに注意)
U+2225 ∥(%81a) ∥(%E2%88%A5) PARALLEL TO
U+2016 &#8214;(%26%238214%3B) ‖(%E2%80%96) DOUBLE VERTICAL LINE
U+FF5E ~(%81%60) ~(%EF%BD%9E) FULLWIDTH TILDE
U+301C &#12316;(%26%2312316%3B) 〜(%E3%80%9C) WAVE DASH
U+FFE4 ¦(%FAU) ¦(%EF%BF%A4) FULLWIDTH BROKEN BAR
¦ U+00A6 &#166;(%26%23166%3B) ¦(%C2%A6) BROKEN BAR
U+2460 ①(%87%40) (%E2%91%A0)
U+3231 ㈱(%87%8A) ㈱(%E3%88%B1)
U+3042 あ(%82%A0) あ(%E3%81%82)
U+FF71 ア(%B1) ア(%EF%BD%B1) 半角カタカナ
U+9AD9 髙(%FB%FC) 髙(%E9%AB%99) IBM 拡張漢字(JIS 第 1〜4 水準外)
U+3402 &#13314;(%26%2313314%3B) 㐂(%E3%90%82) JIS 第 3 水準
U+5F45 彅(%FAg) 彅(%E5%BD%85) JIS 第 3 水準
U+7E6B &#32363;(%26%2332363%3B) 繫(%E7%B9%AB) JIS 第 3 水準.JIS X 0213 へ 2004 年に追加
U+845B 葛(%8A%8B) 葛(%E8%91%9B)
葛 ? U+845B U+E0100 葛&#23128512;(%8A%8B%26%23917760%3B) 葛+異体字セレクタ(%E8%91%9B%F3%A0%84%80) 異体字セレクタを利用
U+30D1 パ(%83p) パ(%E3%83%91)
パ U+30CF U+309A ハ&#12442;(%83n%26%2312442%3B) ハ+半濁点(%E3%83%8F%E3%82%9A) 結合文字
? U+1F600 &#128512;(%26%23128512%3B) ?(%F0%9F%98%80) サロゲートペア
? U+20BB7 &#134071;(%26%23134071%3B) ?(%F0%A0%AE%B7) サロゲートペア

DBとの通信時に変換エラーが発生する

WebページがUTF-8の場合、数値文字参照ではなく文字そのものがブラウザからサーバーへ送信される。サーバーサイドではJDBCドライバーがDBのコードセットに応じて変換を行う。文字によってはSELECTのタイミングで CharConversionException(MalformedInputException) が発生する。

// スタックトレースの最後の部分のみ抜粋
Caused by: com.ibm.db2.jcc.am.SqlException: [jcc][t4][1065][12306][4.25.13] java.io.CharConversionException をキャッチしました。 詳しくは、添付の Throwable を参照してください。 ERRORCODE=-4220, SQLSTATE=null
at com.ibm.db2.jcc.am.b6.a(b6.java:794)
at com.ibm.db2.jcc.am.b6.a(b6.java:66)
// ・・・中略・・・
Caused by: java.nio.charset.MalformedInputException: Input length = 1
at java.nio.charset.CoderResult.throwException(CoderResult.java:274)
at com.ibm.db2.jcc.am.x.a(x.java:52)
at com.ibm.db2.jcc.am.bh.a(bh.java:2952)
... 50 more

-Ddb2.jcc.charsetDecoderEncoder=3というオプションを付与すると、この例外発生を回避できる。変換エラーが発生した文字は、REPLACEMENT CHARACTER(\uFFFD�)へ自動変換される
- https://www-01.ibm.com/support/docview.wss?uid=swg22005262
- https://www-01.ibm.com/support/docview.wss?uid=swg21973226

なおCENT SIGN(U+00A2)は、-Ddb2.jcc.charsetDecoderEncoder=3の設定有無に関わらず単純に文字が消える。ある文字がそれぞれの文字コードに含まれているかはこちらのサイトで確認できる(今回の場合はCENT SIGNはデータベースが利用しているcp943には含まれていないことがわかる)

U+00A2
http://www.fileformat.info/info/unicode/char/00a2/charset_support.htm

上記の表の文字(a,&,¢,¢,—,―,-,−,∥,‖,~,〜,¦,¦,①,㈱,あ,ア,髙,㐂,彅,繫,葛,葛?,パ,パ,?,?)をWebページから入力し、DBへ格納して再度Webページへ表示した結果は以下の通り。

UTF-8を利用する場合、―(U+2015)などの一部の文字で文字化けが発生する。-Ddb2.jcc.ccsid943Mapping=2をJVMオプションに付与することで変換先を変更できるが、文字化けを完全に解消することはできない。Unicode上の複数のコードポイントをibm-943上では単一のコードに割り当てていることから避けることができない。WebページがShift_JISの場合に文字化けが発生していないように見えるが、前述の通り数値文字参照が利用されているためである。
- https://www-01.ibm.com/support/docview.wss?uid=jpn1J1008522
- https://www-01.ibm.com/support/docview.wss?uid=jpn1J1011940

ユーザーの入力 Unicodeコードポイント DBへINSERTしてSELECTした結果(WebページがShift_JISの場合) DBへINSERTしてSELECTした結果(WebページがUTF-8、-Ddb2.jcc.ccsid943Mapping=2なしの場合) DBへINSERTしてSELECTした結果(WebページがUTF-8、-Ddb2.jcc.ccsid943Mapping=2ありの場合)
a U+0061 a a a
& U+0026 & & &
¢ U+00A2 数値文字参照として扱われる 文字が消える 文字が消える
U+FFE0
U+2014 ― (HORIZONTAL BAR, U+2015)へ化ける
U+2015 — (EM DASH, U+2014)へ化ける
U+FF0D − (MINUS SIGN, U+2212)へ化ける
U+2212 - (FULLWIDTH HYPHEN-MINUS, U+FF0D)へ化ける
U+2225 ‖ (DOUBLE VERTICAL LINE, U+2016)へ化ける
U+2016 ∥ (PARALLEL TO, U+2225)へ化ける
U+FF5E 〜 (WAVE DASH, U+301C)へ化ける
U+301C ~ (FULLWIDTH TILDE, U+FF5E)へ化ける
U+FFE4 ¦(BROKEN BAR, U+00A6)へ化ける
¦ U+00A6 ¦ ¦ ¦ (FULLWIDTH BROKEN BAR, U+FFE4)へ化ける
U+2460
U+3231
U+3042
U+FF71
U+9AD9
U+3402 数値文字参照として扱われる ��(REPLACEMENT CHARACTER, U+FFFD)へ化ける ��(REPLACEMENT CHARACTER, U+FFFD)へ化ける
U+5F45
U+7E6B 数値文字参照として扱われる 繋(U+7E4B)へ化ける 繋(U+7E4B)へ化ける
U+845B 葛へ化ける 葛へ化ける
葛 ? U+845B U+E0100 葛?(一部が数値文字参照となる) 葛 �� へ化ける 葛 �� へ化ける
U+30D1
パ U+30CF U+309A パ(一部が数値文字参照となる) ハ �� へ化ける ハ �� へ化ける
? U+1F600 数値文字参照として扱われる �� へ化ける �� へ化ける
? U+20BB7 数値文字参照として扱われる � へ化ける � へ化ける

参考

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

令和の始まりにHello, Reiwa!と叫ぶ

はじめに

Hello, World! をもじった、ただのネタ記事です。

プログラム

C

HelloReiwa.c
#include <stdio.h>
int main() {
    printf("Hello, Reiwa!\n");
    return 0;
}

Java

HelloReiwa.java
public class HelloReiwa {
    public static void main(String[] args) {
        System.out.println("Hello, Reiwa!");
    }
}

Perl

HelloReiwa.pl
print "Hello, Reiwa!\n";

PHP

HelloReiwa.php
<?php
echo "Hello, Reiwa!\n";
?>

Python

HelloReiwa.py
print("Hello, Reiwa!")

Ruby

HelloReiwa.rb
puts "Hello, Reiwa!"

まとめ

ありがとう平成。こんにちは令和。
令和の時代が、幸多き時代になりますように。

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

JCA 使い方メモ

JCA とは

Java Cryptography Architecture(Java 暗号化アーキテクチャ)の略。
Java で暗号技術を使うための API、フレームワーク。

環境

Java

openjdk 11.0.2

OS

Windows 10 (64bit)

前提知識

JCA を使うには、暗号技術についての基礎知識(どういう技術があるのか、どういう仕組なのかとか)が必要になる。
これを知っておかないと、クラス構成の意味や正しい使い方が理解できない恐れがある。
最悪、実際の利用シチュエーションでは問題のある使い方をしてしまい、セキュリティホールを埋め込んでしまうかもしれない。

なので、まずはそもそもの暗号技術について勉強しておく必要がある。

暗号技術についての説明は こちら を参照。

事前定義している関数

検証には jshell を利用している。
よく利用する処理はあらかじめ関数として定義しておいて、特に説明することなく利用している。

事前定義している関数
import java.nio.*
import java.nio.file.*
import java.io.*

// バイト配列を 16 進数表記の文字列に変換する
String toHexString(byte[] bytes) {
    ByteBuffer buffer = ByteBuffer.wrap(bytes);
    StringBuilder sb = new StringBuilder();
    for (int i=0; i<bytes.length; i+=4) {
        sb.append(String.format("%08x", buffer.getInt()));
    }
    return sb.toString();
}

// バイト配列をファイルに出力する
void writeFile(String path, byte[] bytes) throws IOException {
    Files.write(Paths.get(path), bytes);
}

// ファイルをバイト配列として読み取る
byte[] readFile(String path) throws IOException {
    return Files.readAllBytes(Paths.get(path));
}

注意事項

動作検証は JShell で行っているため、記述を簡略化するために入出力ストリームの close() は省略しています。
実際に利用する場合は、 close() するのをお忘れなきよう。

JCA の基本的な仕組み

設計思想

JCA は次のことを実現することを基本方針として設計されている。

  1. 実装の独立性と相互操作性
  2. アルゴリズムの独立性と拡張性

実装の独立性

JCA が提供する様々な暗号技術(暗号化・ハッシュ関数・デジタル署名・etc...)には、様々な事情(米国の法律や新しい暗号技術の登場などなど)により複数の実装から構成されている。
この、暗号技術の実装を提供するコンポーネントのことを暗号化サービス・プロバイダ(もしくは単にプロバイダ)と呼ぶ。

実装の独立性とは、これらの実装を提供するプロバイダの存在を意識しなくて済むようにすることを指している。

つまり、「~~暗号を使いたいから、それをサポートしている・・・プロバイダを指定する」みたいなことをすることなく、「~~暗号を使いたい」とだけ宣言すれば、あとは JCA がよしなにサポートしているプロバイダを見つけて実装クラスを解決してくれるようになっている。

実装の相互操作性

実装の相互操作性とは、複数のプロバイダを組み合わせられることを指している。

つまり、ハッシュ関数の実装には~~プロバイダを使い、乱数生成には・・・プロバイダを使い、鍵生成には***プロバイダ、デジタル署名と暗号化には@@@プロバイダを使う、みたいなことができるようになっている。

アルゴリズムの独立性

普通、1つの暗号技術には複数のアルゴリズムが存在している。
例えば、ハッシュ関数(暗号技術)であれば SHA-1, SHA-2, SHA-3, MD5 など複数のアルゴリズムが存在する。

アルゴリズムの独立性とは、具体的なアルゴリズムに依存しないように暗号技術を使えることを指している。

例えばハッシュ関数については MessageDigest というクラスで抽象化されている。
このクラスには特定のアルゴリズムに依存しない API が定義されている。
MessageDigest のインスタンスを生成するときにアルゴリズムを指定する必要があるが、その後のハッシュ化の処理はアルゴリズムに関係なく同じ実装で実現できるようになっている。

アルゴリズムの拡張性

アルゴリズムの拡張性とは、新しい暗号技術のアルゴリズムが発明され実装されたときに、それを簡単に追加して使えるようにすることを指している。

暗号化サービス・プロバイダ

1つ以上の暗号技術の実装を提供するコンポーネントを暗号化サービス・プロバイダと呼ぶ。
JCA の文脈で単に「プロバイダ」と言った場合、それは暗号化サービス・プロバイダのことを指している。

JDK には最低1つのプロバイダがインストールされている。
プロバイダは静的にも動的に追加できるようになっている。

実際に JDK にデフォルトでインストールされているプロバイダについては、以下に説明がある(Oracle の Java の資料だから、 OpenJDK だと違う可能性がある?)。
JDKプロバイダ・ドキュメント

プロバイダの検索

特定の暗号技術に関するクラスのインスタンスを生成する場合、アルゴリズムを指定してインスタンスを生成する。

例えば MessageDigest のインスタンスを SHA-256 のアルゴリズムを指定して生成する場合は次のように実装する。

jshell
jshell> import java.security.*

jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

すると、 JCA は裏でインストール済みのプロバイダに対して1つずつ「SHA-256 の MessageDigest をサポートしているか?」と問い合わせる。
そして、最初に見つかったサポートしているプロバイダから具体的な実装を取得するようになっている。

これにより、アプリケーションは具体的なプロバイダを意識することなく暗号技術を利用できるようになる。
一応プロバイダの名前を指定してインスタンスを取得することも可能だが、推奨はされていない。

実行環境にインストールされているプロバイダを確認する

プロバイダを表すクラスとして java.security.Provider というクラスが用意されている。

現在の実行環境にインストールされている全ての Provider は、 java.security.SecuritygetProviders() で取得できる。

jshell
jshell> import java.util.stream.*

jshell> Stream.of(Security.getProviders()).forEach(System.out::println)
SUN version 11
SunRsaSign version 11
SunEC version 11
SunJSSE version 11
SunJCE version 11
SunJGSS version 11
SunSASL version 11
XMLDSig version 11
SunPCSC version 11
JdkLDAP version 11
JdkSASL version 11
SunMSCAPI version 11
SunPKCS11 version 11

ドキュメントとして参照したい場合は 4 JDKプロバイダ・ドキュメント あたりを見ればいいと思う。

エンジン・クラス

特定の暗号技術を提供するクラスのことを、エンジン・クラスと呼ぶ。

具体的なエンジン・クラスには、次のようなものがある(一部のみ)。

クラス 提供する機能
SecureRandom 暗号用に予測不可能性を備えた乱数生成
MessageDigest 暗号用のハッシュ関数
Signature デジタル署名の作成と検証
Cipher 暗号化と復号
Mac メッセージ認証コード
KeyGenerator 秘密鍵の生成
KeyPairGenerator 鍵ペアの生成
KeyStore 鍵を管理するキーストア

インスタンスの取得

jshell
jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

エンジン・クラスには getInstance() という static なファクトリメソッドが用意されている。
このファクトリメソッドの引数にアルゴリズムの名前を指定することで、そのアルゴリズムを実装したインスタンスを取得できる。

なお、アルゴリズムの名前は大文字小文字を区別しない

仕様上サポートされているアルゴリズム

各エンジン・クラスには、仕様上サポートしなければならないアルゴリズムが存在している。

どのアルゴリズムがサポート必須となっているかは、各エンジン・クラスの Javadoc に記載されている。

たとえば MessageDigest クラスであれば Java 11 の時点で次の3つが必須となっている。

  • MD5
  • SHA-1
  • SHA-256

MessageDigest (Java SE 11 & JDK 11 )

実行環境のプロバイダがサポートしているアルゴリズムを確認する

jshell
jshell> var provider = Security.getProviders()[0]
provider ==> SUN version 11

jshell> var services = provider.getServices()
services ==> [SUN: SecureRandom.DRBG -> sun.security.provider. ... ImplementedIn=Software}
]

jshell> services.stream().map(s -> s.getType() + ": " + s.getAlgorithm()).forEach(System.out::println)
SecureRandom: DRBG
SecureRandom: SHA1PRNG
Signature: SHA1withDSA
Signature: NONEwithDSA
Signature: SHA224withDSA
Signature: SHA256withDSA
Signature: SHA1withDSAinP1363Format
Signature: NONEwithDSAinP1363Format
Signature: SHA224withDSAinP1363Format
Signature: SHA256withDSAinP1363Format
KeyPairGenerator: DSA
MessageDigest: MD2
MessageDigest: MD5
MessageDigest: SHA
MessageDigest: SHA-224
MessageDigest: SHA-256
MessageDigest: SHA-384
MessageDigest: SHA-512
MessageDigest: SHA-512/224
MessageDigest: SHA-512/256
MessageDigest: SHA3-224
MessageDigest: SHA3-256
MessageDigest: SHA3-384
MessageDigest: SHA3-512
AlgorithmParameterGenerator: DSA
AlgorithmParameters: DSA
KeyFactory: DSA
CertificateFactory: X.509
KeyStore: PKCS12
KeyStore: JKS
KeyStore: CaseExactJKS
KeyStore: DKS
Policy: JavaPolicy
Configuration: JavaLoginConfig
CertPathBuilder: PKIX
CertPathValidator: PKIX
CertStore: Collection
CertStore: com.sun.security.IndexedCollection

プロバイダが提供する個々の暗号技術機能を表すクラスとして、 java.security.Provider.Service というクラスが用意されている。
この Service は、 Provider に用意されている各種 Getter メソッドから取得できる。

Service にはサポートする暗号技術に関する情報を取得するメソッドが用意されている。
例えば、getType() からは "MessageDigest" のようにそのサービスがサポートする暗号技術の種類を取得でき、
getAlgorithm() メソッドからは、 "SHA-256" のように具体的なアルゴリズムの名前を取得できる。

ドキュメントとして参照したい場合は、 Javaセキュリティ標準アルゴリズム名 を見ればいいと思う。

具体的なプロバイダごとにサポートしているアルゴリズムを確認したい場合は、 JDKプロバイダ・ドキュメント を見ればいいと思う。

アルゴリズムに依存しない API

エンジン・クラスは、そのクラスが提供する暗号技術に関する API を、アルゴリズムに依存しない形で提供している。

つまり、 SHA-256 でも MD5 でも、 MessageDigest を利用すれば同じ実装でハッシュ値を生成できるようになっている。

jshell
// MD5 でハッシュ値を計算
jshell> var md5 = MessageDigest.getInstance("MD5")
md5 ==> MD5 Message Digest from SUN, <initialized>

jshell> md5.update("hoge".getBytes())

jshell> md5.digest()
$7 ==> byte[16] { -22, 112, 62, 122, -95, -17, -38, 0, 100, -22, -91, 7, -39, -24, -85, 126 }

// SHA-256 でハッシュ値を計算
jshell> var sha256 = MessageDigest.getInstance("SHA-256")
sha256 ==> SHA-256 Message Digest from SUN, <initialized>

jshell> sha256.update("hoge".getBytes())

jshell> sha256.digest()
$10 ==> byte[32] { -20, -74, 102, -41, 120, 114, 94, -55, 115, 7, 4, 77, 100, 43, -12, -47, 96, -86, -69, 118, -11, 108, 0, 105, -57, 30, -94, 91, 30, -110, 104, 37 }

↑は、 jshell で MD5 と SHA-256 でハッシュ値を生成している例になる。
MessageDigest のインスタンスを生成するときに指定しているアルゴリズムが異なるだけで、ハッシュ値を計算している部分の実装はどちらのアルゴリズムも同じ形になっている。

パッケージが2つに別れている理由

JCA が提供するクラスは、大きく javax.crypto パッケージと java.security パッケージに分かれて提供されている。

これには歴史的な理由がある。
アメリカはかつて、暗号の輸出を厳しく制限していた時期があった。

アメリカ合衆国からの暗号の輸出規制 - Wikipedia

JCA のパッケージ構成はこの規制に対応したものとなっている。
java.security パッケージには輸出可能な技術に関するクラス(MessageDigestSignature)が入れられ、 javax.crypto パッケージには輸出できない技術に関するクラス(CipherKeyAgreement)が入れられている。

プロバイダもこれらに合わせて分けられていたようで、 SUN プロバイダjava.security で提供している機能を、 SunJCE プロバイダjavax.crypto で提供している機能をそれぞれ実装しているっぽい。

規制が厳しかった頃は、 SunJCE は拡張機能として提供されていたらしいが、現在は規制が緩和され JDK にバンドルされるようになっている。

参考:Javaの暗号化 | 1. 一般的なセキュリティ | セキュリティ開発者ガイド

メッセージダイジェスト(ハッシュ)

ハッシュ関数を使うためには java.security.MessageDigest クラスを使用する。

jshell
jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

jshell> byte[] hash = md.digest("hello world".getBytes())
hash ==> byte[32] { -71, 77, 39, -71, -109, 77, 62, 8, -91 ...  -84, -30, -17, -51, -23 }

digest() メソッドにハッシュ化したい値の byte 配列を渡すと、ハッシュ値が byte 配列で返る。

入力を分割する

jshell
jshell> md.update("hello".getBytes())

jshell> md.update(" world".getBytes())

jshell> var hash = md.digest()
hash ==> byte[32] { -71, 77, 39, -71, -109, 77, 62, 8, -91 ...  -84, -30, -17, -51, -23 }

update() メソッドを使うと、入力を複数に分割できる。

もし一度に全ての byte を入力しなければならないとすると、一旦すべてのデータを byte 配列にしなければならないということになる。
ハッシュ対象のデータサイズが小さい場合は問題ないが、大容量のファイルなどをハッシュ化したい場合は全てのデータをメモリ上に展開することになるため厳しくなる。
そういう場合は、入力データをちょっとずつ update() に渡すことで全てのデータを一度にメモリ上に読み込まなくても済むようになる。

digest() を実行すると MessageDigest の状態は初期化されるので、インスタンスは再利用できる。

16進数の文字列に変換する

digest() の結果は byte 配列なので、そのままだと分かりづらい。
よくハッシュ値の文字列表現として 16進数の文字列が利用されるので、その変換をしてみる。

jshell
// ※toHashString() は、事前定義している関数(ページトップを参照)
jshell> toHexString(hash)
$6 ==> "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"

ファイルのハッシュ値を計算する

ファイルからハッシュ値を計算する場合、自力で FileInputStream からデータを取り出して MessageDigest に入力する方法もあるが、より簡単に実装できるようにするためのクラスが用意されている。

OpenJDK 12 の Windows 版 zip ファイルの、 SHA-256 のハッシュ値を実際に計算してみる。

jshell
jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

// ファイルの InputStream を生成
jshell> var in = new BufferedInputStream(new FileInputStream("openjdk-12_windows-x64_bin.zip"))
in ==> java.io.BufferedInputStream@10bbd20a

// DigestInputStream を生成
jshell> var dis = new DigestInputStream(in, md)
dis ==> [Digest Input Stream] SHA-256 Message Digest from SUN, <initialized>

// InputStream から全ての情報を読み取る(読み取り結果は必要ないので nullOutputStream() に捨てる)
jshell> dis.transferTo(OutputStream.nullOutputStream())
$31 ==> 196405895

// ハッシュ値を計算
jshell> var hash = md.digest()
hash ==> byte[32] { 53, -88, -48, 24, -12, 32, -5, 5, -2,  ... -13, -119, 124, 78, -110 }

DigestInputStream は、ストリームからデータを読み取るたびに、読み取ったデータが MessageDigestupdate() に渡される。

jshell
jshell> var md = MessageDigest.getInstance("SHA-256")
md ==> SHA-256 Message Digest from SUN, <initialized>

// ファイルの InputStream を生成
jshell> var in = new BufferedInputStream(new FileInputStream("openjdk-12_windows-x64_bin.zip"))
in ==> java.io.BufferedInputStream@5a1c0542

// DigestOutputStream を生成(書き込まれた情報は必要ないので nullOutputStream() に捨てる)
jshell> var dos = new DigestOutputStream(OutputStream.nullOutputStream(), md)
dos ==> [Digest Output Stream] SHA-256 Message Digest from SUN, <initialized>

// InputStream から全ての情報を読み取り、 DigestOutputStream に書き出す
jshell> in.transferTo(dos)
$24 ==> 196405895

// ハッシュ値を計算
jshell> var hash = md.digest()
hash ==> byte[32] { 53, -88, -48, 24, -12, 32, -5, 5, -2,  ... -13, -119, 124, 78, -110 }

一方 DigestOutputStream は、ストリームにデータを書き出すたびに、書き出したデータが MessageDigestupdate() に渡される。

ちなみに、計算したハッシュ値を 16 進数文字列に変換すると、

jshell
jshell> toHexString(hash)
$12 ==> "35a8d018f420fb05fe7c2aa9933122896ca50bd23dbd373e90d8e2f3897c4e92"

OpenJDK のサイトで公開されているハッシュ値35a8d018f420fb05fe7c2aa9933122896ca50bd23dbd373e90d8e2f3897c4e92 なので、ちゃんと同じ値が計算できている。

暗号技術には様々な「鍵」が存在する(秘密鍵、公開鍵、プライベート鍵、etc...)。
JCA も、それに合わせて様々な鍵を表す型を定義している(SecretKey, PublicKey, PrivateKey, etc...)。

鍵を表す型は様々だが、これらは全て1つの共通のインターフェースを親に持っている。
それが Key インターフェースで、「鍵」を表す最上位の型となる。

Key には、鍵の情報にアクセスするためのメソッドが3つ定義されている。

  1. getAlgorithm()
    • AES や RSA といった、鍵に関連するアルゴリズム名を取得する
  2. getEncoded()
    • 鍵を特定の形式(X.509 や PKCS8 など)にエンコードした値を取得する
  3. getFormat()
    • エンコードの名前を取得する

鍵仕様

Key からはアルゴリズムなどの情報は取得できるが、鍵を構成する具体的なデータは取得できない。
このように具体的な鍵データにアクセスできないことを、 JCA では不透明な表現と呼んでいる。

一方で、鍵を構成する具体的なデータにアクセスできる型も用意されている。
それが KeySpec (鍵仕様)となる。

KeySpec 自体は「この型は鍵仕様である」ということを表現することが目的なので、実際に鍵データにアクセスするための API は定義されていない。
実際の API は、 KeySpec の実装クラスで定義されている。

例えば、 DSAPrivateKeySpec には、鍵生成で使用された素数 $p$ や非公開鍵 $x$ の情報にアクセスできるようになっている。

このように、鍵を構成する具体的なデータにアクセスできることを JCA では 透明な表現 と呼んでいる。

実際の KeyKeySpec および一部のクラス階層は次のような感じになっている。

jca.png

Generator と Factory

鍵を作るための手段として、 JCA には GeneratorFactory の2つが用意されている。

Generator は、鍵を新規に作成する機能を提供する。
例えば、鍵長のようなパラメータを指定して新しい鍵を生成できる。

一方 Factory は、主に鍵と鍵仕様を相互変換する機能を提供する。
鍵によっては異なる2つの鍵仕様から同じ鍵を生成できるものもある(らしい)。

具体的には次のようなクラスが存在する。

暗号化/復号

共通鍵暗号および公開鍵暗号による暗号化/復号処理は、どちらも javax.crypto.Cipher クラスを使用する。

Cipher のアルゴリズム指定

jshell
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher ==> Cipher.AES/CBC/PKCS5Padding, mode: not initialize ... orithm from: (no provider)

CiphergetInstance() に渡す文字列は単純なアルゴリズム名ではなく、ブロック暗号のモードなども指定できるようになっている。

具体的な書式は algorithm/mode/padding となる。

algorithm には AES や DES, RSA などの暗号化アルゴリズムの名前を指定する。
mode は、ブロック暗号のモードで、 ECB や CBC などを指定する。
padding は、暗号化対象のデータがブロックサイズの整数倍でなかったときに、足りない分をパディングする方法を指定する。

なお Cipher.getInstance("AES") のように algorithm だけを指定することも可能となっている。
この場合、モードとパディングはプロバイダが決めているデフォルト値が利用される。
多くの場合、デフォルトのモードは ECB になってしまうため、モードとパディングは常に明示しておいたほうが良い。

初期化

Cipher を使い始めるためには、まず先に初期化をしなければならない。

初期化には、「これから行う処理の種類」と「そのためのパラメータ」を指定する。
たとえば「暗号化」と「鍵」などを渡して初期化することになる。

jshell
// 鍵を生成
jshell> var keyGen = KeyGenerator.getInstance("AES")
keyGen ==> javax.crypto.KeyGenerator@365185bd

jshell> var key = keyGen.generateKey()
key ==> javax.crypto.spec.SecretKeySpec@fffe87d2

// Cipher の初期化
jshell> cipher.init(Cipher.ENCRYPT_MODE, key)

// 初期化パラメータの取得
jshell> var params = cipher.getParameters()
params ==>
    iv:
[0000: F1 D7 23 45 DA A6 7B 42   66 AF ...  46  ..#E...Bf.z..a.F
]

初期化には init() メソッドを使う。

第一引数には初期化の種類を指定する。
初期化の種類は、 Cipher に定義されている定数を使用する。
ENCRYPT_MODE は暗号化モードで、 DECRYPT_MODE は復号モードになる。

第二引数以降には初期化のパラメータを渡す。
基本は暗号化/復号で使う鍵を指定する。

暗号化のアルゴリズムによっては追加のパラメータが必要になる場合もある。
その場合は、 AlgorithmParameterSpec など追加のパラメータを受け取る init() メソッドを使用する。
AlgorithmParameterSpec は、アルゴリズム固有のパラメータを表すインターフェースで、様々な暗号化アルゴリズム固有の実装クラスが用意されている。

追加パラメータが必要なアルゴリズムでも、プロバイダがよしなにデフォルト値を設定してくれることがある。
その場合は、処理モードと鍵を受け取る init() メソッドだけで初期化ができる。
このとき使用されたパラメータは、 getParameters() メソッドで取得できる。

jshell
// Cipher の初期化
jshell> cipher.init(Cipher.ENCRYPT_MODE, key)

// 初期化パラメータの取得
jshell> var params = cipher.getParameters()
params ==>
    iv:
[0000: F1 D7 23 45 DA A6 7B 42   66 AF ...  46  ..#E...Bf.z..a.F
]

ここでは AES 用の鍵を生成して、 Cipher を暗号化モードで初期化している。

Cipher 生成時のモードを CBC にしていたため、本来は初期化ベクトル(IV : Initialization Vector)のパラメータが必要となる。
しかし、 init() で初期化ベクトルの指定を省略していたため、プロバイダがよしなに初期化ベクトルを生成してくれている。

初期化ベクトルは復号モードの初期化でも必要になるので getParameters() で取得しておく必要がある。
仮に使用した暗号化アルゴリズムが追加のパラメータを必要としなかった場合は、 getParameters()null を返す。

初期化を行うと、 Cipher は内部状態が全てリセットされる。
つまり、一度暗号化に使った Cipher のインスタンスを、今度は復号に再利用することができる。
逆にいうと、暗号化の途中で復号モードに初期化してしまうと、暗号化処理の途中で設定していた情報が失われるので注意が必要。

暗号化/復号

jshell
jshell> cipher.doFinal("Java Cryptography Architecture".getBytes())
$63 ==> byte[32] { -24, 54, 9, 79, -2, 118, 101, 69, -63, 30, -104, 77, -21, -24, -28, -4, 31, -56, -125, -121, 24, -115, 55, -92, -68, -35, -6, 90, -108, -122, -16, 21 }

doFinal() メソッドに暗号化/復号したい値(byte 配列)を渡すことで、暗号文/平文(byte 配列)を得ることができる。

MessageDigest と同様で、入力は update() を使って分割することもできる。
ただし、 MessageDigest の場合とは異なり暗号化/復号の結果は update() のたびに返される。

jshell
jshell> cipher.update("Java ".getBytes())
$59 ==> byte[0] {  }

jshell> cipher.update("Cryptography ".getBytes())
$60 ==> byte[16] { -24, 54, 9, 79, -2, 118, 101, 69, -63, 30, -104, 77, -21, -24, -28, -4 }

jshell> cipher.update("Architecture".getBytes())
$61 ==> byte[0] {  }

jshell> cipher.doFinal()
$62 ==> byte[16] { 31, -56, -125, -121, 24, -115, 55, -92, -68, -35, -6, 90, -108, -122, -16, 21 }

さらにブロック暗号を使っている場合、入力サイズがブロックのサイズになるまでは空の byte 配列が返される。
AES の場合、ブロックのサイズは 128 ビット(16 バイト)なので、入力が 16 バイトに達するまでは update() の戻り値は空配列になる。

doFinal() が呼ばれると、 Cipher のインスタンスは直前の初期化時に戻る。
つまり、そのまま別の入力を暗号化/復号するのに再利用できる。

CipherInputStream/CipherOutputStream

DigestInputStream/DigestOutputStream と同じように、 CipherInputStream/OutputStream を連携させたクラスが用意されている。

それぞれのデータと暗号化/復号の流れは、下図のようなイメージになる。

jca.jpg

まずは CipherInputStream を使った場合。

jshell
// 暗号化対象となる InputStream を作成
jshell> var is = new ByteArrayInputStream("Java Cryptography Architecture".getBytes())
is ==> java.io.ByteArrayInputStream@5fcd892a

// CipherInputStream を作成
jshell> var cis = new CipherInputStream(is, cipher)
cis ==> javax.crypto.CipherInputStream@b9afc07

// 出力先の OutputStream を作成
jshell> var out = new ByteArrayOutputStream()
out ==>

// InputStream から全情報を取り出し、暗号化結果を OutputStream に書き出す
jshell> cis.transferTo(out)
$67 ==> 32

// OutputStream に書き出された結果を確認
jshell> out.toByteArray()
$68 ==> byte[32] { -24, 54, 9, 79, -2, 118, 101, 69, -63, 30, -104, 77, -21, -24, -28, -4, 31, -56, -125, -121, 24, -115, 55, -92, -68, -35, -6, 90, -108, -122, -16, 21 }

次に CipherOutputStream を使った場合。

jshell
// 暗号化対象となる InputStream を作成
jshell> var is = new ByteArrayInputStream("Java Cryptography Architecture".getBytes())
is ==> java.io.ByteArrayInputStream@133e16fd

// 暗号文の出力先となる OutputStream を作成
jshell> var out = new ByteArrayOutputStream()
out ==>

// CipherOutputStream を作成
jshell> var cos = new CipherOutputStream(out, cipher)
cos ==> javax.crypto.CipherOutputStream@51b279c9

// 暗号化対象を CipherOutputStream に流し込む
jshell> is.transferTo(cos)
$92 ==> 30

// doFinal() を実行させるため CipherOutputStream を close()
jshell> cos.close()

// 暗号文を確認
jshell> out.toByteArray()
$94 ==> byte[32] { -24, 54, 9, 79, -2, 118, 101, 69, -63, 30, -104, 77, -21, -24, -28, -4, 31, -56, -125, -121, 24, -115, 55, -92, -68, -35, -6, 90, -108, -122, -16, 21 }

CipherInputStreamCipherOutputStream は、どちらも close() 時に CipherdoFinal() が実行される。
なので、 close() は忘れずに実行しておかないと結果が中途半端になったりする1(ストリームなので、基本 try-with-resources などを使うから気にする必要はないと思うが)。

transferTo() は使うべきでない?

CipherInputStreamJavadoc には次のように書かれている。

このクラスを使用するプログラマは、このクラスで定義されていないメソッド、またはオーバーライドされていないメソッド(あとでスーパー・クラスのいずれかに追加された新しいメソッドやコンストラクタなど)を絶対に使用しないでください。それらのメソッドの設計と実装では、CipherInputStreamに関するセキュリティ上の影響が考慮されていない可能性があるためです。

一方、 InputStream には Java 9 で transferTo() という便利なメソッドが追加されている。

この transferTo()CipherInputStream でオーバーライドされていないので、使うべきでない条件を満たしてしまっている。

ただ、この transferTo() は OpenJDK 11 では次のように実装されている。

InputStream.java
    public long transferTo(OutputStream out) throws IOException {
        Objects.requireNonNull(out, "out");
        long transferred = 0;
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int read;
        while ((read = this.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
            out.write(buffer, 0, read);
            transferred += read;
        }
        return transferred;
    }

ここでは、 InputStreamread()OutputStreamwrite() だけが使われている。

もし transferTo() を使わずに処理を書こうとしても、結局この transferTo() がやっていることと同じ実装を書くことになる。
そして、そのとき使用するメソッドは CipherInputStream がオーバーライドしている read()CipherOutputStreamwrite() になる。

となると、結局 transferTo() を使っても問題ないと個人的には思う。

ただし、この実装はあくまで OpenJDK に限った話なので、他の Java 実装だとセキュリティ上問題のある実装になっている可能性もゼロではないかもしれない。

ご利用は自己責任で。

AES

AES の暗号化と復号を実装してみる。

鍵の生成

jshell
// アルゴリズムを AES にして KeyGenerator を作成
jshell> var keyGen = KeyGenerator.getInstance("AES")
keyGen ==> javax.crypto.KeyGenerator@4d50efb8

// 鍵の生成
jshell> var key = keyGen.generateKey()
key ==> javax.crypto.spec.SecretKeySpec@177c6

鍵の生成には KeyGenerator を使用する。
SunJCE プロバイダでは、デフォルトの鍵長は 128 ビットになる。

鍵長の指定

jshell
jshell> keyGen.init(256)

init() メソッドで鍵長を指定できる。指定可能な鍵長は 128, 192, 256 のいずれかのみ(AES の仕様)。

暗号化

jshell
// Cipher を AES で生成
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher ==> Cipher.AES/CBC/PKCS5Padding, mode: not initialize ... orithm from: (no provider)

// 暗号化モードで初期化
jshell> cipher.init(Cipher.ENCRYPT_MODE, key)

// 初期化ベクトルの情報を取得
jshell> var params = cipher.getParameters()
params ==>
    iv:
[0000: 88 8E 06 A0 89 80 0D 11   FF ED ...  45  ...........&.'.E
]

// 暗号化
jshell> var c = cipher.doFinal("Java Cryptography Architecture".getBytes())
c ==> byte[32] { -35, -80, -57, 91, -75, -46, -123, 49, ...  22, 90, -121, 124, -108 }

// 暗号化できているか確認
jshell> new String(c)
$24 ==> "ンーヌ[オメ?1juセエヒ「リ\017}初ョPW」\026Z?|?"

復号時に初期化ベクトルの情報が必要になるので、 getParameters() でパラメータの情報を取得しておく。

復号

jshell
// 復号モードで初期化
jshell> cipher.init(Cipher.DECRYPT_MODE, key, params)

// 復号
jshell> var p = cipher.doFinal(c)
p ==> byte[30] { 74, 97, 118, 97, 32, 67, 114, 121, 112 ... , 99, 116, 117, 114, 101 }

// 復号できているか確認
jshell> new String(p)
$23 ==> "Java Cryptography Architecture"

init() のときに、鍵だけでなく暗号化の初期化で使用したパラメータも指定しなければならない。

鍵をファイルに出力する

jshell
// 鍵をファイルに出力
jshell> writeFile("secret-key", key.getEncoded())

// 暗号化時に使用したパラメータをファイルに出力
jshell> writeFile("aes-params", params.getEncoded())

// 暗号文の生成
jshell> var c = cipher.doFinal("Hello World!!".getBytes())
c ==> byte[16] { -77, 124, 124, -4, -92, 80, -89, 102,  ... 1, 114, 7, 117, -104, 11 }

// 暗号文をファイルに出力
jshell> writeFile("aes-cryptogram", c)

秘密鍵のエンコードされたデータは KeygetEncoded() で取得できる。
また、パラメータの情報は AlgorithmParametersgetEncoded() で取得できる。

鍵をファイルから復元する

jshell
// 秘密鍵の情報をファイルから読み取り、 SecretKeySpec を生成する
jshell> Key key = new SecretKeySpec(readFile("secret-key"), "AES")
key ==> javax.crypto.spec.SecretKeySpec@178a8

// AlgorithmParameters のインスタンスを取得し、ファイルから読み込んだ情報で初期化する
jshell> var params = AlgorithmParameters.getInstance("AES")
params ==>

jshell> params.init(readFile("aes-params"))

// Cipher を生成して読み込んだ情報で初期化
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher ==> javax.crypto.Cipher@df27fae

jshell> cipher.init(Cipher.DECRYPT_MODE, key, params)

// 暗号文をファイルから読み取り、復号
jshell> var m = cipher.doFinal(readFile("aes-cryptogram"))
m ==> byte[13] { 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 33 }

// 結果を確認
jshell> new String(m)
$34 ==> "Hello World!!"

秘密鍵は SecretKeySpec を使って復元できる(アルゴリズムは "AES" を指定)。
このクラスは KeySpec でありながら Key も実装しているため、このまま鍵として利用できる。

パラメータは AlgorithmParameters で復元できる。
まずは getInstance()"AES" を指定してインスタンスを取得する。
そして、 init() でパラメータのデータを設定する。

パスワードベース暗号(PBE)

jshell
// パスワードベース暗号用の KeySpec を生成
jshell> char[] password = { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }
password ==> char[8] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }

jshell> var keySpec = new PBEKeySpec(password)
keySpec ==> javax.crypto.spec.PBEKeySpec@5a8e6209

// SecretKeyFactory を使って KeySpec から Key を生成
jshell> var keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_128")
keyFac ==> javax.crypto.SecretKeyFactory@3234e239

jshell> var key = keyFac.generateSecret(keySpec)
key ==> com.sun.crypto.provider.PBEKey@855e49d8

PBE では、 PBEKey という鍵を使用する。

PBEKey は、パスワードをもとに PBEKeySpec を生成し、SecretKeyFactory を使って PBEKey に変換して取得する。

パスワードは char の配列で指定しなければならない。
これは、 PBEKeySpec の Javadoc に理由が書いてあるが、 String だと値が不変であるためあとでクリアができないかららしい。

SecretKeyFactory のインスタンス取得では、アルゴリズムに PBEWithHmacSHA256AndAES_128 を指定している。

アルゴリズム名は PBEWith<digest|prf>And<encryption> という書式になっている。
digest|prf が MD5 や SHA1 の場合、 RFC8018 でいうところの PBES1 になり、 Hmac* の場合は PBES2 を使うことになる。

jshell
// パスワードベース暗号用の Cipher を生成して暗号化モードで初期化
jshell> var cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_128/CBC/PKCS5Padding")
cipher ==> javax.crypto.Cipher@5891e32e

Cipher も同じアルゴリズムでインスタンスを取得する。
(SunJCE プロバイダの Cipher が PBE でサポートしているモードとパディングは CBC/PKCS5Padding のみなので明示的に指定しなくてもよさそうな気がするが、念の為)

jshell
// 疑似乱数生成器の作成
jshell> var random = new SecureRandom()
random ==> Hash_DRBG,SHA-256,128,reseed_only

// salt の生成
jshell> var salt = new byte[64];
salt ==> byte[64] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... , 0, 0, 0, 0, 0, 0, 0, 0 }

jshell> random.nextBytes(salt)

ソルトを生成する。

ソルトは暗号論的疑似乱数生成器(SecureRandom)を使って生成する。

RFC8018 ではソルトのサイズを最低 64 ビット以上としているので、とりあえず 64 ビットで生成した。

jshell
// イテレーション回数の宣言
jshell> int iterationCount = 1000
iterationCount ==> 1000

// パスワードベース暗号用の AlgorithmParameterSpec を生成
jshell> var keyParamSpec = new PBEParameterSpec(salt, iterationCount)
keyParamSpec ==> javax.crypto.spec.PBEParameterSpec@eec5a4a

jshell> cipher.init(Cipher.ENCRYPT_MODE, key, keyParamSpec)

イテレーション回数とソルトの値を使って、 PBEParameterSpec を生成する。

そして、先程作った鍵とともに Cipher の初期化を行う。

jshell
// 暗号化実行
jshell> var c = cipher.doFinal("Hello World!!".getBytes())
c ==> byte[16] { 101, -52, -106, 26, 67, 118, -20, 22,  ...  42, -71, 115, 123, -122 }

// 暗号結果を確認
jshell> new String(c)
$18 ==> "eフ?\032Cv?\026\031ャ\000*ケs{?"

あとは、他のアルゴリズムと同じように暗号化を行う。

jshell
// 暗号化時に使用したパラメータを取得
jshell> var params = cipher.getParameters()
params ==> PBEWithHmacSHA256AndAES_128

jshell> PBEParameterSpec pbeParamSpec = params.getParameterSpec(PBEParameterSpec.class)
pbeParamSpec ==> javax.crypto.spec.PBEParameterSpec@35a50a4c

// IvParameterSpec を取得
jshell> IvParameterSpec ivSpec = (IvParameterSpec)pbeParamSpec.getParameterSpec()
ivSpec ==> javax.crypto.spec.IvParameterSpec@281e3708

// IV (初期化ベクトル)の値を確認
jshell> var iv = ivSpec.getIV()
iv ==> byte[16] { -67, 47, 111, -2, 34, -17, -89, 74, -7 ... 46, -124, 99, 79, 23, 88 }

CBC で暗号化したので、復号時のために初期化ベクトルを取得しておく必要がある。
暗号化時に自動設定された初期化ベクトルの情報(IvParameterSpec)は、 PBEParameterSpecgetParameterSpec() メソッドで取得できる。

jshell
// 復号用に PBEParameterSpec を生成
jshell> var ivSpec = new IvParameterSpec(iv)
ivSpec ==> javax.crypto.spec.IvParameterSpec@dbd940d

jshell> PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, iterationCount, ivSpec)
pbeParamSpec ==> javax.crypto.spec.PBEParameterSpec@17695df3

// 復号モードで初期化(※key の生成は暗号化時と同じ)
jshell> cipher.init(Cipher.DECRYPT_MODE, key, pbeParamSpec)

// 復号を実行
jshell> var m = cipher.doFinal(c)
m ==> byte[13] { 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 33 }

// 復号結果を確認
jshell> new String(m)
$21 ==> "Hello World!!"

復号時は、再び「パスワード」「ソルト」「イテレーション回数」の情報を使って KeyAlgorithmParameterSpec を生成し、 Cipher を初期化する。
(IV の情報は暗号化時のものを使う必要があるので、 PBEParameterSpec の生成は若干異なっている)

RSA

公開鍵暗号の RSA を使用してみる。

鍵の生成

jshell
// 鍵ペアを生成するための KeyPairGenerator を取得
jshell> var keyPairGen = KeyPairGenerator.getInstance("RSA")
keyPairGen ==> java.security.KeyPairGenerator$Delegate@42dafa95

// 鍵ペアを生成
jshell> var keyPair = keyPairGen.generateKeyPair()
keyPair ==> java.security.KeyPair@1dfe2924

公開鍵暗号の鍵ペアを生成するには、 KeyPairGenerator を使用する。
RSA を使うので、アルゴリズム名は "RSA" を渡す。

generateKeyPair() を実行すると、公開鍵とプライベート鍵のセットを含んだ KeyPair を取得できる。

鍵長の指定

jshell
jshell> keyPairGen.initialize(4096)

jshell> keyPairGen.generateKeyPair().getPublic()
$9 ==> Sun RSA public key, 4096 bits
  params: null
  modulus: 58218231801097229661983999673649965349728454764714757172625245890343045...
  public exponent: 65537

KeyPairGeneratorinitialize() メソッドで鍵長を指定できる。

暗号化

jshell
// RSA 用の Cipher を取得
jshell> var cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
cipher ==> javax.crypto.Cipher@35a50a4c

// 暗号化モードで初期化
jshell> cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic())

// 暗号化実行
jshell> var c = cipher.doFinal("Hello World!!".getBytes())
c ==> byte[256] { 44, 48, -56, -42, -117, -48, 17, -76, ... , -15, 63, 46, -115, -42 }

// 暗号結果を確認
jshell> new String(c)
$14 ==> ",0ネヨ巾\021エ\030?0\022:リ斃w?9ァ,萬|ノ_?轍モォノmトЩア]\002錥)マ\024L5XD?齡ツル訶({x?シ\024,5、S@wM-?杣ウg1ン\025「02鷦ヘラトJ盆ミ?\tゥレmフF悊^?\"ミウ\023ゥ&モホ輙モ瓸コ.Mri\037クi?9ムEDケレgg\023q亥X4\021pM3廱ヤ塗覆ホ\017G\024億ア\017ソ?;ィ Q\177冬U5wgL゙B浅簗涓ミ>\006e?g?9lエ醋棒:\t]垉R\020ソ?nミ?!?&sエ?ォラ\016`H\f6漬?レ?ュ4\nfU呶?.斎"

KeyPairgetPublic() で公開鍵を取得できるので、これで暗号化を行う。
手順は AES の場合と一緒。

復号

jshell
// 暗号化時に生成されたパラメータを取得
jshell> var params = cipher.getParameters()
params ==> MD: SHA-256
MGF: MGF1SHA-1
PSource: PSpecified

// 復号モードで初期化
jshell> cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate(), params)

// 復号実行
jshell> var m = cipher.doFinal(c)
m ==> byte[13] { 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33, 33 }

// 復号結果を確認
jshell> new String(m)
$18 ==> "Hello World!!"

KeyPairgetPrivate() でプライベート鍵が取得できるので、それを使って復号を行う。
こちらも、 Cipher の使い方は AES の場合と同じ感じ。

メッセージ認証コード(MAC)

jshell
// MAC 用に秘密鍵を生成する
jshell> var keyGen = KeyGenerator.getInstance("HmacSHA256")
keyGen ==> javax.crypto.KeyGenerator@17579e0f

jshell> var key = keyGen.generateKey()
key ==> javax.crypto.spec.SecretKeySpec@588390c

// MAC のインスタンスを生成する
jshell> var mac = Mac.getInstance("HmacSHA256")
mac ==> javax.crypto.Mac@7a765367

// MAC を初期化する
jshell> mac.init(key)

// MAC 値を計算する
jshell> var mv = mac.doFinal("Hello World!!".getBytes())
mv ==> byte[32] { 0, -103, -60, 37, -74, 97, 115, -50, 7 ... 80, 57, -84, -59, 90, 23 }

MAC 値の計算には Mac クラスを使用する。

MAC には秘密鍵が必要になるので、 KeyGenerator を使って鍵を生成する。

Mac のインスタンスを生成したら、まずは秘密鍵を使って init() メソッドで初期化を行う。
次に、 doFinal() に MAC 値を計算したい値(byte 配列)を渡す。
計算された MAC 値は、 byte 配列として返される。

分割して計算する

jshell
jshell> mac.update("Hello ".getBytes())

jshell> mac.update("World!!".getBytes())

jshell> mac.doFinal()
$9 ==> byte[32] { 0, -103, -60, 37, -74, 97, 115, -50, 77, 6, 107, 27, 15, -41, -15, 111, -99, -50, -85, -68, 11, -66, 69, -54, 44, -92, 80, 57, -84, -59, 90, 23 }

Cipher などと同様に、 update() を使って計算対象の値を分割して設定できる。

デジタル署名

RSA 署名

jshell
// RSA の鍵ペアを生成する
jshell> var keyPairGen = KeyPairGenerator.getInstance("RSA")
keyPairGen ==> java.security.KeyPairGenerator$Delegate@42dafa95

jshell> var keyPair = keyPairGen.generateKeyPair()
keyPair ==> java.security.KeyPair@1dfe2924

まずは、 RSA 暗号のときと同じように RSA の鍵ペアを生成する。

jshell
// Signature インスタンス取得
jshell> var signature = Signature.getInstance("SHA256WithRSA")
signature ==> Signature object: SHA256WithRSA<not initialized>

// 署名モードで初期化
jshell> signature.initSign(keyPair.getPrivate())

// 署名対象のデータを登録
jshell> signature.update("Hello World!!".getBytes())

// デジタル署名を生成
jshell> var sign = signature.sign()
sign ==> byte[256] { 29, -91, 95, 8, -19, -56, 118, -29, 1 ... 7, -1, 53, 58, -99, -117 }

デジタル署名を作成するには、 Signature クラスを使用する。

Signature は2つの初期化メソッドを持っている。
1つが initSign() で、もう1つが initVerify() になる。

署名を作成する場合は、 initSign() を使って初期化を行う。
このとき、引数には署名で使用する署名鍵(プライベート鍵)を渡す。

初期化が済んだら、次は update() メソッドで署名対象のデータを登録していく。
最後に sign() メソッドを実行すると、デジタル署名が byte 配列で返される。

jshell
// Signature を検証モードで初期化
jshell> signature.initVerify(keyPair.getPublic())

// 検証対象のデータを登録
jshell> signature.update("Hello World!!".getBytes())

// 検証を実行
jshell> signature.verify(sign)
$12 ==> true

検証を行う場合は、 initVerify()Signature を初期化する。
このとき、引数には検証鍵(公開鍵)を渡す。

こちらも update() で検証対象のデータを登録する。
そして、最後に verify() メソッドで検証を実行する。
このとき、引数にはデジタル署名の byte 配列を渡す。

検証が成功した場合は true が返る。

DSA 署名

jshell
// DSA 用の鍵ペアを生成
jshell> var keyPairGen = KeyPairGenerator.getInstance("DSA")
keyPairGen ==> sun.security.provider.DSAKeyPairGenerator$Current@f6c48ac

jshell> var keyPair = keyPairGen.generateKeyPair()
keyPair ==> java.security.KeyPair@1f36e637

// DSA 用の Signature インスタンスを取得
jshell> var signature = Signature.getInstance("SHA256WithDSA")
signature ==> Signature object: SHA256WithDSA<not initialized>

// 署名モードで初期化
jshell> signature.initSign(keyPair.getPrivate())

// 署名対象データを登録
jshell> signature.update("Hello World!!".getBytes())

// デジタル署名を生成
jshell> var sign = signature.sign()
sign ==> byte[63] { 48, 61, 2, 29, 0, -72, 46, -94, 42, 12 ... 4, 59, -38, -5, -48, 127 }

// 検証モードで初期化
jshell> signature.initVerify(keyPair.getPublic())

// 検証対象データを登録
jshell> signature.update("Hello World!!".getBytes())

// 検証を実行
jshell> signature.verify(sign)
$12 ==> true

アルゴリズム名などを DSA 用に変えただけで、実装の方法は RSA の場合と同じ感じでできる。

keytool

keytool | Java Platform, Standard Editionツール・リファレンス

JDK には keytool というコマンドラインツールが同梱されている。
keytool を使うと、鍵や証明書を生成したり管理することができる。

keytoolのヘルプ
$ keytool -h
キーおよび証明書管理ツール

コマンド:

 -certreq            証明書リクエストを生成します
 -changealias        エントリの別名を変更します
 -delete             エントリを削除します
 -exportcert         証明書をエクスポートします
 -genkeypair         鍵ペアを生成します
 -genseckey          秘密鍵を生成します
 -gencert            証明書リクエストから証明書を生成します
 -importcert         証明書または証明書チェーンをインポートします
 -importpass         パスワードをインポートします
 -importkeystore     別のキーストアから1つまたはすべてのエントリをインポートします
 -keypasswd          エントリの鍵パスワードを変更します
 -list               キーストア内のエントリをリストします
 -printcert          証明書の内容を出力します
 -printcertreq       証明書リクエストの内容を出力します
 -printcrl           CRLファイルの内容を出力します
 -storepasswd        キーストアのストア・パスワードを変更します

このヘルプ・メッセージを表示するには"keytool -?、-hまたは--help"を使用します
command_nameの使用方法については、"keytool -command_name --help"を使用します。
事前構成済のオプション・ファイルを指定するには、-conf <url>オプションを使用します。

コマンドの書式

keytool <コマンド> <オプション>

keytool は、実行するコマンドと、そのオプションを渡すことで利用できる。
コマンドとオプションは、全て -名前 という形式になっている。

コマンドの一覧は -h で確認できる。
さらに各コマンドのヘルプは、そのコマンドのオプションに -h を渡すことで詳細を確認できる。

-certreqコマンドのヘルプ
$ keytool -certreq -h
keytool -certreq [OPTION]...

証明書リクエストを生成します

オプション:

 -alias <alias>          処理するエントリの別名
 -sigalg <alg>           署名アルゴリズム名
 -file <file>            出力ファイル名
 -keypass <arg>          鍵のパスワード
 -keystore <keystore>    キーストア名
 -dname <name>           識別名
 -ext <value>            X.509拡張
 -storepass <arg>        キーストアのパスワード
 -storetype <type>       キーストアのタイプ
 -providername <name>    プロバイダ名
 -addprovider <name>     名前でセキュリティ・プロバイダを追加する(SunPKCS11など)
   [-providerarg <arg>]    -addproviderの引数を構成する
 -providerclass <class>  完全修飾クラス名でセキュリティ・プロバイダを追加する
   [-providerarg <arg>]    -providerclassの引数を構成する
 -providerpath <list>    プロバイダ・クラスパス
 -v                      詳細出力
 -protected              保護メカニズムによるパスワード

このヘルプ・メッセージを表示するには"keytool -?、-hまたは--help"を使用します

キーストア

keytool はキーストアと呼ばれるファイルに鍵や証明書などの情報を保存する。
キーストアは、 Java 8 までは JKS という Oracle 独自のファイルフォーマットで作成されていた。
Java 9 以降は PKCS#12 という規格(RFC7292)で定義されているファイルフォーマットで作成される。

keytool の各コマンドは -keystore というオプションを受け取るようになっている。
このオプションで、キーストアのファイルの場所を指定する。
未指定の場合は、デフォルトで ホームディレクトリ/.keystore がキーストアファイルのパスとして使用される。

コマンドの一覧を見るとわかるが、キーストアを新規作成するための専用のコマンドは存在しない。
キーストアファイルは、最初に鍵などを生成しようとしたときに、ファイルが無ければ自動的に作成されるようになっている。

キーストア自体はパスワードベース暗号で暗号化されていて、中身にアクセスするためにはパスワードの入力が必要になる。
キーストアのパスワードは、そのキーストアを最初に作成するときに入力を求められる。
パスワードの長さは 6 桁以上でなければならない。

キーストアの情報は、Java プログラムからもアクセスできるようになっている。
そのためのクラスが KeyStore になる(詳細後述)。

エントリ

キーストア内に格納する鍵や証明書は、総称してエントリと呼ばれる。

キーストアは一種のキーバリューストアになっていて、各エントリにはエイリアスと呼ばれる名前をつけて管理する。
特定のエントリの情報を表示・編集するためには、エイリアスでエントリを指定することになる。

エントリの種類

エントリは2種類存在する。

  • 鍵のエントリ
  • 信頼された証明書のエントリ

「鍵のエントリ」は、秘密鍵か、プライベート鍵とそのペアとなる公開鍵証明書のセットを格納したエントリになる。
公開鍵証明書は自己署名証明書の場合もあれば、ルート証明書までチェーンしている場合もある。

一方「信頼できる証明書のエントリ」には単独の公開鍵証明書が格納される。
他の公開鍵によって署名されている場合も、このエントリに格納される公開鍵証明書は1つだけになる(チェーンはしていない)。

キーストアの中身を確認する

一覧の表示

$ keytool -list
キーストアのタイプ: PKCS12
キーストア・プロバイダ: SUN

キーストアには8エントリが含まれます

hogekey,2019/04/10, SecretKeyEntry,
pbekey,2019/04/10, SecretKeyEntry,
rsakey,2019/04/13, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): CB:D1:0C:85:5F:1D:50:CC:62:C6:68:11:9F:7E:7D:4D:F7:1B:45:40:44:71:99:B3:41:2B:71:7A:E1:A6:02:FB
dsakey,2019/04/13, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): E9:70:1D:B2:8C:8B:18:C2:7C:A4:A0:DC:32:A5:37:06:ED:1B:DF:30:65:32:77:B5:43:77:4D:9D:42:15:70:B2
qiita-cert,2019/04/15, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 57:28:2C:9D:D0:43:19:08:A4:CC:D3:52:CF:5F:32:16:EC:9D:DA:4A:4E:D1:5C:1F:3A:EB:39:3F:76:A3:91:D4

-list コマンドでエントリの一覧を確認できる。

PrivateKeyEntry と書いてるのが、プライベート鍵と公開鍵のペアが格納されたエントリになる(鍵エントリ)。
SecretKeyEntry と書いてるのが、秘密鍵のエントリになる(こちらも鍵エントリ)。
trustedCertEntry と書いてるのが、信頼できる証明書のエントリになる。

エントリを指定して表示

$ keytool -list -alias hogekey
hogekey,2019/04/10, SecretKeyEntry,

$ keytool -list -alias rsakey
rsakey,2019/04/13, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): CB:D1:0C:85:5F:1D:50:CC:62:C6:68:11:9F:7E:7D:4D:F7:1B:45:40:44:71:99:B3:41:2B:71:7A:E1:A6:02:FB

-alias オプションで個別に表示したいエントリのエイリアスを指定すると、そのエイリアスの情報だけを出力できる。

さらに -v オプションをつけることで詳細な情報を出力できる。

$ keytool -list -alias hogekey -v
別名: hogekey
作成日: 2019/04/10
エントリ・タイプ: SecretKeyEntry

$ keytool -list -alias rsakey -v
別名: rsakey
作成日: 2019/04/13
エントリ・タイプ: PrivateKeyEntry
証明書チェーンの長さ: 1
証明書[1]:
所有者: CN=Alice, C=JP
発行者: CN=Alice, C=JP
シリアル番号: 6a59c918
有効期間の開始日: Sat Apr 13 08:41:13 JST 2019終了日: Fri Jul 12 08:41:13 JST 2019
証明書のフィンガプリント:
         SHA1: 1F:11:0E:8C:30:0B:DA:D8:2C:79:AD:C7:4B:2B:70:62:85:94:94:CA
         SHA256: CB:D1:0C:85:5F:1D:50:CC:62:C6:68:11:9F:7E:7D:4D:F7:1B:45:40:44:71:99:B3:41:2B:71:7A:E1:A6:02:FB
署名アルゴリズム名: SHA256withRSA
サブジェクト公開鍵アルゴリズム: 2048ビットRSA鍵
バージョン: 3

拡張:

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 92 68 47 49 D0 26 A7 1D   04 51 64 3E 96 5B B3 05  .hGI.&...Qd>.[..
0010: 40 7B 3B E2                                        @.;.
]
]

キーストアのパスを変更する

$ keytool -storepasswd
新規keystore password:
新規keystore passwordを再入力してください:

-storepasswd コマンドで、キーストアのパスワードを変更できる。

エントリを削除する

$ keytool -delete -alias foo

-delete コマンドで、任意のエントリを削除できる。

削除対象のエントリは、 -alias オプションで指定する。

エントリのエイリアスを変更する

$ keytool -changealias -alias from-alias -destalias to-alias

-changealias コマンドで、任意のエントリのエイリアスを変更できる。

-alias は、変更対象のエイリアスを指定する。
-destalias は、変更後のエイリアスの名前を指定する。

JRE に組み込まれているキーストアを確認する

Java は実行環境にルート証明書を記録したキーストアを保持している。

具体的なファイルは ${JAVA_HOME}/lib/security/cacerts になる。
これもキーストアなので、 keytool を使って内容を確認したり編集したりできる。

cacertsの内容を確認する
$ keytool -list -cacerts
キーストアのパスワードを入力してください:
キーストアのタイプ: JKS
キーストア・プロバイダ: SUN

キーストアには93エントリが含まれます

verisignclass2g2ca [jdk],2018/06/13, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 3A:43:E2:20:FE:7F:3E:A9:65:3D:1E:21:74:2E:AC:2B:75:C2:0F:D8:98:03:05:BC:50:2C:AF:8C:2D:9B:41:A1
digicertassuredidg3 [jdk],2017/12/01, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 7E:37:CB:8B:4C:47:09:0C:AB:36:55:1B:A6:F4:5D:B8:40:68:0F:BA:16:6A:95:2D:B1:00:71:7F:43:05:3F:C2
verisignuniversalrootca [jdk],2017/12/01, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 23:99:56:11:27:A5:71:25:DE:8C:EF:EA:61:0D:DF:2F:A0:78:B5:C8:06:7F:4E:82:82:90:BF:B8:60:E8:4B:3C

...

証明書のフィンガプリント(SHA-256): 43:48:A0:E9:44:4C:78:CB:26:5E:05:8D:5E:89:44:B4:D8:4F:96:62:BD:26:DB:25:7F:89:34:A4:43:C7:01:61
addtrustqualifiedca [jdk],2017/12/01, trustedCertEntry,
証明書のフィンガプリント(SHA-256): 80:95:21:08:05:DB:4B:BC:35:5E:44:28:D8:FD:6E:C2:CD:E3:AB:5F:B9:7A:99:42:98:8E:B8:F4:DC:D0:60:16

デフォルトのパスワードは changeit となっている。
ちなみに、ドキュメントには「システム管理者は、SDKのインストール後、このファイルのパスワードとデフォルト・アクセス権を変更する必要があります。」と書かれている。

オプションの -cacerts を指定すると、 cacerts ファイルをキーストアとして指定したのと同じ状態になるので、 -keystore でファイルパスを指定しなくてもアクセスできる。

Java で SSL 通信などを実装すると、デフォルトではこの cacerts を使って証明書の検証が行われる。
また、後述する証明書のインポートのときにも参照される場合がある。

秘密鍵を生成する

$ keytool -genseckey -keyalg AES -keysize 256 -alias HogeKey
キーストアのパスワードを入力してください:**********
新規パスワードを再入力してください:**********

共通鍵暗号の秘密鍵を生成するには、 -genseckey コマンドを使用する。

パスワードの入力を求められているが、これはキーストアファイル自体を暗号化/復号するためのパスワードになる。
特に今回は初回アクセスだった(キーストアを初めて作成した)ので、新規パスワードの再入力も促されている。
次回以降は、パスワードの入力は一回だけになる。

-keyalg で鍵のアルゴリズムとして AES を指定し、鍵長を 256 としている。

-alias では、登録される秘密鍵につけるエイリアスを指定している。
省略可能だが、その場合は mykey という値になる。

パスワードベース鍵は生成できない?

-keyalg にパスワードベース鍵のアルゴリズムを指定してエントリを作成してみた。

$ keytool -genseckey -keyalg PBEWithHmacSHA256AndAES_128 -keysize 256 -alias pbeKey
キーストアのパスワードを入力してください:*******
保存するパスワードを入力してください:**************
パスワードを再入力してください:**************

特にエラーもなくエントリが生成できたので、成功した感じがする。
しかし、このエントリを Java プログラムから取得すると、次のようになる。

jshell
(前略)

jshell> var entry = keystore.getEntry("pbeKey", password)
entry ==> Secret key entry with algorithm PBEWithMD5AndDES

アルゴリズムが PBEWithMD5AndDES になってる。。。
正式なサポートはされていないということだろうか?

公開鍵暗号の鍵ペアを生成する

$ keytool -genkeypair -keyalg RSA -keysize 2048 -dname CN=Alice,C=JP -alias rsakey

公開鍵暗号の鍵ペアを生成するには、 -genkeypair コマンドを使用する。

共通鍵暗号のときと同様で、 -keyalg で鍵のアルゴリズムを、 -keysize で鍵長(ビット数)を指定できる。
(DSA の鍵ペアを作るなら、 -keyalgDSA と指定すればいい)

-genkeypair では、公開鍵は自己署名証明書(X.509)の形で生成される。
-dname では、証明書の発行者(issuer)と所有者(subject)に設定される X.500 識別名を指定する。
-dname の指定を省略した場合は、コマンドラインから X.500 識別名の入力を求められる。

X.500 識別名

X.500 識別名は、 CN=Amazon, OU=Server CA 1B, O=Amazon, C=US のように <属性名>=<値> をカンマで区切った形で記述する。
属性名には次の値を指定できる。

属性名 意味
CN 一般名(common name)
所有者の名前など
OU 部門名(organization unit)
部や課などの名前
O 組織名(organization name)
会社名など
L 地域名(locality name)
都市名など
S 地方名(state name)
州名など(日本なら都道府県レベル?)
C 国コード(country)
国を識別する2桁のコード(JP, US など)

実際の例をいくつか。

確認したサイト subjectの値
Qiita
https://qiita.com/
CN=qiita.com
日本オラクル
https://www.oracle.com/jp/index.html
CN=www-cs-01.oracle.com
OU=Content Management Services IT
O=Oracle Corporation
L=Redwood Shores
S=California
C=US
Amazon
https://www.amazon.co.jp/
CN=www.amazon.co.jp
O=Amazon.com, Inc.
L=Seattle
S=Washington
C=US
内閣府
https://www.cao.go.jp/
CN=*.cao.go.jp
O=Cabinet Office
L=Chiyoda-ku
S=Tokyo
C=JP
Panasonic
https://panasonic.jp/
CN=panasonic.jp
O=Panasonic Corporation
L=Kadoma
S=Osaka
C=JP

各属性名は、全てを指定する必要はない。
ただし、順序には制約があり、 CN, OU, O, L, S, C の順に並んでいなければならない。

-dname オプションで指定する場合、空白スペースを含めたい場合はオプションの値を引用符(")で囲う必要がある。

-dnameを引用符で囲う例
$ keytool ... -dname "CN=Alice, S=Osaka Fu, C=JP"

また、半角カンマは属性を区切る特別な意味を持つので、属性値に半角含めたい場合はバックスラッシュでエスケープする必要がある。

属性値に半角カンマを使用する
$ keytool ... -dname CN=Hoge\,Fuga

証明書の有効期間

証明書の有効期間は、次の2つのオプションで制御できる。

  • -startdate
  • -validity

-startdate は、証明書の有効期間の開始日時になる。
-validity は、証明書の有効期間を日数で指定する。

つまり、 -startdate で指定した日時から -validity で指定した日数の間が、証明書の有効期間になる。

-startdate のデフォルト値は、鍵を作成したときの実行日時で、 -validity のデフォルト値は 90 となっている。
したがって、証明書のデフォルトの有効期間は、鍵ペアを作成した日時から 90 日間ということになる。

-startdate は、相対的な方法と絶対的な方法のいずれかで指定できる。

相対的な方法では、実行日時からの相対的な時間で開始日時を指定する。
具体的には次のように記述する。

相対的な開始日時の指定
$ keytool ... -startdate +10y-1m+5d+8H

この指定は、 +10y で「プラス10年」、-1m で「マイナス1ヶ月」、+5d で「プラス5日」、+8H で「プラス8時間」を表している。

相対指定の書式は ([+-]nnn[ymdHMS])+ となっている。
つまり、年・月・日・時・分・秒をそれぞれプラス/マイナスでどれくらいシフトさせるかで指定する。

一方、絶対指定は次のように指定する。

絶対的な開始日時の指定
$ keytool ... -startdate "2019/12/01 12:00:00"

こちらは見たまんま。

書式としては、 [yyyy/mm/dd] [HH:MM:SS] となる。
省略した方は実行日時が利用される。

つまり、年月日だけを指定した場合は、時分秒が実行時間になり、
時分秒だけを指定した場合は、年月日が実行日になる。

証明書を出力する

$ keytool -exportcert -alias alice-key -file alice-cert
証明書がファイル<alice-cert>に保存されました

-exportcert コマンドを使うと、指定したエントリにある証明書を X.509 形式で出力できる。
(秘密鍵のエントリは証明書がないので指定できない)

デフォルトは標準出力に出力される。
ファイルに出力するには -file オプションを指定する。

出力形式は、デフォルトはバイナリ形式となる。
PEM 形式にするには、 -rfc オプションを指定する。

PEM形式で出力する例
$ keytool -exportcert -alias alice-key -rfc
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIEFj7+/DANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJj
...略...
7ticcWecmUspMZ2dx/lO
-----END CERTIFICATE-----

証明書ファイルの内容を表示する

$ keytool -printcert -file alice-cert
所有者: CN=alice
発行者: CN=ca
シリアル番号: 163efefc
有効期間の開始日: Sat Apr 13 11:45:48 JST 2019終了日: Fri Jul 12 11:45:48 JST 2019
証明書のフィンガプリント:
         SHA1: 07:E5:76:25:83:D1:C2:C5:66:C3:87:9E:E9:7A:B0:E8:1C:07:63:83
         SHA256: 55:ED:F8:4B:CC:3F:C9:A9:3F:5D:D8:B3:CC:51:33:58:3E:36:4A:10:06:1F:E7:19:94:25:73:66:30:E7:85:EF
署名アルゴリズム名: SHA256withRSA
サブジェクト公開キー・アルゴリズム: 2048ビットRSAキー
バージョン: 3

拡張:
...

-printcert コマンドを -file オプションを指定して実行すると、指定した証明書ファイルの中身を確認できる。
-file を指定しない場合は、デフォルトで標準入力から情報を読み取る。

デフォルトは、上述のように人間が読みやすい形式で出力される。
PEM 形式で出力するには -rfc オプションを指定する。

PEM形式で出力した場合
$ keytool -printcert -file alice-cert -rfc
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIEFj7+/DANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJj
...略...
7ticcWecmUspMZ2dx/lO
-----END CERTIFICATE-----

-printcert コマンドはキーストアに関係ないので、 -keystore を指定せずに実行できる。

任意の SSL サーバーの証明書を表示する

$ keytool -printcert -sslserver qiita.com
Certificate #0
====================================
所有者: CN=qiita.com
発行者: CN=Amazon, OU=Server CA 1B, O=Amazon, C=US
シリアル番号: 793de44ec0e815de65a3fbb3e35a1e2
有効期間の開始日: Sun Mar 31 09:00:00 JST 2019終了日: Thu Apr 30 21:00:00 JST 2020
証明書のフィンガプリント:
         SHA1: C0:D8:EE:56:3B:9F:68:22:0B:36:F3:9E:2A:D3:69:4E:3C:1D:61:44
         SHA256: 57:28:2C:9D:D0:43:19:08:A4:CC:D3:52:CF:5F:32:16:EC:9D:DA:4A:4E:D1:5C:1F:3A:EB:39:3F:76:A3:91:D4
署名アルゴリズム名: SHA256withRSA
サブジェクト公開キー・アルゴリズム: 2048ビットRSAキー
バージョン: 3

...

Certificate #3
====================================
所有者: CN=Starfield Services Root Certificate Authority - G2, O="Starfield Technologies, Inc.", L=Scottsdale, ST=Arizona, C=US
発行者: OU=Starfield Class 2 Certification Authority, O="Starfield Technologies, Inc.", C=US
シリアル番号: a70e4a4c3482b77f
...

-printcert コマンドに -sslserver オプションをつけて実行すると、指定した SSL サーバーの証明書を出力できる。

-sslserver にはホストとポートを host:port の形で指定できる。
ポート番号は省略可能で、デフォルトは 443 になる。

プロキシ環境下で実行する場合は、 -J-Dhttps.proxyHost-J-Dhttps.proxyPort オプションでプロキシの情報を指定する(例:-J-Dhttps.proxyHost=proxy.host.name -J-Dhttps.proxyPort=8080)。

こちらもデフォルトは人間が読める形式になる。
PEM 形式にするには、 -rfc オプションを指定する。

証明書ファイルとしてローカルに保存したい場合は、 -rfc オプションをつけたうえで出力結果をファイルにリダイレクトすればいい(-file は入力ファイルのオプションなので、これを指定してもファイルには保存されない)。

証明書署名要求を作成する

$ keytool -certreq -alias alice-key
-----BEGIN NEW CERTIFICATE REQUEST-----
MIICkDCCAXgCAQAwGzELMAkGA1UEBhMCSlAxDDAKBgNVBAMTA1RvbTCCASIwDQYJ
...略...
wX7FTUBXCD8CjXFxosmZ97YJf7Xy89cmdArgVNE9T9pJJpht
-----END NEW CERTIFICATE REQUEST-----

-certreq コマンドを使うと、証明書署名要求を作成できる。

-alias オプションには、対象となる公開鍵エントリのエイリアスを指定する。

デフォルトは標準出力に出力される。
ファイルに出力したい場合は、リダイレクトするか -file オプションで出力先のファイルを指定する。

証明書署名要求をファイルに出力する
$ keytool -certreq -alias alice-key -file alice-certreq

証明書署名要求から証明書を生成する

# キーストアの内容を確認
$ keytool -list
...

キーストアには2エントリが含まれます

ca-key,2019/04/13, PrivateKeyEntry,
...
alice-key,2019/04/13, PrivateKeyEntry,
...

# 証明書署名要求ファイルから証明書を生成
$ keytool -gencert -infile alice-certreq -alias ca-key -outfile alice-cert

-gencert コマンドを使うと、証明書署名要求ファイルから証明書(X.509 形式)を生成できる。

上の例は、証明書署名要求ファイル alice-certreq に対して、キーストア内に存在する鍵ペアエントリ ca-key を使って証明書を生成している。

-infile は、処理対象となる証明書署名要求ファイルを指定する。
未指定の場合は標準入力から入力する。

-alias では、証明書の署名に使用する鍵のエントリを指定する。

-outfile は、結果の証明書を出力するファイルを指定している。
未指定の場合は標準出力に出力される。

デフォルトでは、証明書はバイナリ形式で出力される。
-rfc オプションを指定すると、 PEM 形式で出力できる。

-rfcオプションを指定した場合
$ keytool -gencert -infile alice-certreq -alias ca-key -rfc
-----BEGIN CERTIFICATE-----
MIIC2zCCAcOgAwIBAgIEF+xeBDANBgkqhkiG9w0BAQsFADANMQswCQYDVQQDEwJj
...略...
es0hcACS0jLw0VoJYjSB
-----END CERTIFICATE-----

証明書を取り込む

証明書を取り込むには -importcert コマンドを使用する。

インポートできるデータの形式は、 PKCS#7 形式の証明書チェーンか X.509 証明書の単独またはシーケンスになる。

このコマンドには2つの機能が存在する。

  1. 証明書応答のインポート
  2. 信頼できる証明書のインポート

-importcert コマンドでは、条件によってこのいずれかの機能が実行される。

次の条件を満たした場合は、証明書応答のインポートとして動作する。

  • -alias で指定した名前のエントリがすでにキーストアに存在する
  • そのエントリがプライベート鍵のエントリである

一方、次の条件を満たした場合は、信頼できる証明書のインポートとして動作する。

  • -alias で指定した名前のエントリがキーストアに存在しない

証明書応答のインポート

-alias で指定した名前のエントリがプライベート鍵のエントリとして既にキーストアに存在する場合、インポートしようとしている証明書は証明書応答と判断される。
つまり、その鍵エントリ内の公開鍵証明書から生成した証明書署名要求に対して、 CA が署名して返してきた証明書応答をインポートしようとしている、という扱いになる。

したがって、 keytool は鍵エントリに含まれる公開鍵証明書を、インポート対象に指定した証明書応答で置き換えるように動作する。

証明書応答の検証

インポートが実際に行われる前に、証明書応答が正当なものか検証が行われる。

検証は、大きく次の2つが行われる。

  1. 証明書応答が、確かに -alias で指定された鍵エントリに対応するものかどうか
  2. 証明書応答が信頼できるかどうか

証明書応答が -alias で指定した鍵エントリに対応するものかどうかは、証明書応答の中に鍵エントリ内の公開鍵と同じものがあるかどうかで判断される。
証明書応答の中に鍵エントリ内の公開鍵証明書が存在しないのであれば、その証明書応答は全然関係のないモノなので、エラーとなる。

jca.jpg

証明書応答が信頼できるかどうかの判定方法は、証明書応答内に存在する証明書の数によって大きく2つに分かれる。

証明書応答内に複数の証明書が含まれる場合

証明書応答内に複数の証明書が含まれる場合は、それらの証明書で有効な証明書チェーンが構築できるかどうかが試される。

まず最初に、証明書応答の中からインポート先の鍵エントリと同じ公開鍵を持つ証明書を見つけ出す。
そして、その証明書を先頭にして、署名の検証が OK となる証明書を次々と繋げていく。

jca.jpg

末尾にきた証明書が自己署名証明書だった場合、その証明書と同じ証明書がキーストア内に存在するかが確認される。
キーストア内に同じ自己署名証明書が存在した場合は、証明書チェーンは正当なものと判断され、構築された証明書チェーンが鍵エントリにインポートされる。
同じ自己署名証明書が存在しなかった場合は、インポートを続行して良いか keytool が確認を求めてくる。

一方、末尾にきた証明書が自己署名証明書でなかった場合、その署名の検証が通る証明書がキーストア内に存在しないか確認される。
キーストア内に該当する証明書が見つかった場合は、証明書チェーンの末尾にキーストア内で見つかった証明書を追加したうえで、鍵エントリに証明書チェーンがインポートされる。
該当する証明書が見つからなかった場合は、インポートを続行して良いか keytool が確認を求めてくる。

jca.jpg

証明書応答が単独の証明書の場合

証明書応答内に証明書が1つだけ存在する場合は、検証の動作が少し変わる。

この場合、 keytool はインポート先のキーストア内に存在する証明書を使って証明書チェーンが再現できないか試みる。

jca.jpg

最終的にルートまで証明書チェーンが再現できた場合は、信頼できるモノとして再現されたチェーンが鍵エントリに登録される。
チェーンが再現できなかった場合(途中で検証OKとなる公開鍵を見つけられなかった場合)、インポートを続行して良いか keytool が確認を求めてくる。

信頼できる証明書のインポート

-alias で指定したエントリがキーストア内に存在しない場合、信頼できる証明書としてインポートが行われる。

この場合、インポートしようとしているデータに証明書が複数存在しても、先頭の1つだけが取り込まれる。

このときも、証明書が正当なものか検証が行われる。
この検証は、証明書応答が単独の証明書だった場合と同じ動作になる。

つまり、キーストア内の証明書を使って証明書チェーンが再現できるかどうかで、正当な証明書かどうかが判定される。

チェーンが再現できればそのままインポートが完了する。
再現できない場合は、インポートを続行して良いか keytool が確認を求めてくる。

cacerts の証明書も検証に利用する

-importcert コマンドのオプションで -trustcacerts を指定すると、検証のときに cacerts 内のルート証明書も利用されるようになる。
(デフォルトは、対象のキーストア内の証明書だけが利用される)

実際にやってみる

# Alice のキーストアを確認
$ keytool -keystore alice.keystore -list
...
キーストアには1エントリが含まれます

alice-key,2019/04/18, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): C7:55:BB:57:C4:3F:57:02:BE:2E:57:1E:7C:6F:F4:A9:55:E5:90:92:6B:7D:DF:5F:14:FF:F2:65:16:81:E1:56

# CA のキーストアを確認
$ keytool -keystore ca.keystore -list
...
キーストアには2エントリが含まれます

middle-ca-key,2019/04/18, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): CE:79:D5:32:D0:1E:5E:3A:55:BB:3E:CD:CE:5E:5F:9F:4D:79:97:2E:E7:EF:F9:60:62:C5:D0:4E:AD:28:B4:60
root-ca-key,2019/04/18, PrivateKeyEntry,
証明書のフィンガプリント(SHA-256): C0:06:6C:5C:2C:F5:3D:8B:DB:D4:EC:19:C5:30:A0:90:71:00:CC:17:40:E8:EB:38:64:5B:8F:40:3A:D4:3F:7C

検証のために、2つのキーストアを用意した。

1つは Alice のキーストアで、現在は自作した自己署名証明書の公開鍵ペアのエントリが1つだけ存在している(alice-key)。

もう1つは CA (認証局)のキーストアで、中間認証局用の公開鍵ペア(middle-ca-key)とルート認証局用の公開鍵ペア(root-ca-key)の2つのエントリが存在している。
(ちなみに、 middle-ca-keyroot-ca-key で署名済み)

ここから、 Alice の公開鍵ペアから証明書署名要求を生成し、認証局の鍵で署名し、 Alice のキーストアにインポートしてみる。

# Alice の鍵から証明書署名要求を生成
$ keytool -keystore alice.keystore -certreq -alias alice-key -file alice.csr

# 中間認証局の鍵で署名
$ keytool -keystore ca.keystore -gencert -alias middle-ca-key -infile alice.csr -outfile alice.cer

# 生成された証明書の中身を確認
$ keytool -printcert -file alice.cer
証明書[1]:
所有者: CN=alice
発行者: CN=middle-ca
シリアル番号: 25fc2e01
...

証明書[2]:
所有者: CN=middle-ca
発行者: CN=root-ca
シリアル番号: 248f2111
...

Alice の鍵ペアから証明書署名要求を生成し、中間認証局の鍵で署名した公開鍵証明書を生成した。

中身をみると、 Alice→中間認証局の順に証明書がチェーンしていることがわかる(ルート認証局の証明書は入っていない)。

これを、 Alice の元の鍵エントリにインポートする(つまり、証明書応答としてインポートする)。

$ keytool -keystore alice.keystore -importcert -file alice.cer -alias alice-key

応答したトップレベルの証明書:

所有者: CN=middle-ca
発行者: CN=root-ca
シリアル番号: 248f2111
...

... は信頼されていません。 応答をインストールしますか。[いいえ]:

トップレベルの証明書が信頼できないと判断され、インポートして良いか確認を促された。
root-ca の証明書は Alice のキーストアには存在しないので、インポートしようとした証明書応答は信頼できないモノと判断されている。

ここは一旦インポートを中断し、先にルート認証局の証明書を Alice のキーストアにインポートする。

# ルート認証局の公開鍵証明書をエクスポート
$ keytool -keystore ca.keystore -exportcert -alias root-ca-key -file root-ca.cer
証明書がファイル<root-ca.cer>に保存されました

# Alice のキーストアにルート認証局の証明書をインポート
$ keytool -keystore alice.keystore -importcert -file root-ca.cer -alias root-ca-cert
所有者: CN=root-ca
発行者: CN=root-ca
シリアル番号: 13b64cb6
...
証明書のフィンガプリント:
         SHA1: E9:D2:EF:D2:1D:86:8F:04:50:0C:ED:DD:5B:C1:C5:DD:FE:64:77:3D
         SHA256: C0:06:6C:5C:2C:F5:3D:8B:DB:D4:EC:19:C5:30:A0:90:71:00:CC:17:40:E8:EB:38:64:5B:8F:40:3A:D4:3F:7C
...

この証明書を信頼しますか。 [いいえ]:  y
証明書がキーストアに追加されました

-alias に指定したエントリは存在しないので、 -importcert コマンドは「信頼できる証明書のインポート」として動作する。

この証明書は自己署名証明書なので、インポートする人(キーストアの管理者)が信頼するかどうか決めるしか無い。
ここは信頼するので、 y でインポートを完了させた。

実際に信頼できる証明書をインポートする場合は、画面に表示されているフィンガープリントなどの情報を見て、確かに意図した証明書をインポートしていることを注意深く確認しなければならない。

あらためて Alice の証明書応答をインポートしてみる。

# Alice の証明書応答をインポート
$ keytool -keystore alice.keystore -importcert -file alice.cer -alias alice-key
証明書応答がキーストアにインストールされました

# インポート後の Alice の鍵エントリを確認
$ keytool -keystore alice.keystore -storepass password -list -v -alias alice-key
別名: alice-key
作成日: 2019/04/18
エントリ・タイプ: PrivateKeyEntry
証明書チェーンの長さ: 3
証明書[1]:
所有者: CN=alice
発行者: CN=middle-ca
シリアル番号: 25fc2e01
...

証明書[2]:
所有者: CN=middle-ca
発行者: CN=root-ca
シリアル番号: 248f2111
...

証明書[3]:
所有者: CN=root-ca
発行者: CN=root-ca
シリアル番号: 13b64cb6
...

今度はルート認証局の証明書がインストール済みだったため、証明書チェーンの検証がすんなり成功し、そのままインストールが完了した。

また、インポート後は Alice の鍵エントリ内の公開鍵証明書に証明書チェーンが格納されているのがわかる。

KeyStore

keytool コマンドで生成・管理するキーストアファイルは、 Java のプログラムからもアクセスできるように API が用意されている。

KeyStore インスタンスを取得する

jshell
jshell> var keystore = KeyStore.getInstance("PKCS12")
keystore ==> java.security.KeyStore@3fd7a715

キーストアにアクセスするには、 KeyStore クラスを使用する。

getInstance() では、キーストアのタイプを指定する。
Java 9 以降であれば、キーストアのタイプはデフォルトで PKCS12 なので、 "PKCS12" でインスタンスを生成している。
(古いキーストアであれば JKS の可能性もあるので、その場合は "JKS" と指定する)

キーストアファイルを読み込む

jshell
jshell> char[] password = { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }
password ==> char[8] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }

jshell> keystore.load(new FileInputStream("alice.keystore"), password)

キーストアファイルを読み込むには、 load() メソッドを使う。
第一引数にはキーストアファイルの InputStream を指定し、第二引数にはキーストアファイルのパスワードを char 配列で指定する。

インスタンス取得と同時に読み込む

jshell
jshell> var keystore = KeyStore.getInstance(new File("alice.keystore"), password)
keystore ==> java.security.KeyStore@525b461a

getInstance(File, char[]) なら、 KeyStore インスタンスの生成からキーストアファイルのロードまでを一回で実行できる。

キーストアのタイプは自動判定される。

全エントリのエイリアス名を取得する

jshell
jshell> keystore.aliases().asIterator().forEachRemaining(System.out::println)
alice-key
aes-key
root-ca-cert

aliases() メソッドで、全エントリのエイリアス名を取得できる。

エイリアス名からエントリの種類を確認する

jshell
jshell> keystore.isKeyEntry("alice-key")
$12 ==> true

jshell> keystore.isCertificateEntry("alice-key")
$13 ==> false

jshell> keystore.isKeyEntry("root-ca-cert")
$14 ==> false

jshell> keystore.isCertificateEntry("root-ca-cert")
$15 ==> true

エイリアスが指すエントリが鍵エントリかどうかは isKeyEntry() で確認できる。

信頼できる証明書エントリかどうかは、 isCertificateEntry() で確認できる。

秘密鍵を取得する

jshell
jshell> var seckey = keystore.getKey("seckey", password)
seckey ==> javax.crypto.spec.SecretKeySpec@fffe8f5b

jshell> seckey.getAlgorithm()
$29 ==> "AES"

getKey() で、指定したエイリアスの鍵を秘密鍵として取得できる。

第一引数はエイリアス名を渡し、第二引数は鍵を保護しているパスワード(普通はキーストアのパスワードと同じ)を渡す。

プライベート鍵を取得する

jshell
jshell> var alicePrivateKey = keystore.getKey("alice-key", password)
alicePrivateKey ==> SunRsaSign RSA private CRT key, 2048 bits
  param ... 96220784207006016968977089

jshell> alicePrivateKey.getClass()
$38 ==> class sun.security.rsa.RSAPrivateCrtKeyImpl

公開鍵ペアの鍵エントリに対して getKey() を使うと、プライベート鍵を取得できる。

公開鍵(証明書)を取得する

jshell
// 証明書を取得
jshell> Certificate aliceCert = keystore.getCertificate("alice-key")
aliceCert ==> [
[
  Version: V3
  Subject: CN=alice
  Signature ... 3 D5  .........%$.m...

]

// 証明書から公開鍵を取得
jshell> PublicKey alicePublicKey = aliceCert.getPublicKey()
alicePublicKey ==> Sun RSA public key, 2048 bits
  params: null
  mo ... 9
  public exponent: 65537

公開鍵ペアの鍵エントリに対して getCertificate() を使うと、公開鍵証明書(java.security.cert.Certificate)を取得できる。

証明書チェーンを取得する

jshell
jshell> Certificate[] chain = keystore.getCertificateChain("alice-key")
chain ==> Certificate[3] { [
[
  Version: V3
  Subject: CN= ... C0  .;;.O...&.....N.

] }

getCertificateChain() を使うと、指定したエントリに存在する証明書チェーンを Certificate の配列で取得できる。

秘密鍵エントリを追加する

jshell
// 秘密鍵を生成
jshell> var secKey = KeyGenerator.getInstance("AES").generateKey()
secKey ==> javax.crypto.spec.SecretKeySpec@fffe806b

// 秘密鍵のエントリを生成
jshell> var secKeyEntry = new KeyStore.SecretKeyEntry(secKey)
secKeyEntry ==> Secret key entry with algorithm AES

// 秘密鍵エントリをキーストアに登録
jshell> var protection = new KeyStore.PasswordProtection(password)
protection ==> java.security.KeyStore$PasswordProtection@27d415d9

jshell> keystore.setEntry("sec-key", secKeyEntry, protection)

// 登録された秘密鍵エントリを取り出して確認
jshell> keystore.getKey("sec-key", password)
$10 ==> javax.crypto.spec.SecretKeySpec@fffe806b

キーストアにエントリを追加するには、 setEntry() を使う。

第一引数は、エントリのエイリアス。
第二引数は、登録するエントリ。
第三引数は、エントリの保護に関するパラメータを渡す。

秘密鍵のエントリは、 KeyStore.SecretKeyEntry で作成する。

パラメータは、エントリを暗号化するときに使用するパスワードを渡すことになる。
一応キーストアとは別のパスワードを指定可能だが、同じにしておくのが無難?(keytool は、キーストアのタイプが PKCS12 ならキーストアと同じパスワードを使うようになっている)

ファイルに出力する

jshell
jshell> keystore.store(new FileOutputStream("alice.keystore"), password)

store() で、指定した出力ストリームに KeyStore の内容を出力できる。

証明書

新規作成する API はない?

証明書を表すクラスとして java.security.cert.Certificate というクラスが用意されており、このインスタンスを構築するためのクラスとして java.security.cert.CertificateFactory というクラスが存在する。

しかし、 CertificateFactory は既存の証明書を読み込んで Certificate を構築するもので、ゼロから証明書を生成するものではない。

ざっと java.security.cert パッケージ を見ても、証明書を生成するようなクラスは見当たらない。

一応 Keytool の -gencert コマンドの実装を見てみたが、 sun.security.x509 パッケージ(内部API)のクラスが使われていた。

標準 API だけで証明書を生成する方法はなさげ。
keytool を使って生成するしかなさそう。

証明書を読み込む

指定可能な証明書タイプ

エンコードされた証明書を読み込むには、 CertificateFactory を使用する。

getInstance() で指定できる証明書のタイプは、標準機能としては X.509 だけで、 Javadoc なども基本的に X.509 の説明が中心となって説明されている。

CertificateFactory型 | Javaセキュリティ標準アルゴリズム名

ということで、ここでの説明も X.509 を前提として記述する。

証明書を1つずつ読み込む

jshell
// CertificateFactory を生成
jshell> var factory = CertificateFactory.getInstance("X.509")
factory ==> java.security.cert.CertificateFactory@3abbfa04

// 読み込む証明書ファイル(alice.cer)の入力ストリームを生成
jshell> var in = new FileInputStream("alice.cer")
in ==> java.io.FileInputStream@31ef45e3

// 入力ストリームから証明書の情報を読み込む
jshell> factory.generateCertificate(in)
$5 ==> [
[
  Version: V3
  Subject: CN=alice
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
...
]

jshell> factory.generateCertificate(in)
$6 ==> [
[
  Version: V3
  Subject: CN=middle-ca
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11
...
]

jshell> factory.generateCertificate(in)
|  例外java.security.cert.CertificateException: Could not parse certificate: java.io.IOException: Empty input
|        at X509Factory.engineGenerateCertificate (X509Factory.java:110)
|        at CertificateFactory.generateCertificate (CertificateFactory.java:355)
|        at (#7:1)
|  原因: java.io.IOException: Empty input
|        at X509Factory.engineGenerateCertificate (X509Factory.java:106)
|        ...

エンコードされた X.509 証明書を読み込むには、 generateCertificate() を使用する。

対象データは、 DER エンコードされたバイナリ形式のデータか、もしくは PEM 形式のデータである必要がある。

対象データに複数の証明書のデータが含まれている場合、 generateCertificate() を呼び出すごとに先頭から1つずつ証明書を読み込むことができる。
(証明書がもう存在しないのにさらに読み込もうとすると、例外がスローされる)

複数の証明書をまとめて読み込む

jshell
jshell> Collection<? extends Certificate> certs = factory.generateCertificates(new FileInputStream("alice.cer"))
certs ==> [[
[
  Version: V3
  Subject: CN=alice
  Signatur ...  70  ....$..&...2.n.p

]]

jshell> certs.size()
$12 ==> 2

generateCertificates() なら、対象データ内のすべての証明書をまとめて読み込める。

証明書の署名を検証する

jshell
// ↑で読み込んだ証明書のコレクションから、1つずつ証明書を抽出
jshell> var iterator = certs.iterator()
iterator ==> java.util.ArrayList$Itr@27808f31

// alice の証明書を取得
jshell> var aliceCert = iterator.next()
aliceCert ==> [
[
  Version: V3
  Subject: CN=alice
  Signature ... 3 D5  .........%$.m...

]

// 中間認証局の証明書を取得
jshell> var middleCaCert = iterator.next()
middleCaCert ==> [
[
  Version: V3
  Subject: CN=middle-ca
  Signa ... 0 70  ....$..&...2.n.p

]

// 中間認証局の公開鍵で、 alice の証明書を検証(検証OK)
jshell> aliceCert.verify(middleCaCert.getPublicKey())

// alice 自身の公開鍵で、 alice の証明書を検証(検証NG)
jshell> aliceCert.verify(aliceCert.getPublicKey())
|  例外java.security.SignatureException: Signature does not match.
|        at X509CertImpl.verify (X509CertImpl.java:459)
|        at X509CertImpl.verify (X509CertImpl.java:391)
|        at (#18:1)

証明書の署名を検証するには verify() を使用する。

引数には、検証で使用する公開鍵を渡す。

検証が OK であれば何も起こらず、 NG の場合は例外がスローされる。

乱数

jshell
// SecureRandom インスタンスを生成
jshell> var random = new SecureRandom()
random ==> Hash_DRBG,SHA-256,128,reseed_only

// 100 未満の乱数を取得
jshell> random.nextInt(100)
$3 ==> 91

// 512 ビットのランダムなバイト配列を生成
jshell> byte[] bytes = new byte[64]
bytes ==> byte[64] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... , 0, 0, 0, 0, 0, 0, 0, 0 }

jshell> random.nextBytes(bytes)

jshell> bytes
bytes ==> byte[64] { 97, 86, -17, 9, 81, 39, -18, -43, 40, -43, -21, 93, 112, -78, -1, 59, -5, 110, 115, 13, -86, 34, -81, -90, -107, -82, -102, -72, 19, 87, 9, -38, -91, -7, 47, -119, 103, -24, 52, -40, -3, 36, -9, -62, 10, -32, 36, 27, -74, -75, 58, -54, -124, -49, -6, 77, 78, 32, 96, 66, 67, -86, -39, -98 }

暗号用の乱数生成には、 SecureRandom を使用する。

Java には乱数を生成するためのクラスとして Random クラスも存在する。
しかし、こちらは予測不可能性を持たないため、暗号技術には利用してはいけない。

SecureRandom は、他のエンジンクラスとは異なり getInstace() 以外にも コンストラクタ でインスタンスを生成できる。

コンストラクタでインスタンスを生成した場合、優先度の高いプロバイダからアルゴリズムが選択される。

環境ごとにどのアルゴリズムが選択されるかは、 SecureRandom実装 | 4 JDKプロバイダ・ドキュメント で確認できる。

getInstance() を使うべきか、コンストラクタを使うべきか

SecureRandom のインスタンスは、他のエンジンクラス同様 getInstance() で取得することもできるし、コンストラクタで取得することもできる。
前者はアルゴリズムを明示する必要があるが、後者はあらかじめ決められた優先順位によってアルゴリズムが決定する。

どちらを使って SecureRandom のインスタンスは生成すべきなのか?

Javadoc や JCA の公式ドキュメントを見ても、明確に「こちらを使え」と書かれているところは見つけられなかった。

いろいろ調べて出した個人的な結論は、「特に理由が無いのならコンストラクタを使う」となった(あくまで個人的な結論なので、間違っているかも)。

理由は、コンストラクタを使用した方が、環境ごとに最適なアルゴリズムを選択しつつ可搬性を維持することができるから。

Unix 系の環境では、デフォルトのアルゴリズムは NativePRNG になる。
しかし、このアルゴリズムは Windows 環境では使用できない。
つまり、アルゴリズムを NativePRNG 指定にしていると、そのプログラムは Windows には移植できないことになる。
(逆に、 Windows-PRNG は Unix 系環境では使用できない)

また、 Windows 環境では Java 8 までデフォルトのアルゴリズムは SHA1PRNG だった。
しかし、 Java 9 で DRBG が追加されたことで、最優先は DRBG に置き換わった(JEP 273: DRBG-Based SecureRandom Implementations)。

DRBGNIST Special Publication 800-90A Revision 1 で説明されている疑似乱数生成方法で、 SHA1PRNG よりも強力らしい。

このようにアルゴリズムが新しいものに置き換わるといったケースにも対応しやすいので、コンストラクタを使ったほうが良いのかなと思う。
(あくまで、個人的な結論です)

getInstanceStrong()

Java 8 で getInstanceStrong() というメソッドが追加されている。

名前に Strong とあるように、強力な乱数を生成するアルゴリズムが選択される。
公開鍵暗号の鍵ペア生成など、重要な値の生成のときに利用するらしい。

例えば Unix 系環境で使用すると、具体的なアルゴリズムは NativePRNGBlocking になる。

デフォルトで選択される NativePRNG と比べると何が違うのかというと、 nextBytes()generateSeed() が、それぞれ /dev/random/dev/urandom のどちらを使用するかが異なる。

アルゴリズム nextBytes() generateSeed()
NativePRNG /dev/urandom /dev/random
NativePRNGBlocking /dev/random /dev/random
NativePRNGNonBlocking /dev/urandom /dev/urandom

SecureRandom | 表4-4 SUNプロバイダでのアルゴリズム

この /dev/random/dev/urandom は乱数を生成するための疑似デバイスで、 Unix 系の OS で利用できる。

/dev/random は、環境ノイズを収集して乱数を生成するため、真の乱数を得ることができる。
しかし、情報が十分に集まっていない状態で乱数を得ようとすると、処理がブロック(待機)させられてしまう。

一方、 /dev/urandom は収集した情報を使い回すことで、処理をブロックすることなく乱数を生成できる。
しかし、情報を再利用しているため乱数としての安全性は /dev/random に劣る。

参考

/dev/random は、安全性は高いが、代わりにプログラムの性能を低下させる恐れがある。
/dev/urandom は、安全性は劣るが、代わりにプログラムの性能は落とさずに済む。

デフォルトで選択される NativePRNG は、シードの生成には真の乱数を得られる /dev/random を使用するが、通常の乱数生成では /dev/urandom を使用している。
つまり、 NativePRNG は安全性をやや落としつつも、性能は確保できるようになっている。

一方 NativePRNGBlocking は、常に /dev/random から乱数を得るようになっている。
つまり、性能は捨てて安全性に全振りしている。

プログラムの性能よりも安全性を優先しなければならない場面では getInstanceStrong() を使い、どちらも両立しなければならない場面ではコンストラクタでデフォルト実装を使うようにすればいいのだと思う(個人の意見)。

Diffie-Hellman 鍵交換

Diffie-Hellman 鍵交換を実現するためのクラスとして、 KeyAgreement が用意されている。

KeyAgreement を用いた鍵交換の流れは、以下のような感じになる。

jca.png

以下、 Alice を表す jshell と Bob を表す jshell をそれぞれ立ち上げて、鍵の交換を試してみる(カレントディレクトリは2つとも同じ場所)。

Alice
// 鍵ペアを生成する
jshell> var aliceKeyPairGen = KeyPairGenerator.getInstance("DH")
aliceKeyPairGen ==> java.security.KeyPairGenerator$Delegate@6d7b4f4c

// 鍵ペアを生成
jshell> var aliceKeyPair = aliceKeyPairGen.generateKeyPair()
aliceKeyPair ==> java.security.KeyPair@1786dec2

// Alice の KeyAgreement を生成
jshell> var aliceKeyAgree = KeyAgreement.getInstance("DH")
aliceKeyAgree ==> javax.crypto.KeyAgreement@2c039ac6

// Alice の秘密鍵で初期化
jshell> aliceKeyAgree.init(aliceKeyPair.getPrivate())

// Alice の公開鍵をファイルに出力
jshell> writeFile("alice-dh-public-key", aliceKeyPair.getPublic().getEncoded())

// 公開鍵のフォーマットは X.509
jshell> aliceKeyPair.getPublic().getFormat()
$32 ==> "X.509"

鍵交換には、まずは KeyPairGenerator を使って鍵ペアを生成する。
アルゴリズム名には DiffieHellman か、略称の DH を指定する。

この鍵ペアの公開鍵には、素数 $p$ と原始元 $g$、そして適当に選択された乱数 $a$ から計算した値 $y = g^{a} \bmod p$ が含まれている。

KeyAgreementDiffieHellman (または DH)でインスタンスを取得し、秘密鍵で初期化しておく。

Bob
// Alice の公開鍵を読み取り
jshell> var encodedAlicePublicKey = readFile("alice-dh-public-key")
encodedAlicePublicKey ==> byte[556] { 48, -126, 2, 40, 48, -126, 1, 27, 6,  ...  70, 34, 8, -3, -119, 14 }

// Alice のエンコードされた公開鍵を復元
jshell> var bobKeyFactory = KeyFactory.getInstance("DH")
bobKeyFactory ==> java.security.KeyFactory@7a9273a8

jshell> var alicePublicKeySpec = new X509EncodedKeySpec(encodedAlicePublicKey)
alicePublicKeySpec ==> java.security.spec.X509EncodedKeySpec@e874448

jshell> var alicePublicKey = bobKeyFactory.generatePublic(alicePublicKeySpec)
alicePublicKey ==> SunJCE Diffie-Hellman Public Key:
y:
    17cc14 ...
g:
    02
l:
    1024

Alice から受け取った公開鍵は X.509 形式なので、 X509EncodedKeySpec を使って復元できる。

KeySpec から Key を生成するのは KeyFactory の役割なので、 KeyFactory を使って復元を行う。
アルゴリズム名は、 DiffieHellman または DH を指定する。

Bob
// Bob の鍵ペアを生成
jshell> var bobKeyPairGen = KeyPairGenerator.getInstance("DH")
bobKeyPairGen ==> java.security.KeyPairGenerator$Delegate@42d8062c

jshell> var dhParams = ((DHPublicKey)alicePublicKey).getParams()
dhParams ==> javax.crypto.spec.DHParameterSpec@cb51256

jshell> bobKeyPairGen.initialize(dhParams)

jshell> var bobKeyPair = bobKeyPairGen.generateKeyPair()
bobKeyPair ==> java.security.KeyPair@5bcea91b

復元された Alice の公開鍵は、 DHPublicKey という型になっている。
この getPrams() メソッドを使って、 Alice 側で生成された鍵交換の具体的なパラメータを取得する。

鍵交換のパラメータが取得できたら、その値から Bob 側の鍵ペアを生成する。

Bob
// Bob の KeyAgreement を生成して、 Bob の秘密鍵で初期化
jshell> var bobKeyAgree = KeyAgreement.getInstance("DH")
bobKeyAgree ==> javax.crypto.KeyAgreement@27f723

jshell> bobKeyAgree.init(bobKeyPair.getPrivate())

// Bob の公開鍵をファイルに出力
jshell> writeFile("bob-dh-public-key", bobKeyPair.getPublic().getEncoded())

Bob 側の KeyAgreement は、 Bob の秘密鍵で初期化しておく。

そして、 Bob の公開鍵を Alice に送る。

Alice
// Bob の公開鍵を読み取り
jshell> var encodedBobPublicKey = readFile("bob-dh-public-key")
encodedBobPublicKey ==> byte[557] { 48, -126, 2, 41, 48, -126, 1, 27, 6,  ... 5, -20, 52, -85, -95, 51 }

// Alice 側で Bob の公開鍵を復元
jshell> var aliceKeyFactory = KeyFactory.getInstance("DH")
aliceKeyFactory ==> java.security.KeyFactory@7b69c6ba

jshell> var bobPublicKeySpec = new X509EncodedKeySpec(encodedBobPublicKey)
bobPublicKeySpec ==> java.security.spec.X509EncodedKeySpec@12f41634

jshell> var bobPublicKey = aliceKeyFactory.generatePublic(bobPublicKeySpec)
bobPublicKey ==> SunJCE Diffie-Hellman Public Key:
y:
    f82361 ...
g:
    02
l:
    1024

Alice は、 Bob から受け取った公開鍵を復元する。
この手順は、 Bob が Alice の公開鍵を復元したときと同じ。

Alice
// Bob の公開鍵の情報をもとに、秘密鍵を生成
jshell> aliceKeyAgree.doPhase(bobPublicKey, true)
$20 ==> null

jshell> var secret = aliceKeyAgree.generateSecret()
secret ==> byte[256] { 102, 50, -83, 94, 119, -90, -46, -27, ... 66, -94, -121, -19, -119 }
Bob
// Alice の公開鍵の情報をもとに、秘密鍵を生成
jshell> bobKeyAgree.doPhase(alicePublicKey, true)
$25 ==> null

jshell> var secret = bobKeyAgree.generateSecret()
secret ==> byte[256] { 102, 50, -83, 94, 119, -90, -46, -27, ... 66, -94, -121, -19, -119 }

最後に、それぞれ KeyAgreementdoPhase() メソッドを呼ぶことで、秘密鍵生成の準備が完了する。

秘密鍵の生成は、 generateSecret() で行う。

以下、生成された秘密鍵を使って暗号化と復号ができることを確認している。

Alice
// 生成された鍵の先頭 128 ビットを使って AES の秘密鍵を生成
jshell> var secretKey = new SecretKeySpec(secret, 0, 16, "AES")
secretKey ==> javax.crypto.spec.SecretKeySpec@fffe86a4

// AES で暗号化
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5PAdding")
cipher ==> javax.crypto.Cipher@12cdcf4

jshell> cipher.init(Cipher.ENCRYPT_MODE, secretKey)

// 暗号文を書き出し
jshell> writeFile("cryptograph", cipher.doFinal("Hello Diffie-Hellman!!".getBytes()))

// 初期化ベクトルを書き出し
jshell> writeFile("iv", cipher.getParameters().getEncoded())
Bob
// Alice 同様、 AES 用の秘密鍵を生成
jshell> var secretKey = new SecretKeySpec(secret, 0, 16, "AES")
secretKey ==> javax.crypto.spec.SecretKeySpec@fffe86a4

// 初期化ベクトルをファイルから復元
jshell> var params = AlgorithmParameters.getInstance("AES")
params ==>

jshell> params.init(readFile("iv"))

// Cipher を復号モードで準備
jshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher ==> javax.crypto.Cipher@cb51256

jshell> cipher.init(Cipher.DECRYPT_MODE, secretKey, params)

// 暗号文を復号
jshell> var message = cipher.doFinal(readFile("cryptograph"))
message ==> byte[22] { 72, 101, 108, 108, 111, 32, 68, 105, 1 ... 08, 109, 97, 110, 33, 33 }

jshell> new String(message)
$31 ==> "Hello Diffie-Hellman!!"

参考


  1. CipherInputStreamclose() してなくても結果が中途半端になっていないが、実装をみると入力ストリームが末尾まで達すると doFinal() しているっぽい 

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