/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.security;

import javax.crypto.spec.SecretKeySpec;
import javax.crypto.Cipher;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.GeneralSecurityException;	// 5.7.2.1 (2014/01/17)

import java.nio.charset.Charset;				// 5.5.2.6 (2012/05/25)
import java.nio.channels.FileChannel;			// 5.7.2.1 (2014/01/17)
import java.nio.ByteBuffer;						// 5.5.2.6 (2012/05/25)

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

import org.opengion.fukurou.util.Closer;		// 5.5.2.6 (2012/05/25)

/**
 * HybsCryptography は、セキュリティ強化の為の Hybs独自の暗号化クラスです。
 *
 * このクラスは、暗号化キーを受け取り、それに基づいて暗号化/復号化を行います。
 * ここでの暗号化は、秘密キー方式でバイト文字列に変換されたものを、１６進アスキー文字に
 * 直して、扱っています。よって、暗号化/復号化共に、文字列として扱うことが可能です。
 *
 * @og.rev 4.0.0.0 (2005/08/31) 新規追加
 * @og.group ライセンス管理
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public final class HybsCryptography {
	private final SecretKeySpec sksSpec ;
	private static final String CIPHER_TYPE = "Blowfish" ;

	/**
	 * 数字から１６進文字に変換するテーブルです。
	 */
	private static final char[] HEXA_DECIMAL = 
		{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
		  'a', 'b', 'c', 'd', 'e', 'f' };

	/**
	 * プラットフォーム依存のデフォルトの Charset です。
	 * プラットフォーム依存性を考慮する場合、エンコード指定で作成しておく事をお勧めします。
	 *
	 * @og.rev 5.5.2.6 (2012/05/25) findbugs対応
	 */
	private static final Charset DEFAULT_CHARSET = Charset.defaultCharset() ;

	// 注意：秘密キーは、８の倍数でないといけない。
	private static final String HYBS_CRYPT_KEY = "2a5a88891d37ae59" ;

	/**
	 * 内部設定の秘密鍵を使用して，暗号化を行うオブジェクトを構築します。
	 * ここでの暗号化は、Java標準のセキュリティパッケージを使用しています。
	 * 
	 * @og.rev 6.2.5.0 (2015/06/05) 引数付コンストラクタを使用
	 */
	public HybsCryptography() {
//		sksSpec = new SecretKeySpec( HYBS_CRYPT_KEY.getBytes( DEFAULT_CHARSET ), CIPHER_TYPE );		// 5.5.2.6 (2012/05/25) findbugs対応
		this( HYBS_CRYPT_KEY );
	}

	/**
	 * 秘密鍵の文字列を受け取って，暗号化を行うオブジェクトを構築します。
	 * ここでの暗号化は、Java標準のセキュリティパッケージを使用しています。
	 * 秘密鍵のサイズを、8 の倍数 (32 以上 448 以下) にする必要があります。
	 * 
	 * @og.rev 5.8.8.0 (2015/06/05) null時の挙動はデフォルトキーを利用する
	 *
	 * @param	cryptKey	暗号化を行う秘密鍵
	 */
	public HybsCryptography( final String cryptKey ) {
//		sksSpec = new SecretKeySpec( cryptKey.getBytes( DEFAULT_CHARSET ), CIPHER_TYPE );		// 5.5.2.6 (2012/05/25) findbugs対応
		// 5.8.8.0 (2015/06/05) null時はデフォルトキーを利用
		final String useKey;
		if( cryptKey == null || cryptKey.length() == 0 ){
			useKey = HYBS_CRYPT_KEY;
		}
		else{
			useKey = cryptKey;
		}
		sksSpec = new SecretKeySpec( useKey.getBytes( DEFAULT_CHARSET ), CIPHER_TYPE );	
	}

	/**
	 * セキュリティカラムのDBTyepに対してHybs独自の暗号化を行います。
	 * 暗号化されたデータは、通常 byte 文字ですが、１６進数アスキー文字列に変換
	 * したものを返します。
	 * この暗号化では、引数が null の場合は、ゼロ文字列を返します。
	 *
	 * @og.rev 5.7.2.1 (2014/01/17) Exceptionをまとめます。
	 *
	 * @param	org	暗号化を行う元の文字列
	 *
	 * @return	暗号化された文字列(HEXADECIMAL化)
	 * @og.rtnNotNull
	 */
	public String encrypt( final String org ) {
		if( org == null || org.isEmpty() ) { return ""; }

		try {
			final Cipher cipher = Cipher.getInstance( CIPHER_TYPE );
			cipher.init( Cipher.ENCRYPT_MODE, sksSpec );
			final byte[] encrypted = cipher.doFinal( org.getBytes( DEFAULT_CHARSET ) );		// 5.5.2.6 (2012/05/25) findbugs対応

			return byte2hexa( encrypted );
		}
		// 5.7.2.1 (2014/01/17) Exceptionをまとめます。
		catch( GeneralSecurityException	ex ) {
			final String errMsg = "暗号化処理に失敗しました。[" + org + "]"
							+ ex.getMessage() ;
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * セキュリティカラムのDBTyepに対してHybs独自の復号化を行います。
	 * ここでの復号化は、encrypt で暗号化された文字を戻す場合に使用します。
	 * この復号化では、null は復号化できないため、ゼロ文字列を返します。
	 *
	 * @og.rev 5.7.2.1 (2014/01/17) Exceptionをまとめます。
	 *
	 * @param	hex	復号化を行う暗号化された１６進数アスキー文字列
	 *
	 * @return	復号化された元の文字列
	 * @og.rtnNotNull
	 */
	public String decrypt( final String hex ) {
		if( hex == null || hex.isEmpty() ) { return ""; }

		try {
			final Cipher cipher = Cipher.getInstance( CIPHER_TYPE );
			cipher.init( Cipher.DECRYPT_MODE, sksSpec );
			final byte[] encrypted = hexa2byte( hex );
			final byte[] decrypted = cipher.doFinal( encrypted );
			return new String( decrypted,DEFAULT_CHARSET );		// 5.5.2.6 (2012/05/25) findbugs対応
		}
		// 5.7.2.1 (2014/01/17) Exceptionをまとめます。
		catch( GeneralSecurityException	ex ) {
			final String errMsg = "復号化処理に失敗しました。[" + hex + "]"
							+ ex.getMessage() ;
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * バイト配列を１６進数アスキー文字列に変換します。
	 *
	 * バイト配列を、２文字の0～9,a～ｆのアスキーに変換されます。
	 * これにより、すべての文字を、アスキー化できます。
	 * アスキー化で、上位が0F以下の場合でも、0 を出すことで、固定長に変換します。
	 *
	 * よって、入力バイトの２倍のlength()を持ったStringを作成します。
	 *
	 * @param	input バイト配列
	 *
	 * @return	１６進数アスキー文字列
	 */
	public static String byte2hexa( final byte[] input ) {
		String rtn = null;
//		if( input != null ) {
		if( input != null && input.length > 0 ) {
			final int len = input.length ;
			char[] ch = new char[len*2];
			for( int i=0; i<len; i++ ) {
				final int high = (input[i] & 0xf0) >> 4 ;
				final int low  = input[i] & 0x0f ;
				ch[i*2]   = HEXA_DECIMAL[high];
				ch[i*2+1] = HEXA_DECIMAL[low];
			}
			rtn =  new String(ch);
		}
		return rtn;
	}

	/**
	 * １６進数アスキー文字列をバイト配列に変換します。
	 *
	 * ２文字の0～9,a～ｆのアスキー文字列を、バイト配列に変換されます。
	 *
	 * よって、入力Stringの１/２倍のlengthを持ったバイト配列を作成します。
	 *
	 * @param	input １６進数アスキー文字列
	 *
	 * @return	バイト配列
	 */
	public static byte[] hexa2byte( final String input ) {
		byte[] rtn = null;
		if( input != null ) {
			final int len = input.length() ;
			rtn = new byte[len/2];
			for( int i=0; i<len/2; i++ ) {
				char ch = input.charAt( i*2 );
				final int high = ( ch < 'a' ) ? ch-'0' : ch-'a'+10 ;
				ch = input.charAt( i*2+1 );
				final int low  = ( ch < 'a' ) ? ch-'0' : ch-'a'+10 ;
				rtn[i] = (byte)(high << 4 | low);
			}
		}
		return rtn;
	}

	/**
	 * MessageDigestにより、MD5 でハッシュした文字に変換します。
	 *
	 * MD5で、１６Byteのバイトに変換されますが、ここでは、１６進数で文字列に変換しています。
	 *
	 * 変換方法は、各バイトの上位/下位を１６進文字列に変換後、連結しています。
	 * これは、Tomcat等の digest 認証(MD5使用時)と同じ変換方式です。
	 * 連結後の文字列長は、３２バイト(固定)になります。
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) util.StringUtil から移動
	 *
	 * @param	input 変換前の文字列
	 *
	 * @return	MD5でハッシュした文字列。３２バイト(固定)
	 */
	public static String getMD5( final String input ) {
		String rtn = null;
		if( input != null ) {
			try {
				final MessageDigest md5 = MessageDigest.getInstance( "MD5" );
				md5.update( input.getBytes( DEFAULT_CHARSET ) );	// 5.5.2.6 (2012/05/25) findbugs対応
				final byte[] out = md5.digest();
				rtn = byte2hexa( out );
			}
			catch( NoSuchAlgorithmException ex ) {
				final String errMsg = "MessageDigestで失敗しました。[" + input + "]"
							+ ex.getMessage() ;
				throw new RuntimeException( errMsg,ex );
			}
		}
		return rtn;
	}

	/**
	 * MessageDigestにより、MD5 でハッシュした文字に変換します。
	 *
	 * MD5で、１６Byteのバイトに変換されますが、ここでは、１６進数で文字列に変換しています。
	 *
	 * 変換方法は、各バイトの上位/下位を１６進文字列に変換後、連結しています。
	 * これは、Tomcat等の digest 認証(MD5使用時)と同じ変換方式です。
	 * 連結後の文字列長は、３２バイト(固定)になります。
	 *
	 * @og.rev 5.7.2.1 (2014/01/17) Exceptionをまとめます。
	 *
	 * @param	input 変換前のFile
	 *
	 * @return	MD5でハッシュした文字列。３２バイト(固定)
	 */
	public static String getMD5( final File input ) {
		String rtn = null;
		if( input != null ) {
			FileInputStream	fis	= null;
			FileChannel		fc	= null;
			try {
				final MessageDigest md5 = MessageDigest.getInstance( "MD5" );
				fis = new FileInputStream( input );
				fc  =fis.getChannel();
				final ByteBuffer bb = fc.map( FileChannel.MapMode.READ_ONLY , 0L , fc.size() );
				md5.update( bb );
				final byte[] out = md5.digest();
				rtn = byte2hexa( out );
			}
			catch( NoSuchAlgorithmException ex ) {
				final String errMsg = "MessageDigestで MD5 インスタンスの作成に失敗しました。[" + input + "]"
							+ ex.getMessage() ;
				throw new RuntimeException( errMsg,ex );
			}
			catch( IOException ex ) {
				final String errMsg = "ファイルの読み取りを失敗しました。[" + input + "]"
							+ ex.getMessage() ;
				throw new RuntimeException( errMsg,ex );
			}
			finally {
				Closer.ioClose( fc );
				Closer.ioClose( fis );
			}
		}
		return rtn;
	}

	/**
	 * 暗号化のテストを行う為のメインメソッド
	 *
	 * java HybsCryptography KEY TEXT で起動します。
	 *   KEY  : 秘密鍵(8 の倍数 (32 以上 448 以下)文字)
	 *   TEXT : 変換する文字列
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) 循環参照の解消(LogWriter 削除)
	 *
	 * @param	args	引数配列
	 */
	public static void main( final String[] args ) {
		if( args.length != 2 ) {
			System.out.println( "java HybsCryptography KEY TEXT" );
			System.out.println( "  KEY  : 秘密鍵(8 の倍数 (32 以上 448 以下)文字)" );
			System.out.println( "  TEXT : 変換する文字列" );
			return;
		}

		final HybsCryptography cript = new HybsCryptography( args[0] );

		System.out.println( "IN   TEXT : " + args[1] );

		final String hexa = cript.encrypt( args[1] );
		System.out.println( "HEXA TEXT : " + hexa );

		final String data = cript.decrypt( hexa );
		System.out.println( "OUT  DATA : " + data );
	}
}
