- 投稿日:2019-05-01T22:44:15+09:00
数値(int)⇔文字列(String)変換
- 投稿日:2019-05-01T22:24:38+09:00
いろはちゃんコンテスト Day2 - A-Cまで
とりあえず、解けたところまで。。。
終了後は、解けた問題の考察があっていたかの確認
解けなかった問題をeditorialを読んで考察。。。しても理解不能。。。考察力を磨くとは(ryもう少しレベルアップしたら残りの問題をやりに戻ってくる予定
A問題
- やっててよかったEducational DP Contest
- $LCS+1$の長さが [真・$p$字決まり]
- LCS解説は
- id:nitoyon さん作成のslideshare資料
- 典型的な DP (動的計画法) のパターンを整理 Part 1 ~ ナップサック DP 編 ~
- 北陸先端科学技術大学院大学の講義資料?
- プログラミングコンテストチャレンジブック(P.56-P.57)
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); } }
- 投稿日:2019-05-01T22:06:25+09:00
いろはちゃんコンテスト Day1 - A-Fまで
とりあえず、解けたところまで。。。
コンテスト中に解けない問題はeditorialみても解けませんっすわ。。。A問題
- 最初の1文字を出力する
private void solveA() { String s = next(); out.println(s.charAt(0)); }B問題
- 「先頭の文字を末尾に移動する」という操作を$K回$行った結果
- この手の問題、きちんとローテすると絶対時間足りないんで
- ローテしたかの様に、
後ろに文字を足していく- 生成できた文字列のk番目から元の文字の長さを切り出す
入力例2:
JKrolling
99生成後文字列(ここの99番目から108番目までを切り出して出力)
JKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingJKrollingprivate 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()); }
- 投稿日:2019-05-01T20:10:19+09:00
【初心者向け】JavaGoldが解説するPythonの基礎 パート1
概要
今回の3人のプロジェクトでPythonを使うため1から勉強したので
今回学んだ内容をJavaとの違いを含めて皆様にシェアできればと思います!!
このパート1ではJavaとの違い&概要を、パート2で基本的な構文(if文&例外処理等)を説明していきます。ちなみに仕事ではJavaしか使う機会がなく、1年前の3年目の夏にJava SE8 Goldを取得しました。
(本日から令和が始まりましたね!!)
アジェンダ
・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. None1.数値型
①整数型
→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]=False4.辞書型
Javaで言うMap。内容の書き換えが可能で、キーと値のセットでデータを管理。
データは波括弧 { } の中にキーと値をセットにしたデータをカンマで区切って宣言する。
キーに対する値の型は辞書内で一致しなくても良い。directory={'key1': 'value1', 'key2': 'value2'} print(directory['key1']) #value15.タプル型
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 版
- 投稿日:2019-05-01T19:52:06+09:00
WebSphere LibertyでHTTPSを有効化する
やりたいこと
自作のWebアプリケーションをGoogleのLighthouseにかけてみようと思ったらHTTPS化することが必須のようだったので、LibertyでHTTPSを使う方法を試してみた。
SANが必要
自己署名証明書を利用してHTTPS通信をすれば良いので難しくないと思ったのだが、昔はCommon Name(CN)が設定されていればOKだったが、Chrome 58以降は証明書にSubject Alternative Name(SAN)が登録されていることを要求するため、SANを含む証明書を作る必要がある。
試した環境
- 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/securityにkey.p12というファイルが生成される。このファイルはPKCS12という形式のファイルで中に秘密鍵と公開鍵証明書が保存されている。ファイルにはパスワードがかかっていてserver.xmlの<keyStore>要素でパスワードを指定する。内容の確認方法は以下の通り(パスワードは
<keyStore>で指定したもの)。default という別名(alias)で証明書が登録されていることがわかる。keytool -v -list -keystore key.p12SAN付き証明書の作成手順
秘密鍵を入手してそこから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.p12Macのキーチェーンに証明書を登録する
キーチェーンアクセス.appを起動して、左ペインから「ログイン」、「証明書」を開く。右のペインに証明書の一覧が表示されるのでここに上記の手順で作成したdefault.crtをドラッグ&ドロップで追加する。追加したらSSLの部分を「常に信頼する」に変更する。
Chromeでアクセスし直すと、自己署名証明書利用時の警告メッセージなくアクセスできる。
参考
p12ファイルからの秘密鍵、証明書の取り出し方
以下のコマンドで取り出せる。前述の通りiTerm.appだとパスワード入力がうまくいかないのでTerminal.appを使った。
# 秘密鍵を取り出す openssl pkcs12 -in key.p12 -nocerts -nodes -out privatekey # 証明書を取り出す openssl pkcs12 -in key.p12 -clcerts -nokeys -out default2.crtp12かjksか
昔はp12ではなくjks形式だった気がすると思ってOpenLibertyのIssueをのぞいてみたところ19.0.0.3から変わったのかもしれない。一般的にp12が推奨されているっぽい。
- https://github.com/OpenLiberty/open-liberty/issues/7041
- 投稿日:2019-05-01T19:09:27+09:00
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
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 サポート・ロードマップをご確認ください。
- 投稿日:2019-05-01T17:46:54+09:00
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.batcd (開発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を設定
- 投稿日:2019-05-01T17:46:54+09:00
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ファイル
- 投稿日:2019-05-01T17:13:08+09:00
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での役者の役割がわかりました。
結構行き詰まる人もいそうだというのが私の感想です。
- 投稿日:2019-05-01T15:58:17+09:00
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 successfully3. データベース・サービスとは?
データベース・サービスとは、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.comsqlclによる接続サンプルは下記の通り彡(゚)(゚)
./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:ウォレットのパス(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/86355be72a47a840d10eset 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=…")) ) :
- 投稿日:2019-05-01T12:06:12+09:00
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オブジェクトを返却するようにしています。具体的なコードは以下のイメージです。
ExampleFormpublic 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の挙動について後述しますのでご覧ください。
対処法
私の考える対処法としては、メソッド名を変更することです。
以下のようなコードになります。ExampleFormpublic 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メソッド内の以下記述になります。setupReadMethodif (!propDesc.getPropertyType().equals(propertyType)) { invalidPropertyNames.add(propertyName);すでにPropertyDescImplが登録されている場合かつ、プロパティタイプ(引数の型・戻り値の型)が異なる場合は、invalidPropertyNamesに登録されるという流れになります。
以上のことから、Seasar2ではsetterの引数の型とgetterの戻り値の型が異なる場合には、プロパティの管理対象から除外されてしまい、自動でsetされないということになります。
その他参考になる記事
上記の内容はFormでのjson操作でしたが、Springと同じように上位階層でjsonをオブジェクトに詰めることが出来るようになる方法は以下の記事を参考にしてください。
SAStrutsとJSON @shienaさん
- 投稿日:2019-05-01T11:49:08+09:00
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);と書きましょう。
- 投稿日:2019-05-01T10:55:52+09:00
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.2Javaプログラム
Javaのプログラムは通常、実行前にjavacツールで
.javaから.classというバイトコード(中間コードとも言う)にコンパイルします。そしてプログラム実行時にJVM上でバイトコードをインタプリタ方式で実行、もしくはJITコンパイラによってマシンコードに再コンパイルして実行されます。JITコンパイラはJRE(Javaランタイム環境)のコンポーネントの一つで、プログラムメソッドのマシンコードを最適化しパフォーマンス向上をする仕組みです。ここからはCLIで実際にJavaプログラムのコンパイルと実行、アーカイブの作成までの作業を行います。
基本となるコンパイルと実行
まずは
Hello world.と出力するプログラムを書いて実行してみます。実行するプログラムファイルは下記となるのでviなどで用意してください。App.javaclass 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.javaclass 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.javaclass 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.javaclass 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.javaAppのコンパイルと一緒にHumanもコンパイルされたことが確認できます。
$ java App Alice Bob Carol My name is Alice. My name is Bob. My name is Carol.引数の数だけHumanインスタンスを生成しメソッドが実行されたことを確認しました。
パッケージ管理
パッケージ名を付与して、別々のパッケージのプログラムとしてコンパイルしてみます。まずはHumanクラスにパッケージ名を付与します。また、このクラスは別のパッケージからアクセスされるクラスなので、各修飾子を正しく付与しました。
Human.javapackage 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.javapackage 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 filesclassesディレクトリが作成され、その下に
.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.mfMain-Class: jp.co.sample.AppMANIFESTファイルを用意したら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"
- 投稿日:2019-05-01T00:18:08+09:00
Java を使うWeb アプリの文字コードを Shift_JIS から UTF-8 に変更する場合の考慮点
概要
昔からあるWebアプリケーションのHTML文字コード設定をUTF-8に変える時のポイントをまとめてみる。
前提
- Webページの文字コード設定をShift_JISからUTF-8へ変更する
- 利用したソフトウェアは以下
- OpenJDK 11
- WebSphere Liberty 19.0.0.3
- Db2 11.1.4.4
- Chrome
- 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 identityHTMLの変更箇所
- HTML5仕様には文字コードにはUTF-8を指定するべきとの記載あり
- その他のモダンなWebページの書き方はHTML5 BoilerplateやNu HTML Checkerを使うと便利
<!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の場合、
😀に変換されるが、UTF-8の場合は?がそのまま送信される。以下の例では、¢、?などで数値文字参照が利用される。
ユーザーの入力 Unicodeコードポイント Formエンコードデータ(Shift_JIS)の場合 Formエンコードデータ(UTF-8)の場合 備考 a U+0061 a a & U+0026 &(%26) &(%26) ¢ U+00A2 ¢(%26%23162%3B)¢(%C2%A2) CENT SIGN. Shift_JIS に存在しない文字 ¢ U+FFE0 ¢(%81%91) ¢(%EF%BF%A0) FULLWIDTH CENT SIGN — U+2014 —(%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 ‖(%26%238214%3B)‖(%E2%80%96) DOUBLE VERTICAL LINE ~ U+FF5E ~(%81%60) ~(%EF%BD%9E) FULLWIDTH TILDE 〜 U+301C 〜(%26%2312316%3B)〜(%E3%80%9C) WAVE DASH ¦ U+FFE4 ¦(%FAU) ¦(%EF%BF%A4) FULLWIDTH BROKEN BAR ¦ U+00A6 ¦(%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 㐂(%26%2313314%3B)㐂(%E3%90%82) JIS 第 3 水準 彅 U+5F45 彅(%FAg) 彅(%E5%BD%85) JIS 第 3 水準 繫 U+7E6B 繫(%26%2332363%3B)繫(%E7%B9%AB) JIS 第 3 水準.JIS X 0213 へ 2004 年に追加 葛 U+845B 葛(%8A%8B) 葛(%E8%91%9B) 葛 ? U+845B U+E0100 葛�(%8A%8B%26%23917760%3B)葛+異体字セレクタ(%E8%91%9B%F3%A0%84%80) 異体字セレクタを利用 パ U+30D1 パ(%83p) パ(%E3%83%91) パ U+30CF U+309A パ(%83n%26%2312442%3B)ハ+半濁点(%E3%83%8F%E3%82%9A) 結合文字 ? U+1F600 😀(%26%23128512%3B)?(%F0%9F%98%80) サロゲートペア ? U+20BB7 𠮷(%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 数値文字参照として扱われる � へ化ける � へ化ける 参考
- 投稿日:2019-05-01T00:00:13+09:00
令和の始まりにHello, Reiwa!と叫ぶ
はじめに
Hello, World! をもじった、ただのネタ記事です。
プログラム
C
HelloReiwa.c#include <stdio.h> int main() { printf("Hello, Reiwa!\n"); return 0; }Java
HelloReiwa.javapublic class HelloReiwa { public static void main(String[] args) { System.out.println("Hello, Reiwa!"); } }Perl
HelloReiwa.plprint "Hello, Reiwa!\n";PHP
HelloReiwa.php<?php echo "Hello, Reiwa!\n"; ?>Python
HelloReiwa.pyprint("Hello, Reiwa!")Ruby
HelloReiwa.rbputs "Hello, Reiwa!"まとめ
ありがとう平成。こんにちは令和。
令和の時代が、幸多き時代になりますように。
- 投稿日:2019-05-01T00:00:06+09:00
JCA 使い方メモ
JCA とは
Java Cryptography Architecture(Java 暗号化アーキテクチャ)の略。
Java で暗号技術を使うための API、フレームワーク。環境
Java
openjdk 11.0.2OS
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 は次のことを実現することを基本方針として設計されている。
- 実装の独立性と相互操作性
- アルゴリズムの独立性と拡張性
実装の独立性
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 のアルゴリズムを指定して生成する場合は次のように実装する。jshelljshell> 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.Securityの getProviders() で取得できる。jshelljshell> 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 鍵を管理するキーストア インスタンスの取得
jshelljshell> 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 )
実行環境のプロバイダがサポートしているアルゴリズムを確認する
jshelljshell> 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 パッケージに分かれて提供されている。
これには歴史的な理由がある。
アメリカはかつて、暗号の輸出を厳しく制限していた時期があった。JCA のパッケージ構成はこの規制に対応したものとなっている。
java.securityパッケージには輸出可能な技術に関するクラス(MessageDigestやSignature)が入れられ、javax.cryptoパッケージには輸出できない技術に関するクラス(CipherやKeyAgreement)が入れられている。プロバイダもこれらに合わせて分けられていたようで、 SUN プロバイダ が
java.securityで提供している機能を、 SunJCE プロバイダ がjavax.cryptoで提供している機能をそれぞれ実装しているっぽい。規制が厳しかった頃は、 SunJCE は拡張機能として提供されていたらしいが、現在は規制が緩和され JDK にバンドルされるようになっている。
参考:Javaの暗号化 | 1. 一般的なセキュリティ | セキュリティ開発者ガイド
メッセージダイジェスト(ハッシュ)
ハッシュ関数を使うためには java.security.MessageDigest クラスを使用する。
jshelljshell> 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配列で返る。入力を分割する
jshelljshell> 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 のハッシュ値を実際に計算してみる。
jshelljshell> 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は、ストリームからデータを読み取るたびに、読み取ったデータがMessageDigestのupdate()に渡される。jshelljshell> 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は、ストリームにデータを書き出すたびに、書き出したデータがMessageDigestのupdate()に渡される。ちなみに、計算したハッシュ値を 16 進数文字列に変換すると、
jshelljshell> toHexString(hash) $12 ==> "35a8d018f420fb05fe7c2aa9933122896ca50bd23dbd373e90d8e2f3897c4e92"OpenJDK のサイトで公開されているハッシュ値は
35a8d018f420fb05fe7c2aa9933122896ca50bd23dbd373e90d8e2f3897c4e92なので、ちゃんと同じ値が計算できている。鍵
暗号技術には様々な「鍵」が存在する(秘密鍵、公開鍵、プライベート鍵、etc...)。
JCA も、それに合わせて様々な鍵を表す型を定義している(SecretKey,PublicKey,PrivateKey, etc...)。鍵を表す型は様々だが、これらは全て1つの共通のインターフェースを親に持っている。
それが Key インターフェースで、「鍵」を表す最上位の型となる。
Keyには、鍵の情報にアクセスするためのメソッドが3つ定義されている。
- getAlgorithm()
- AES や RSA といった、鍵に関連するアルゴリズム名を取得する
- getEncoded()
- 鍵を特定の形式(X.509 や PKCS8 など)にエンコードした値を取得する
- getFormat()
- エンコードの名前を取得する
鍵仕様
Keyからはアルゴリズムなどの情報は取得できるが、鍵を構成する具体的なデータは取得できない。
このように具体的な鍵データにアクセスできないことを、 JCA では不透明な表現と呼んでいる。一方で、鍵を構成する具体的なデータにアクセスできる型も用意されている。
それが KeySpec (鍵仕様)となる。
KeySpec自体は「この型は鍵仕様である」ということを表現することが目的なので、実際に鍵データにアクセスするための API は定義されていない。
実際の API は、KeySpecの実装クラスで定義されている。例えば、 DSAPrivateKeySpec には、鍵生成で使用された素数 $p$ や非公開鍵 $x$ の情報にアクセスできるようになっている。
このように、鍵を構成する具体的なデータにアクセスできることを JCA では 透明な表現 と呼んでいる。
実際の
KeyとKeySpecおよび一部のクラス階層は次のような感じになっている。Generator と Factory
鍵を作るための手段として、 JCA には
GeneratorとFactoryの2つが用意されている。
Generatorは、鍵を新規に作成する機能を提供する。
例えば、鍵長のようなパラメータを指定して新しい鍵を生成できる。一方
Factoryは、主に鍵と鍵仕様を相互変換する機能を提供する。
鍵によっては異なる2つの鍵仕様から同じ鍵を生成できるものもある(らしい)。具体的には次のようなクラスが存在する。
GeneratorFactory暗号化/復号
共通鍵暗号および公開鍵暗号による暗号化/復号処理は、どちらも javax.crypto.Cipher クラスを使用する。
Cipher のアルゴリズム指定
jshelljshell> var cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher ==> Cipher.AES/CBC/PKCS5Padding, mode: not initialize ... orithm from: (no provider)
CipherのgetInstance()に渡す文字列は単純なアルゴリズム名ではなく、ブロック暗号のモードなども指定できるようになっている。具体的な書式は
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のインスタンスを、今度は復号に再利用することができる。
逆にいうと、暗号化の途中で復号モードに初期化してしまうと、暗号化処理の途中で設定していた情報が失われるので注意が必要。暗号化/復号
jshelljshell> 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()のたびに返される。jshelljshell> 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と同じように、CipherとInputStream/OutputStreamを連携させたクラスが用意されている。それぞれのデータと暗号化/復号の流れは、下図のようなイメージになる。
まずは
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 }
CipherInputStreamとCipherOutputStreamは、どちらもclose()時にCipherのdoFinal()が実行される。
なので、close()は忘れずに実行しておかないと結果が中途半端になったりする1(ストリームなので、基本 try-with-resources などを使うから気にする必要はないと思うが)。transferTo() は使うべきでない?
CipherInputStreamの Javadoc には次のように書かれている。このクラスを使用するプログラマは、このクラスで定義されていないメソッド、またはオーバーライドされていないメソッド(あとでスーパー・クラスのいずれかに追加された新しいメソッドやコンストラクタなど)を絶対に使用しないでください。それらのメソッドの設計と実装では、CipherInputStreamに関するセキュリティ上の影響が考慮されていない可能性があるためです。
一方、
InputStreamには Java 9 で transferTo() という便利なメソッドが追加されている。この
transferTo()はCipherInputStreamでオーバーライドされていないので、使うべきでない条件を満たしてしまっている。ただ、この
transferTo()は OpenJDK 11 では次のように実装されている。InputStream.javapublic 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; }ここでは、
InputStreamのread()とOutputStreamのwrite()だけが使われている。もし
transferTo()を使わずに処理を書こうとしても、結局このtransferTo()がやっていることと同じ実装を書くことになる。
そして、そのとき使用するメソッドはCipherInputStreamがオーバーライドしているread()かCipherOutputStreamのwrite()になる。となると、結局
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 ビットになる。鍵長の指定
jshelljshell> 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)秘密鍵のエンコードされたデータは
Keyの getEncoded() で取得できる。
また、パラメータの情報はAlgorithmParametersの getEncoded() で取得できる。鍵をファイルから復元する
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@855e49d8PBE では、 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)は、 PBEParameterSpec のgetParameterSpec()メソッドで取得できる。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!!"復号時は、再び「パスワード」「ソルト」「イテレーション回数」の情報を使って
KeyとAlgorithmParameterSpecを生成し、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 を取得できる。鍵長の指定
jshelljshell> keyPairGen.initialize(4096) jshell> keyPairGen.generateKeyPair().getPublic() $9 ==> Sun RSA public key, 4096 bits params: null modulus: 58218231801097229661983999673649965349728454764714757172625245890343045... public exponent: 65537
KeyPairGeneratorの initialize() メソッドで鍵長を指定できる。暗号化
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冬U5wgL゙B浅簗涓ミ>\006e?g?9lエ醋棒:\t]垉R\020ソ?nミ?!?&sエ?ォラ\016`H\f6漬?レ?ュ4\nfU呶?.斎"
KeyPairのgetPublic()で公開鍵を取得できるので、これで暗号化を行う。
手順は 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!!"
KeyPairのgetPrivate()でプライベート鍵が取得できるので、それを使って復号を行う。
こちらも、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配列として返される。分割して計算する
jshelljshell> 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 の鍵ペアを作るなら、-keyalgにDSAと指定すればいい)
-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.htmlCN=www-cs-01.oracle.comOU=Content Management Services ITO=Oracle CorporationL=Redwood ShoresS=CaliforniaC=USAmazon
https://www.amazon.co.jp/CN=www.amazon.co.jpO=Amazon.com, Inc.L=SeattleS=WashingtonC=US内閣府
https://www.cao.go.jp/CN=*.cao.go.jpO=Cabinet OfficeL=Chiyoda-kuS=TokyoC=JPPanasonic
https://panasonic.jp/CN=panasonic.jpO=Panasonic CorporationL=KadomaS=OsakaC=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つの機能が存在する。
- 証明書応答のインポート
- 信頼できる証明書のインポート
-importcertコマンドでは、条件によってこのいずれかの機能が実行される。次の条件を満たした場合は、証明書応答のインポートとして動作する。
-aliasで指定した名前のエントリがすでにキーストアに存在する- そのエントリがプライベート鍵のエントリである
一方、次の条件を満たした場合は、信頼できる証明書のインポートとして動作する。
-aliasで指定した名前のエントリがキーストアに存在しない証明書応答のインポート
-aliasで指定した名前のエントリがプライベート鍵のエントリとして既にキーストアに存在する場合、インポートしようとしている証明書は証明書応答と判断される。
つまり、その鍵エントリ内の公開鍵証明書から生成した証明書署名要求に対して、 CA が署名して返してきた証明書応答をインポートしようとしている、という扱いになる。したがって、 keytool は鍵エントリに含まれる公開鍵証明書を、インポート対象に指定した証明書応答で置き換えるように動作する。
証明書応答の検証
インポートが実際に行われる前に、証明書応答が正当なものか検証が行われる。
検証は、大きく次の2つが行われる。
- 証明書応答が、確かに
-aliasで指定された鍵エントリに対応するものかどうか- 証明書応答が信頼できるかどうか
証明書応答が
-aliasで指定した鍵エントリに対応するものかどうかは、証明書応答の中に鍵エントリ内の公開鍵と同じものがあるかどうかで判断される。
証明書応答の中に鍵エントリ内の公開鍵証明書が存在しないのであれば、その証明書応答は全然関係のないモノなので、エラーとなる。証明書応答が信頼できるかどうかの判定方法は、証明書応答内に存在する証明書の数によって大きく2つに分かれる。
証明書応答内に複数の証明書が含まれる場合
証明書応答内に複数の証明書が含まれる場合は、それらの証明書で有効な証明書チェーンが構築できるかどうかが試される。
まず最初に、証明書応答の中からインポート先の鍵エントリと同じ公開鍵を持つ証明書を見つけ出す。
そして、その証明書を先頭にして、署名の検証が OK となる証明書を次々と繋げていく。末尾にきた証明書が自己署名証明書だった場合、その証明書と同じ証明書がキーストア内に存在するかが確認される。
キーストア内に同じ自己署名証明書が存在した場合は、証明書チェーンは正当なものと判断され、構築された証明書チェーンが鍵エントリにインポートされる。
同じ自己署名証明書が存在しなかった場合は、インポートを続行して良いか keytool が確認を求めてくる。一方、末尾にきた証明書が自己署名証明書でなかった場合、その署名の検証が通る証明書がキーストア内に存在しないか確認される。
キーストア内に該当する証明書が見つかった場合は、証明書チェーンの末尾にキーストア内で見つかった証明書を追加したうえで、鍵エントリに証明書チェーンがインポートされる。
該当する証明書が見つからなかった場合は、インポートを続行して良いか keytool が確認を求めてくる。証明書応答が単独の証明書の場合
証明書応答内に証明書が1つだけ存在する場合は、検証の動作が少し変わる。
この場合、 keytool はインポート先のキーストア内に存在する証明書を使って証明書チェーンが再現できないか試みる。
最終的にルートまで証明書チェーンが再現できた場合は、信頼できるモノとして再現されたチェーンが鍵エントリに登録される。
チェーンが再現できなかった場合(途中で検証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-keyはroot-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 インスタンスを取得する
jshelljshell> var keystore = KeyStore.getInstance("PKCS12") keystore ==> java.security.KeyStore@3fd7a715キーストアにアクセスするには、 KeyStore クラスを使用する。
getInstance()では、キーストアのタイプを指定する。
Java 9 以降であれば、キーストアのタイプはデフォルトで PKCS12 なので、"PKCS12"でインスタンスを生成している。
(古いキーストアであれば JKS の可能性もあるので、その場合は"JKS"と指定する)キーストアファイルを読み込む
jshelljshell> 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配列で指定する。インスタンス取得と同時に読み込む
jshelljshell> var keystore = KeyStore.getInstance(new File("alice.keystore"), password) keystore ==> java.security.KeyStore@525b461agetInstance(File, char[]) なら、
KeyStoreインスタンスの生成からキーストアファイルのロードまでを一回で実行できる。キーストアのタイプは自動判定される。
全エントリのエイリアス名を取得する
jshelljshell> keystore.aliases().asIterator().forEachRemaining(System.out::println) alice-key aes-key root-ca-certaliases() メソッドで、全エントリのエイリアス名を取得できる。
エイリアス名からエントリの種類を確認する
jshelljshell> 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() で確認できる。
秘密鍵を取得する
jshelljshell> var seckey = keystore.getKey("seckey", password) seckey ==> javax.crypto.spec.SecretKeySpec@fffe8f5b jshell> seckey.getAlgorithm() $29 ==> "AES"getKey() で、指定したエイリアスの鍵を秘密鍵として取得できる。
第一引数はエイリアス名を渡し、第二引数は鍵を保護しているパスワード(普通はキーストアのパスワードと同じ)を渡す。
プライベート鍵を取得する
jshelljshell> 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)を取得できる。
証明書チェーンを取得する
jshelljshell> 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 ならキーストアと同じパスワードを使うようになっている)ファイルに出力する
jshelljshell> 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つずつ証明書を読み込むことができる。
(証明書がもう存在しないのにさらに読み込もうとすると、例外がスローされる)複数の証明書をまとめて読み込む
jshelljshell> 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 ==> 2generateCertificates() なら、対象データ内のすべての証明書をまとめて読み込める。
証明書の署名を検証する
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)。
DRBGは NIST 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/randomNativePRNGBlocking/dev/random/dev/randomNativePRNGNonBlocking/dev/urandom/dev/urandomSecureRandom | 表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を用いた鍵交換の流れは、以下のような感じになる。以下、 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$ が含まれている。
KeyAgreementもDiffieHellman(または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: 1024Alice から受け取った公開鍵は 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: 1024Alice は、 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 }最後に、それぞれ
KeyAgreementの doPhase() メソッドを呼ぶことで、秘密鍵生成の準備が完了する。秘密鍵の生成は、 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!!"参考
CipherInputStreamはclose()してなくても結果が中途半端になっていないが、実装をみると入力ストリームが末尾まで達するとdoFinal()しているっぽい ↩










