Java ハッシュ(hash)化を試してみる、暗号化とは違うらしいけど

ハッ~シュん!と、くしゃみが出て、思わずハクション大魔王を呼んでしまいそうな季節、いかがお過ごしでしょうか?どうも僕です。

部屋の掃除をすると決意した昨日の夜から、1mmも掃除に取り掛かれていない、そんな12/08(土)です...

というわけで、今回は、ハッシュ化のお話です、Javaでね。

 

話は変わりますが、常駐先の方に教えていただき、「日本Javaユーザーグループ(Japan Java User Group/JJUG)」ってグループの開催する、2018/12/15(土)イベントに申し込んじゃいました。

www.java-users.jp

全部見たいけど~、

f:id:ts0818:20181208095307j:plain

f:id:ts0818:20181208095323j:plain

⇧  複数の講演があるので、泣く泣く候補を絞りました。GraphQLの内容も見たかったですが...

メールで申込完了の知らせが届いてました。

f:id:ts0818:20181208093954j:plain

 

難しくて理解できないとは思いますが、来週のJavaのイベントが楽しみではあります。 

話が脱線してしまいましたが、レッツ、トライ~。

 

ハッシュ化とは?

そも、ハッシュ化とは?

Wikipediaさ~ん!

hash

ハッシュ - Wikipedia

⇧  いろいろ候補が出てきてしまうんですが、プログラム的には、「ハッシュ関数」のくくりが該当しそうですかね。 

そんじゃ、「ハッシュ関数」のWikipediaの見解はというと、

ハッシュ関数 (ハッシュかんすう、hash function) あるいは要約関数とは、あるデータが与えられた場合にそのデータを代表する数値を得る操作、または、その様な数値を得るための関数のこと。ハッシュ関数から得られた数値のことを要約値ハッシュ値または単にハッシュという。

ハッシュ関数 - Wikipedia

⇧  「ハッシュ関数」を使って、『元ある値(パスワードとか?)』を加工することを「ハッシュ化」、加工した値を、『ハッシュ値』または『ハッシュ』っていうらしいですね。

Wikipediaさんの図を参考にすると、下記のようなイメージらしいです。(※「ハッシュ値」の値は、あくまでイメージです。)

f:id:ts0818:20181208104529p:plain

で、「ハッシュ関数」で、『元ある値(パスワードとか?)』を加工した時に、『ハッシュ値』が被ってしまうことがあると...駄目ですやん...

ハッシュ関数の入力を「キー (key)」と呼ぶ。ハッシュ関数は2つ以上のキーに同じハッシュ値マッピングすることがある。多くの場合、このような衝突の発生は最小限に抑えるのが望ましい。したがって、ハッシュ関数はキーとハッシュ値マッピングする際に可能な限り一様になるようにしなければならない。

ハッシュ関数 - Wikipedia

⇧  起こりえてしまうらしいですね...

 

なので、データベースとかに『パスワード』を加工して『ハッシュ値』として保存する場合は、『ハッシュ値』+『会員ID(重複なし)』みたいな形で、「一意(重複のない)」とするようなイメージですかね?

 

ハッシュ関数」の具体的な用途はというと、 

  • 検索の高速化やデータ比較処理の高速化
    • データベース内の項目の検索
    • 大きなファイル内で重複しているレコードや似ているレコードを検出
    • 核酸の並びから類似する配列を検索
  • 改竄の検出

ハッシュ関数 - Wikipedia

ということらしい。 

ハッシュ関数は、チェックサムチェックディジットフィンガープリント誤り訂正符号暗号学的ハッシュ関数などと関係がある。これらの概念は一部はオーバーラップしているが、それぞれ用途が異なり、異なった形で設計・最適化されている。

ハッシュ関数 - Wikipedia

⇧ このへんは、割愛で...

 

またプログラミング言語の一部(PerlRuby等、主に高等言語とされる一般的なプログラミング言語の多く)においては、連想配列のことを伝統的にハッシュと呼ぶが、これは連想配列そのもののプログラムの内部的実装に拠るものであり、ハッシュ関数そのものとは全く異なる。。

ハッシュ関数 - Wikipedia

⇧  「連想配列」のことを「ハッシュ」って...紛らわしいことを... 

  

ちなみに、「Perl」の生みの親である、『ラリー・ウォールLarry Wall1954年9月27日 - )』 さんは、『プログラマの三大美徳』を唱えた人でもあると。

ラリー・ウォールによれば、プログラマの三大美徳とは次の通りである。

  • 怠惰(Laziness)
  • 短気(Impatience)
  • 傲慢(Hubris)

プログラマに必要とされる効率や再利用性の重視処理速度の追求品質にかける自尊心を言ったものである

プログラマ - Wikipedia

ただ、私は言いたいのです。プログラマーには、お酒も必須であると。

お酒が必要な理由、それが、「バルマーピーク」さ~。

「血中アルコール濃度が0.129~0.138%のときに超人的なプログラミング能力を発揮できる」

バルマーピークは本当にあるのか 検証してみた | PLAID engineer blog

tech.plaid.co.jp

⇧  上記サイト様によりますと、効果のほどは...う~ん...な結果になってしまうらしいですが。「バルマーピーク」については、Swizec Teller さんという方が仰っているそうですね。

swizec.com

⇧  何故、プログラマーは夜働くのかって、仰られていますが、私は、夜は眠りたい人ですかね...

とは言え、プログラマーの美徳は、

ts0818によれば、プログラマの四大美徳とは次の通りである。

  • 怠惰(Laziness)
  • 短気(Impatience)
  • 傲慢(Hubris)
  • 酒(Ballmer Peak)

プログラマに必要とされる効率や再利用性の重視処理速度の追求品質にかける自尊心・超人思考を言ったものである

ts0818 - Wikipedia  ← 無いけど

⇧  のようになるのかと。なんか、美徳って、難しいですね...

キン肉マンジェロニモのように、スーパーマンロードを昇りきれば、超人に、もとい、超人プログラマーになれますかね...いや、スーパーマンロードを昇らなくても、酒を飲んだら良かですか!

ビバ!酒浸りの日々!

ただ、アルコールは筋肉を分解するという弊害もあるとか、悩ましいですね。

 

脱線しました。「暗号学的ハッシュ関数」 の説明では、

暗号学的ハッシュ関数(あんごうがくてきハッシュかんすう、cryptographic hash function)は、ハッシュ関数のうち、暗号など情報セキュリティの用途に適する暗号数理的性質をもつもの。任意の長さの入力を(通常は)固定長の出力に変換する。

暗号学的ハッシュ関数 - Wikipedia

と。

「メッセージダイジェスト」は、暗号学的ハッシュ関数の多数ある応用のひとつであり、メールなどの「メッセージ」のビット列から暗号学的ハッシュ関数によって得たハッシュ値を、そのメッセージの内容を保証する「ダイジェスト」として利用するものである。

暗号学的ハッシュ関数 - Wikipedia

と。

暗号学的ハッシュ関数は、一般的なハッシュ関数に望まれる性質や、決定的であることの他、次のような暗号学的な特性をもたなければならない。

  • ハッシュ値から、そのようなハッシュ値となるメッセージを得ることが(事実上)不可能であること(原像計算困難性、弱衝突耐性)。
  • 同じハッシュ値となる、異なる2つのメッセージのペアを求めることが(事実上)不可能であること(強衝突耐性)。
  • メッセージをほんの少し変えたとき、ハッシュ値は大幅に変わり、元のメッセージのハッシュ値とは相関がないように見えること。

暗号学的ハッシュ関数 - Wikipedia

となってますかね。

具体的には、

  • 原像計算困難性 (preimage resistance)
    • ハッシュ値 h が与えられたとき、そこから h = hash(m) となるような任意のメッセージ m を探すことが困難でなければならない。これは一方向性関数の原像計算困難性に関連している。この特性がない関数は(第1)原像攻撃に対して脆弱である。
  • 第2原像計算困難性
    • 入力 m1 が与えられたとき、hash(m1) = hash(m2) となる(すなわち、衝突する)ような別の入力 m2m1とは異なる入力)を見つけることが困難でなければならない。これを「弱衝突耐性」ともいう。この特性がない関数は、第2原像攻撃に対して脆弱である。
  • 強衝突耐性
    • hash(m1) = hash(m2) となるような2つの異なるメッセージ m1 と m2 を探し出すことが困難でなければならない。一般に誕生日のパラドックスによって、強衝突耐性を持つためには、原像計算困難性を持つために必要なハッシュ値の2倍の長さのハッシュ値が必要である。

暗号学的ハッシュ関数 - Wikipedia

となっていますかね...よく分からん...。 とりあえず、『ハッシュ値』が突き止められないようにする必要があるってことですかね。時間あるときに調査で...。

 

ハッシュ化と暗号化と復号化と

恋しさとせつなさと心強さと 、がそれぞれ異なる意味合いを持つのと同様、

ハッシュ化と暗号化は異なるらしいと。 

blog.goo.ne.jp

⇧  上記サイト様が詳しいです。 

ハッシュ化したものは、もう二度と元の状態には戻せない、もうあの日は戻らない、もう二度と戻らない日々を、俺達は走り続ける、という不可逆的な処理であると。

一方、暗号化は、復号化によって、元に戻せると、ありの~、ままの~姿見せるのよ~、という可逆的な処理であると。

 

分かりにくくて、すみません...イメージ図を用意しました。

 

■ 比較のイメージ図

f:id:ts0818:20181208124456p:plain

ハッシュ化は、「不可逆的」

暗号化は、「可逆的」

であると。

え?ハッシュ化は、元に戻せない?じゃあ、ハッシュ化で保存した「ハッシュ値」と、再度入力された「パスワード」をどうやって比較するのさ?とお思いの貴方!素晴らしい!

そうです、『再度入力された「パスワード」』を、『ハッシュ化で保存した「ハッシュ値」』の時と同じ「ハッシュ関数」にてハッシュ化して、比較するってことになります、多分。

f:id:ts0818:20181208131101p:plain

 

「ハッシュ」・「ソルト」・「ストレッチング」のコラボレーション

今のご時世、ただの「ハッシュ化」では不十分であるらしい。

www.casleyconsulting.co.jp

⇧  上記サイト様が詳しいです。

 

「ハッシュ」・「ソルト」・「ストレッチング」イメージ図

f:id:ts0818:20181208143054p:plain

ハッシュ化する際に、「ソルト」として乱数を用意し、『「元の値」 + 「ソルト」』で、ハッシュ化 すると。で、そのハッシュ化を繰り返し行うことを、「ストレッチング」っていうようです。

ハッシュ値』が流出したとしても、『ハッシュ値』を生成の際に使用されたアルゴリズムを突き止められないようにすれば、安全ではあるということでしょうかね。

 

Java でハッシュ化してみる

んじゃあ、実際に、試してみますか。

www.casleyconsulting.co.jp

howtodoinjava.com

 ⇧  上記サイト様を参考にさせていただきました。

 

Eclipseで、適当に「Java プロジェクト」を作成し、f:id:ts0818:20181208134223p:plain

「プロジェクト名(P):」を適当に入力し、「次へ(N) >」で。

f:id:ts0818:20181208134332p:plain

「完了(F)」で。

f:id:ts0818:20181208134451p:plain

「クラス」ファイルも作成で。

f:id:ts0818:20181208134614p:plain

「パッケージ(K):」「名前(M):」を適当に入力し、「public static void main(String args)(V)」にチェックし、「完了(F)」。

f:id:ts0818:20181208134807p:plain

もう一つ、「クラス」ファイルを作成。

f:id:ts0818:20181208144852p:plain

「public static void main(String args)(V)」にはチェックせず、「完了(F)」。

f:id:ts0818:20181208145014p:plain

クラスファイルが用意できました。

f:id:ts0818:20181208145127p:plain

んで、クラスファイルの内容はこんな感じ 

/HashTest/src/util/PasswordUtil.java

package util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

public class PasswordUtil {
	// ハッシュ化アルゴリズム
	private static final String ALGORITHM = "PBKDF2WithHmacSHA256";
	// ストレッチング回数
	private static final int ITERATION_COUNT = 10000;
	// 生成される鍵の長さ
	private static final int KEY_LENGTH = 256;

	public static String getHashedPassword(String password, String salt) {
		// PBEKeyを生成に必要な値を作成
		char[] passCharAry = password.toCharArray();
		byte[] hashedSalt = getSalt(salt);

		// PBEKeyを生成
		PBEKeySpec keySpec = new PBEKeySpec(passCharAry, hashedSalt, ITERATION_COUNT, KEY_LENGTH);
		// 秘密鍵を扱う用
		SecretKeyFactory skf = null;

		try {
			// 「PBKDF2WithHmacSHA256」アルゴリズムの秘密鍵を変換するオブジェクトの生成
			skf = SecretKeyFactory.getInstance(ALGORITHM);
		} catch (NoSuchAlgorithmException e) {
			// TODO 自動生成された catch ブロック
			e.printStackTrace();
		}

		// 秘密鍵のインターフェイス
		// すべての秘密鍵のインターフェイスを扱える
		SecretKey secretKey = null;
		try {
			// PBEKeyの鍵仕様で、秘密鍵の生成
			secretKey = skf.generateSecret(keySpec);
		} catch (InvalidKeySpecException e) {
			// TODO 自動生成された catch ブロック
			e.printStackTrace();
		}
		// 鍵(秘密鍵)を一次符号化形式
		byte[] passByteAry = secretKey.getEncoded();
		
		// 生成されたバイト配列を16進数の文字列に変換
		StringBuffer sf = new StringBuffer(64);
		for (byte b : passByteAry) {
			sf.append(String.format("%02x", b & 0xff));
		}
		return sf.toString();
	}

	// ソルトを生成
	private static byte[] getSalt(String salt) {
		byte[] saltBytes = null;
		MessageDigest messageDigest = null;

		try {
			messageDigest = MessageDigest.getInstance("SHA-256");
		} catch (NoSuchAlgorithmException e) {
			// TODO 自動生成された catch ブロック
			e.printStackTrace();
		}
		if(messageDigest != null) {
			messageDigest.update(salt.getBytes());
			saltBytes = messageDigest.digest();
		}
		return saltBytes;
	}

}

/HashTest/src/controller/HashTest.java

package controller;

import util.PasswordUtil;

public class HashTest {

	public static void main(String[] args) {
		// 入力されたパスワードを想定
		String pass01 = "password";
		String pass02 = "possword";

		// 入力されたユーザー名を想定
		String user01 = "ts0818";
		String user02 = "Larry Wall";

		// パスワード、ユーザーがともに同一のパターン
		String hashedPass01 = PasswordUtil.getHashedPassword(pass01, user01);
		String hashedPass02 = PasswordUtil.getHashedPassword(pass01, user01);
		System.out.println(hashedPass01);
		System.out.println(hashedPass02);

		// パスワード、または、ユーザーが違うパターン
		String hashedPass03 = PasswordUtil.getHashedPassword(pass01, user02);
		String hashedPass04 = PasswordUtil.getHashedPassword(pass02, user01);
		System.out.println(hashedPass03);
		System.out.println(hashedPass04);
	}
}

 

「HashTest.java」上で右クリックし、「実行」>「Java アプリケーション」で。 

f:id:ts0818:20181208161255p:plain

コンソールに結果が表示されます。

f:id:ts0818:20181208160954p:plain

入力された情報が同一の場合は、「ハッシュ値」が同一になることが確認できました!

ただ、今回の方法は、Java 8 以上じゃないと使えないようなので、ご注意を。

 

結局、掃除が、まったく手付かずになってしまった...明日、心を入れ替えて、掃除に取り掛かります...たぶん...

今回はこのへんで。