話題の記事

【即席】コピペでできる暗号化/符号化処理 Java編【3日目】

2012.12.03

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。いつの間にか、サーバー側のこむろです。

最近、クライアント側も高性能になってきたため、色々な大事な情報を格納したりすることも増えてきました。またサーバーとの通信によって最新の情報を取得することも多いです。そんな中、暗号化や符号化は重要な技術ですが、自分で実装するのはなかなか骨が折れます。

しかし、最近のプログラム言語では、1から自分で実装しなくても、便利なクラスやライブラリが含まれていることが多いと思います。今回は自分の備忘録も含めて、自分のよく使う暗号化やら符号化やらのプログラムの断片をまとめてみました

今回は、Javaを例にサンプルコードをざざっと記述してみました。Javaには元から色々な便利な暗号に関するクラスやライブラリが用意されているので、存分に利用していきたいと思います。

Base64 Encode/Decode

バイナリ等のダメ文字を含むデータを、64種類の印字可能文字列に置き換えてくれます。標準では入ってませんが、Apache commons library等のライブラリを使うことで実現できます。アルゴリズムは比較的容易なので、自前で実装することも可能ですが、結構めんどうです。

Apache commons codec libraryを利用したBase64エンコード

/**
 * Base64エンコードを行う.
 * 
 * @param bytes Base64エンコードを施す対象データ
 * @return Base64エンコード処理後の文字列
 */
public static String base64Encode(byte[] bytes) {
	return (new Base64()).encodeToString(bytes);
}

実行結果は次の通り

Plain data : くらすめそっど クラス?メ&%ソッド classmethod
Encode to Base64 : 44GP44KJ44GZ44KB44Gd44Gj44GpIOOCr+ODqeOCuT/jg6EmJeOCveODg+ODieOAgGNsYXNzbWV0aG9k

Base64デコード

/**
 * Base64デコードを行う.
 * 
 * @param encoded Base64エンコード後の文字列.
 * @return デコード後の文字列.
 * @throws UnsupportedEncodingException 不正な文字エンコードを指定した場合にthrowされる.
 */
public static String base64Decode(String encoded) throws UnsupportedEncodingException {
	byte[] buff = (new Base64()).decode(encoded);
	return new String(buff, "utf-8");
}

実行結果

Plain data : くらすめそっど クラス?メ&%ソッド classmethod
Encode to Base64 : 44GP44KJ44GZ44KB44Gd44Gj44GpIOOCr+ODqeOCuT/jg6EmJeOCveODg+ODieOAgGNsYXNzbWV0aG9k
Decode from Base64 : くらすめそっど クラス?メ&%ソッド classmethod

Apache commons library

sun.misc.BASE64Encoderは、非推奨のパッケージである警告が出されるので使用しません。今回は、Apache Commons Code Library 1.7を利用しています。1.3等の古いライブラリの場合は、encodeToString等のメソッドが存在しないので注意してください

URL Encode/Decode

URLに文字列として使用可能なように、空白やら記号やらのダメ文字を変換してあげます

URLEncode. URLに使えるようにダメ文字を変換

URLEncoder.encode(source, enc);

sourceが変換したい文字列。encは、文字エンコード(大体utf-8で良いのではないかと・・・)

URLDecode. URLEncodeされた文字列を復元

URLDecoder.decode(source, enc);

URLEncoder

秘密鍵を使って一意なキーを生成

秘密鍵とパスフレーズを使って、一意なキーを生成します。認証キーの生成などに利用されるようです。

利用シーンは、ユーザーの認証に使うとかでしょうか。利用できるアルゴリズムは、「Java ™ 暗号化アーキテクチャー標準アルゴリズム名のドキュメント」を参考にしてください

署名キーと対象文字列から、一意な認証キーを生成

/**
 * 認証キーの作成
 * @param target 認証元の文字列
 * @param signatureKey 認証キーを作成する署名キー
 * @param algorithm アルゴリズム <br>
 * AES, ARCFOUR, Blowfish, DES, DESede, HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512 が利用可
 * @return 生成した認証キー
 * @throws NoSuchAlgorithmException 存在しないアルゴリズムの場合throw
 * @throws InvalidKeyException "Message Authentication Code" (MAC) algorithmに適さないKeyを指定するとthrow
 */
public static String generateAuthenticationKey(String target,
		String signatureKey, String algorithm)
		throws NoSuchAlgorithmException, InvalidKeyException {
	// 秘密鍵の作成
	SecretKey secretKey = new SecretKeySpec(signatureKey.getBytes(),
			algorithm);

	// 認証キーの作成
	Mac mac = Mac.getInstance(algorithm);
	mac.init(secretKey);
	mac.update(target.getBytes());
	
	// 暗号化
	byte[] encryptedData = mac.doFinal();
	
	// base64エンコード
	return base64Encode(encryptedData);
}

実行結果

アルゴリズム : HmacSHA256
対象文字列 : ももんがー もっモンガー momonga, 署名キー : 8$ijaijeoriaogsufhw9ur09bsor0iq0AJ0Ohew9rhserijsogibs0f$rsrs
認証キー : 2EX8IrvAasHkDGu++gTJ5QLgHNJ1fYgXxRONY1hzG1k=

MessageDigest

いわゆるハッシュ値を扱えるクラスです。MD5やらSHAやらに代表される不可逆な固定長HASH値を生成できます。ファイルの中身が正しいかをチェックするのに、checksumとして使われてたりするあれです

MD5生成

/**
 * MD5変換.
 * @param source 変換したい文字列
 * @return MD5変換後の文字列
 * @throws NoSuchAlgorithmException
 * @throws IllegalArgumentException
 */
public static String convertToMD5(String source)
		throws NoSuchAlgorithmException, IllegalArgumentException {
	if (source == null || source.length() == 0) {
		throw new IllegalArgumentException("You must input String");
	}

	MessageDigest md = MessageDigest.getInstance("MD5");
	md.update(source.getBytes());
	byte[] hash = md.digest();

	return hashByteToMD5(hash);
}

/**
 * Byte配列を文字列に変換する. 0x10以下の時は、0をつけて表示する.
 * @param hash MD5変換後のbyte配列
 * @return MD5変換後の文字列
 */
public static String hashByteToMD5(byte[] hash) {
	StringBuilder builder = new StringBuilder();
	for (int idx = 0; idx < hash.length; idx++) {
		if ((0xff & hash[idx]) < 0x10) {
			builder.append("0" + Integer.toHexString((0xff & hash[idx])));
		} else {
			builder.append(Integer.toHexString((0xff & hash[idx])));
		}
	}
	return builder.toString();
}
[/java]
<p>実行結果</p>


<p>とりあえず要件は満たしました。単なる文字列とかの場合は問題ありませんが、ファイルのチェックサムとして利用したい場合は、少々不便です。</p>
<p>ファイルを読み込んだ後、いちいち文字列やbyte配列にするのもバカらしいので、ファイルを読み込みながらMD5も生成します。</p>
<h4>ファイルを読みながらMD5ハッシュ値を生成</h4>

private static final int BUFFER_SIZE = 256;
/**
 * ファイルを読み込みながら、MD5も生成する.
 * @param is ファイル読み込みのInputStreamオブジェクト
 * @param available 確保するメモリサイズ.
 * @return ファイルの中身とMD5文字列を格納したDTO
 * @throws NoSuchAlgorithmException アルゴリズムが発見できない
 * @throws IOException ファイル読み込みエラー
 */
public static FileDto readInputStreamMd5(InputStream is, int available)
		throws NoSuchAlgorithmException, IOException {

	FileDto result = new FileDto();
	ByteArrayOutputStream os = new ByteArrayOutputStream();
	if (is == null) {
		throw new IllegalArgumentException("InputStream must not null");
	}

	try {
		MessageDigest md = MessageDigest.getInstance("MD5");
		is = new DigestInputStream(is, md);

		byte[] buffer = new byte[BUFFER_SIZE];
		while (is.read(buffer, 0, buffer.length) != -1) {
			os.write(buffer, 0, buffer.length);
		}
		os.flush();
		
		// MD5を取得.
		byte[] hash = md.digest();

		result.setBuffer(os.toByteArray());
		result.setMd5(hashByteToMD5(hash));
	} finally {
		if(os != null) {
			os.close();
		}
		is.close();
	}
	return result;
}

DTOクラス

public class FileDto {
	private byte[] buffer;
	private String md5;
	
	public void setBuffer(byte[] buffer) {
		this.buffer = buffer;
	}
	
	public byte[] getBuffer() {
		return this.buffer;
	}
	
	public void setMd5(String md5) {
		this.md5 = md5;
	}
	
	public String getMd5() {
		return this.md5;
	}
}

実行結果

Input File Name : /Users/komuro/Downloads/test_image.jpg
ReadByteSize : 82401
MD5 : ace7c2b3122ca11025d1e9d6f631ce97

引数で渡されたInputStreamは読み込み完了後には、最後までシークしてしまうので、メソッドの呼び出し後、再度利用しようとしても使えません。markとresetを使って再利用することもできますが、今回はなしで。興味のある方は、AWS SDKのRepeatableInputStreamクラスが結構実装の参考になりますので、ご参考に。RepatableInputStream

ByteArrayOutputStreamを利用して書き出してますが、Fileに落としたければ、FileOutputStreamやFileChannelを使って書き込んであげればOKです。

ここで紹介したのはほんの一例です。もっとたくさんの暗号処理や符号化処理が存在するので、パッケージの中身を探索してみると面白いかもしれません。

特に暗号化の部分は、数学者が研究してアルゴリズムとして担保されているものが簡単に利用できるよう実装されているので、数式の意味は分からずとも、処理の中身を読むだけでもなかなか良い勉強になると思います。

参考資料