package com.nifty.cloud.ncs.encrypt;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import net.arnx.jsonic.JSON;

import org.apache.commons.codec.binary.Base64;

import com.nifty.cloud.ncs.NiftyCloudAdjustedRangeInputStream;
import com.nifty.cloud.ncs.NiftyCloudClientException;
import com.nifty.cloud.ncs.NiftyCloudInputStream;
import com.nifty.cloud.ncs.service.NcsObject;
import com.nifty.cloud.ncs.service.StorageHeadersImpl;


/**
 * 暗号化に使用するユーティリティクラスです。
 */
public class EncryptionUtils
{
	/**
	 * EncryptionInstructionオブジェクトを生成します。
	 *
	 * @param materials
	 *  暗号鍵情報
	 * @param cryptoProvider
	 *  暗号化プロバイダー
	 * @return 暗号化情報
	 */
	public static EncryptionInstruction generateInstruction(EncryptionKeyMaterials materials, Provider cryptoProvider)
	{
		// 対称鍵を生成
		SecretKey envelopeSymmetricKey = generateOneTimeUseSymmetricKey();
		// 対称暗号生成
		Cipher symmetricCipher = createSymmetricCipher(envelopeSymmetricKey, Cipher.ENCRYPT_MODE, cryptoProvider, null);

		// 対称鍵を暗号化
		byte[] encryptedEnvelopeSymmetricKey = getEncryptedSymmetricKey(envelopeSymmetricKey, materials, cryptoProvider);

		return (new EncryptionInstruction(materials.getMaterialsDescription(), encryptedEnvelopeSymmetricKey,
				envelopeSymmetricKey, symmetricCipher));
	}

	/**
	 * InstructionFileからEncryptionInstructionオブジェクトを生成します。
	 *
	 * @param instructionFile
	 *  暗号化情報
	 * @param materials
	 *  暗号鍵情報
	 * @param cryptoProvider
	 *  暗号化プロバイダー
	 * @return 暗号化情報
	 */
	public static EncryptionInstruction buildInstructionFromInstructionFile(NcsObject instructionFile, EncryptionKeyMaterials materials, Provider cryptoProvider)
	{
		try {
			Map<String,String> instructionJSON = JSON.decode(instructionFile.getContent());

			// Instructionファイルの内容を取得
			byte[] encryptedSymmetricKeyBytes = instructionJSON.get(StorageHeadersImpl.ENCRYPTION_KEY).getBytes();
			byte[] initVectorBytes = instructionJSON.get(StorageHeadersImpl.ENCRYPTION_IV).getBytes();
			String materialsDescriptionString = instructionJSON.get(StorageHeadersImpl.ENCRYPTION_MATERIALS_DESCRIPTION);
			Map<String, String> materialsDescription = JSON.decode(materialsDescriptionString);

			// 暗号化情報はBase64エンコードされているのでデコード
			encryptedSymmetricKeyBytes = Base64.decodeBase64(encryptedSymmetricKeyBytes);
			initVectorBytes = Base64.decodeBase64(initVectorBytes);

			if (encryptedSymmetricKeyBytes == null || initVectorBytes == null || materialsDescription == null) {
				// 暗号化情報を取得できなかった場合はエラー
				throw new NiftyCloudClientException(
						String.format("Necessary encryption info not found in the instruction file '%s' in bucket '%s'",
								instructionFile.getObjectName(), instructionFile.getBucketName()));
			}

			// 暗号化鍵情報のdescriptionがオリジナルと異なる場合、エラー
			if (!materialsDescription.equals(materials.getMaterialsDescription())) {
				throw ( new NiftyCloudClientException(
						String.format("Unable to retrieve the encryption materials that originally " +
								"encrypted object corresponding to instruction file '%s' in bucket '%s'.",
								instructionFile.getObjectName(), instructionFile.getBucketName())) );
			}

			// 暗号化された対称鍵を復号化
			SecretKey symmetricKey = getDecryptedSymmetricKey(encryptedSymmetricKeyBytes, materials, cryptoProvider);
			// 復号化した対称鍵から対称暗号生成
			Cipher cipher = createSymmetricCipher(symmetricKey, Cipher.DECRYPT_MODE, cryptoProvider, initVectorBytes);

			return (new EncryptionInstruction(materialsDescription, encryptedSymmetricKeyBytes, symmetricKey, cipher));
		} catch (Exception e) {
			throw (new NiftyCloudClientException("Unable to parse retrieved instruction file : " + e.getMessage()));
		}
	}

	/**
	 * メタデータからInstructionオブジェクトを生成します。
	 *
	 * @param object
	 *  メタデータを含むNcsObject
	 * @param materials
	 *  暗号鍵情報
	 * @param cryptoProvider
	 *  暗号化プロバイダー
	 * @return 暗号化情報
	 */
	public static EncryptionInstruction buildInstructionFromMetadata(NcsObject object, EncryptionKeyMaterials materials, Provider cryptoProvider)
	{
		// メタデータから暗号化情報を取得
		byte[] encryptedSymmetricKeyBytes = getCryptoBytesFromMetadata(StorageHeadersImpl.ENCRYPTION_KEY, object);
		byte[] initVectorBytes = getCryptoBytesFromMetadata(StorageHeadersImpl.ENCRYPTION_IV, object);
		String materialsDescriptionString = getStringFromMetadata(StorageHeadersImpl.ENCRYPTION_MATERIALS_DESCRIPTION, object);
		Map<String, String> materialsDescription = JSON.decode(materialsDescriptionString);

		if (encryptedSymmetricKeyBytes == null || initVectorBytes == null || materialsDescription == null) {
			// 暗号化情報が取得できない場合はエラー
			throw (new NiftyCloudClientException(
					String.format("Necessary encryption info not found in the headers of file '%s' in bucket '%s'",
							object.getObjectName(), object.getBucketName())));
		}

		// 暗号化鍵情報のdescriptionがオリジナルと異なる場合、エラー
		if (!materialsDescription.equals(materials.getMaterialsDescription())) {
			throw ( new NiftyCloudClientException(
					String.format("Unable to retrieve the encryption materials that originally " +
							"encrypted file '%s' in bucket '%s'.",
							object.getObjectName(), object.getBucketName())) );
		}

		// 暗号化されたキーを復号化
		SecretKey symmetricKey = getDecryptedSymmetricKey(encryptedSymmetricKeyBytes, materials, cryptoProvider);
		// 復号化した対称鍵から対称暗号生成
		Cipher cipher = createSymmetricCipher(symmetricKey, Cipher.DECRYPT_MODE, cryptoProvider, initVectorBytes);

		return (new EncryptionInstruction(materialsDescription, encryptedSymmetricKeyBytes, symmetricKey, cipher));
	}

	/**
	 * Instructionファイルを使用してオブジェクトデータを復号化します。
	 *
	 * @param object
	 *  複合化するNcsObject
	 * @param instruction
	 *  暗号化情報
	 * @return 複合化したNcsObject
	 */
	public static NcsObject decryptObjectUsingInstruction(NcsObject object, EncryptionInstruction instruction)
	{
		InputStream decryptedInputStream = new CipherInputStream(object.getContent(), instruction.getSymmetricCipher());
		object.setContent(decryptedInputStream);
		return object;
	}

	/**
	 * 暗号化情報がInstructionファイルに含まれるかどうかの判定を行います。
	 *
	 * @param instructionFile
	 *  Instructionファイル
	 * @return 暗号化情報がInstructionファイルに含まれるかどうかのフラグ
	 */
	public static boolean isEncryptionInfoInInstructionFile(NcsObject instructionFile)
	{
		if (instructionFile == null) {
			return (false);
		}

		Map<String, String> metadata = instructionFile.getUserMetadata();
		if (metadata == null) {
			return (false);
		}

		return (metadata.containsKey(StorageHeadersImpl.ENCRYPTION_INSTRUCTION_FILE));
	}

	/**
	 * 暗号化情報がメタデータに含まれるかどうかの判定を行います。
	 *
	 * @param object
	 *  対象のNcsObjcet
	 * @return 暗号化情報がメタデータに含まれるかどうかのフラグ
	 */
	public static boolean isEncryptionInfoInMetadata(NcsObject object)
	{
		Map<String, String> metadata = object.getUserMetadata();
		if (metadata == null) {
			return false;
		}

		if (metadata.containsKey(StorageHeadersImpl.ENCRYPTION_IV) &&
				metadata.containsKey(StorageHeadersImpl.ENCRYPTION_KEY) &&
				metadata.containsKey(StorageHeadersImpl.ENCRYPTION_MATERIALS_DESCRIPTION)) {
			return (true);
		}

		return (false);
	}

	/**
	 * 暗号化のために調整されたrangeを返却します。
	 *
	 * @param range
	 *  オブジェクトの取得範囲
	 * @return 調整後のrange
	 */
	public static long[] getAdjustedCryptoRange(long[] range)
	{
		// Range妥当性確認
		if (range == null || range[0] > range[1] || range[0] < 0 || range[1] < 0) {
			return (range);
		}

		long lowerBound;
		long upperBound;
		long offset;
		long cipherBlockSize = EncryptionConstants.SYMMETRIC_CIPHER_BLOCK_SIZE;

		// Rangeの開始位置
		offset = range[0] % cipherBlockSize;
		lowerBound = range[0] - offset - cipherBlockSize;
		if (lowerBound < 0) {
			lowerBound = 0;
		}

		// Rangeの終了位置
		offset = cipherBlockSize - (range[1] % cipherBlockSize);
		upperBound = range[1] + offset + cipherBlockSize;

		long[] adjustedCryptoRange = new long[2];
		adjustedCryptoRange[0] = lowerBound;
		adjustedCryptoRange[1] = upperBound;

		return (adjustedCryptoRange);
	}

	/**
	 * 暗号化のためにオブジェクトデータのrangeを調整したNcsObjectを返却します。
	 *
	 * @param object
	 *  範囲調整の対象となるNcsObject
	 * @param range
	 *  取得範囲
	 * @return rangeを調整したNcsObject
	 */
	public static NcsObject getRangeAdjustedObject(NcsObject object, long[] range)
	{
		if (range == null || range[0] > range[1]) {
			return (object);
		} else {
			try {
				InputStream adjustedRangeContents = new NiftyCloudAdjustedRangeInputStream(object.getContent(), range[0], range[1]);
				object.setContent(adjustedRangeContents);

				return (object);

			} catch (IOException e) {
				throw new NiftyCloudClientException("Error adjusting output to desired byte range: " + e.getMessage());
			}
		}
	}

	/**
	 * 暗号化に使用するワンタイム・キーを生成します。
	 *
	 * @return ワンタイム・キー
	 */
	public static SecretKey generateOneTimeUseSymmetricKey()
	{
		KeyGenerator generator;
		try {
			generator = KeyGenerator.getInstance(EncryptionConstants.SYMMETRIC_KEY_ALGORITHM_AES);
			generator.init(EncryptionConstants.SYMMETRIC_KEY_LENGTH_256, new SecureRandom());

			return (generator.generateKey());

		} catch (NoSuchAlgorithmException e) {
			throw new NiftyCloudClientException("Unable to generate envelope symmetric key:" + e.getMessage(), e);
		}
	}

	/**
	 * 対称鍵と初期化ベクトル、モードを指定して、対称鍵暗号を作成します。
	 * 特定の暗号化プロバイダは、暗号化の実装を提供します。
	 * 暗号化プロバイダがnullの場合、デフォルトJCE暗号化プロバイダが使用されます。
	 *
	 * @param symmetricKey
	 *  対称鍵
	 * @param encryptMode
	 *  暗号化モード
	 * @param cryptoProvider
	 *  暗号化プロバイダー
	 * @param initVector
	 *  初期化ベクトル
	 * @return 暗号
	 */
	public static Cipher createSymmetricCipher(SecretKey symmetricKey, int encryptMode, Provider cryptoProvider, byte[] initVector)
	{
		try {
			Cipher cipher;
			if (cryptoProvider != null) {
				cipher = Cipher.getInstance(EncryptionConstants.SYMMETRIC_CIPHER_METHOD, cryptoProvider);
			} else {
				cipher = Cipher.getInstance(EncryptionConstants.SYMMETRIC_CIPHER_METHOD);
			}
			if (initVector != null) {
				cipher.init(encryptMode, symmetricKey, new IvParameterSpec(initVector));
			} else {
				cipher.init(encryptMode, symmetricKey);
			}

			return (cipher);

		} catch (Exception e) {
			throw (new NiftyCloudClientException("Unable to build cipher with the provided algorithm and padding: " + e.getMessage(), e));
		}
	}

	/**
	 * 秘密鍵により暗号化された対称鍵のバイト列を取得します。
	 *
	 * @param toBeEncrypted
	 *  暗号化の対象となる対称鍵
	 * @param materials
	 *  暗号鍵情報
	 * @param cryptoProvider
	 *  暗号化プロバイダー
	 * @return 暗号化された対称鍵のバイト列
	 */
	public static byte[] getEncryptedSymmetricKey(SecretKey toBeEncrypted, EncryptionKeyMaterials materials, Provider cryptoProvider)
	{
		Key encryptionKey;
		if (materials.getKeyPair() != null) {
			encryptionKey = materials.getKeyPair().getPublic();
		} else {
			encryptionKey = materials.getEncryptionKey();
		}

		try {
			Cipher cipher;
			byte[] toBeEncryptedBytes = toBeEncrypted.getEncoded();
			if (cryptoProvider != null) {
				cipher = Cipher.getInstance(encryptionKey.getAlgorithm(), cryptoProvider);
			} else {
				cipher = Cipher.getInstance(encryptionKey.getAlgorithm()); // デフォルトではJCEプロバイダを使用
			}
			cipher.init(Cipher.ENCRYPT_MODE, encryptionKey);

			return (cipher.doFinal(toBeEncryptedBytes));

		} catch (Exception e) {
			throw (new NiftyCloudClientException("Unable to com.nifty.cloud.ncs.encrypt symmetric key: " + e.getMessage(), e));
		}
	}

	/**
	 * 秘密鍵によって復号化された対称鍵を取得します。
	 *
	 * @param encryptedSymmetricKeyBytes
	 * @param materials
	 * @param cryptoProvider
	 * @return
	 */
	private static SecretKey getDecryptedSymmetricKey(byte[] encryptedSymmetricKeyBytes, EncryptionKeyMaterials materials,	Provider cryptoProvider)
	{
		Key decryptionKey;
		if (materials.getKeyPair() != null) {
			decryptionKey = materials.getKeyPair().getPrivate();
		} else {
			decryptionKey = materials.getEncryptionKey();
		}
		try {
			Cipher cipher;
			if (cryptoProvider != null) {
				cipher = Cipher.getInstance(decryptionKey.getAlgorithm(), cryptoProvider);
			} else {
				cipher = Cipher.getInstance(decryptionKey.getAlgorithm());
			}
			cipher.init(Cipher.DECRYPT_MODE, decryptionKey);
			byte[] decryptedSymmetricKeyBytes = cipher.doFinal(encryptedSymmetricKeyBytes);

			return (new SecretKeySpec(decryptedSymmetricKeyBytes, EncryptionConstants.SYMMETRIC_KEY_ALGORITHM_AES));
		} catch (Exception e) {
			throw (new NiftyCloudClientException("Unable to decrypt symmetric key from object metadata : " + e.getMessage(), e));
		}
	}

	/**
	 * 指定した暗号で暗号化されたオブジェクトデータのストリームを取得します。
	 *
	 * @param input
	 *  暗号化の対象となる入力ストリーム
	 * @param symmetricCipher
	 *  対称暗号
	 * @return 暗号化ストリーム
	 */
	public static InputStream getEncryptedInputStream(InputStream input, Cipher symmetricCipher)
	{
		try {
			InputStream originalInputStream = input;
			originalInputStream = new NiftyCloudInputStream(originalInputStream, 40960);
			return (new CipherInputStream(originalInputStream, symmetricCipher));

		} catch (Exception e) {
			throw (new NiftyCloudClientException("Unable to create cipher input stream: " + e.getMessage(), e));
		}
	}

	/**
	 * Base64エンコードされた暗号化情報をデコードして、バイト列としてメタデータから取得します。
	 *
	 * @param headerName
	 * @param object
	 * @return
	 * @throws NullPointerException
	 */
	private static byte[] getCryptoBytesFromMetadata(String headerName, NcsObject object) throws NullPointerException
	{
		Map<String, String> userMetadata = object.getUserMetadata();

		if (userMetadata == null || !userMetadata.containsKey(headerName)) {
			return (null);
		} else {
			byte[] valueBytes = userMetadata.get(headerName).getBytes();
			// Convert Base64 bytes to binary data.
			return (Base64.decodeBase64(valueBytes));
		}
	}

	/**
	 * 暗号化情報を文字列としてメタデータから取得します。
	 *
	 * @param headerName
	 * @param object
	 * @return
	 * @throws NullPointerException
	 */
	private static String getStringFromMetadata(String headerName, NcsObject object) throws NullPointerException
	{
		Map<String, String> userMetadata = object.getUserMetadata();
		if (userMetadata == null || !userMetadata.containsKey(headerName)) {
			return ("");
		} else {
			return (userMetadata.get(headerName));
		}
	}

	/**
	 * Instructionファイルの内容をメタデータに反映します。
	 *
	 * @param object
	 *  対象となるNcsObject
	 * @param instruction
	 *  暗号化情報
	 */
	public static void updateMetadataWithEncryptionInstruction(NcsObject object, EncryptionInstruction instruction)
	{
		byte[] encryptedKeyBytes = instruction.getEncryptedSymmetricKey();
		Cipher symmetricCipher = instruction.getSymmetricCipher();
		Map<String, String> materialsDescription = instruction.getMaterialsDescription();

		// 暗号化された対称鍵
		if (encryptedKeyBytes != null) {
			encryptedKeyBytes = Base64.encodeBase64(encryptedKeyBytes);
			object.addUserMetadata(StorageHeadersImpl.ENCRYPTION_KEY, new String(encryptedKeyBytes));
		}

		// 初期化ベクトル
		byte[] initVectorBytes = symmetricCipher.getIV();
		initVectorBytes = Base64.encodeBase64(initVectorBytes);
		object.addUserMetadata(StorageHeadersImpl.ENCRYPTION_IV, new String(initVectorBytes));

		// 暗号鍵情報のdescription
		String descriptionJSON = JSON.encode(materialsDescription);
		object.addUserMetadata(StorageHeadersImpl.ENCRYPTION_MATERIALS_DESCRIPTION, descriptionJSON);
	}

	/**
	 * 暗号化後のデータサイズを算出します。
	 *
	 * @param symmetricCipher
	 *  対称暗号
	 * @param object
	 *  対象となるNcsObject
	 * @return 暗号化後のContent-Length
	 */
	public static long calculateCryptoContentLength(Cipher symmetricCipher, NcsObject object)
	{
		long plaintextLength;
		plaintextLength = object.getContentLength();

		long cipherBlockSize = symmetricCipher.getBlockSize();
		long offset = cipherBlockSize - (plaintextLength % cipherBlockSize);

		return (plaintextLength + offset);
	}

	/**
	 * Instructionファイルの情報を含むNcsObjcetを生成します。
	 *
	 * @param bucketName
	 *  バケット名
	 * @param objectName
	 *  オブジェクト名
	 * @param instruction
	 *  暗号化情報
	 * @return 暗号化情報を含むNcsObject
	 */
	public static NcsObject createInstructionRequestObject(String bucketName, String objectName, EncryptionInstruction instruction)
	{
		NcsObject instrObj = new NcsObject(bucketName, objectName + EncryptionConstants.INSTRUCTION_SUFFIX);

		// Instructionファイルのアップロード
		byte[] instructionBytes = convertInstructionToJSONString(instruction).getBytes();
		instrObj.setContentLength(instructionBytes.length);
		instrObj.setContentType("application/json");
		instrObj.setContent(new ByteArrayInputStream(instructionBytes));
		// Instructionファイル識別用メタデータ
		instrObj.addUserMetadata(StorageHeadersImpl.ENCRYPTION_INSTRUCTION_FILE, "yes");

		return (instrObj);
	}

	/**
	 * InstructionファイルのSHA256ハッシュ値を計算します
	 *  オブジェクト名
	 * @param instruction
	 *  暗号化情報
	 * @return InstructionファイルのSHA256ハッシュ値
	 */
	public static byte[] computeInstructionFileSHA256(EncryptionInstruction instruction)
	{
		try {
			byte[] instructionBytes = convertInstructionToJSONString(instruction).getBytes();

			MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
			messageDigest.update(instructionBytes, 0, instructionBytes.length);
			return messageDigest.digest();
		} catch (Exception e){
			throw (new NiftyCloudClientException("Can't calculate SHA256 hash: " + e.getMessage(), e));
		}
	}

	/**
	 * EncryptionInstructionをJSON形式の文字列に変換します。
	 *
	 * @param instruction
	 *  暗号化情報
	 * @return 暗号化情報をJSON形式に変換した文字列
	 */
	public static String convertInstructionToJSONString(EncryptionInstruction instruction)
	{
		String json = null;

		try {
			String materialsDescriptionJSON = JSON.encode(instruction.getMaterialsDescription());
			byte[] initVector = instruction.getSymmetricCipher().getIV();
			initVector = Base64.encodeBase64(initVector);
			byte[] encryptedKeyBytes = instruction.getEncryptedSymmetricKey();
			encryptedKeyBytes = Base64.encodeBase64(encryptedKeyBytes);

			Map<String,String> instructionJSON = new HashMap<String,String>();

			instructionJSON.put(StorageHeadersImpl.ENCRYPTION_MATERIALS_DESCRIPTION, materialsDescriptionJSON);
			instructionJSON.put(StorageHeadersImpl.ENCRYPTION_KEY, new String(encryptedKeyBytes));
			instructionJSON.put(StorageHeadersImpl.ENCRYPTION_IV, new String(initVector));

			json = JSON.encode(instructionJSON);

		} catch (Exception e) {
			throw (new NiftyCloudClientException("Unable to convert instruction to json."));
		}

		return (json);
	}

}
