package top.octopusyan.utils;

import cn.hutool.crypto.ProviderFactory;
import cn.hutool.crypto.SecureUtil;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;

/**
 * @author : octopus yan
 * @email : octopus_yan@foxmail.com
 * @description : 常用加密工具类
 * @create : 2021-03-18 19:03
 */
public class EncryptionUtil {
    private static final String defaultPassword = "yanfrp";
    private static Logger logger = LoggerFactory.getLogger(EncryptionUtil.class);

    private static BASE64Encoder BASE64Encoder;
    private static MessageDigest MessageDigest_SHA1;
    private static MessageDigest MessageDigest_MD5;
    private static Cipher Cipher_ECB_MOB;
    private static Cipher Cipher_ALGORITHM_MODE_PADDING;
    private static Signature Signature_SHA256_RSA;

    /**
     * 密钥算法
     */
    private static final String DES = "DES";
    private static final String AES = "AES";
    private static final String RES = "RES";
    private static final String RSA = "RSA";
    /**
     * 加解密算法/工作模式/填充方式
     */
    private static final String ECB_MOB = "DES/ECB/PKCS5Padding";
    //    private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS7Padding";
    private static final String ALGORITHM_MODE_PADDING = "AES/ECB/PKCS5Padding";

    private static final Charset UTF_8 = StandardCharsets.UTF_8;

    private static final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
            'E', 'F'};
    private static final char hexDigitsLower[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c',
            'd', 'e', 'f'};

    public static void init() {

        BASE64Encoder = new BASE64Encoder();
        try {
            MessageDigest_SHA1 = MessageDigest.getInstance("SHA-1");
            MessageDigest_MD5 = MessageDigest.getInstance("MD5");
            Cipher_ECB_MOB = Cipher.getInstance(ECB_MOB);
            Cipher_ALGORITHM_MODE_PADDING = Cipher.getInstance(ALGORITHM_MODE_PADDING);
            Signature_SHA256_RSA = Signature.getInstance("SHA256WithRSA");
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            e.printStackTrace();
        }

    }

    public static String getDefaultPassword() {
        return getSHA(defaultPassword + getMessageDigestString("frp." + defaultPassword, hexDigits, MessageDigest_SHA1));
    }

    public static String decodeAppData(String str) {
        try {
            return DESdecode(str, getDefaultPassword());
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String encodeAppData(String str) {
        try {
            return DESencode(str, getDefaultPassword());
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static String getSha1(String str) {
        if (str == null || str.length() == 0) {
            return null;
        }
        return getMessageDigestString(str, hexDigitsLower, MessageDigest_SHA1);
    }

    public static String getSHA(String spara) {
        String sRtn;

        byte[] plainText = spara.getBytes(UTF_8);
        // 开始使用算法
        MessageDigest_SHA1.update(plainText);
        // 输出算法运算结果
        sRtn = BASE64Encoder.encode(MessageDigest_SHA1.digest());

        return sRtn;
    }

    /**
     * DES加密
     *
     * @param data     加密内容
     * @param password 秘钥
     */
    public static String DESencode(String data, String password) throws Exception {
        byte[] pasByte;

        Key key = getDESKey(password);
        Cipher_ECB_MOB.init(Cipher.ENCRYPT_MODE, key);
        pasByte = Cipher_ECB_MOB.doFinal(data.getBytes(UTF_8));

        return Base64.encode(pasByte);
    }

    /**
     * DES解密
     *
     * @param data     加密内容
     * @param password 秘钥
     * @return
     */
    public static String DESdecode(String data, String password) throws Exception {
        byte[] pasByte;
        Key key = getDESKey(password);
        Cipher_ECB_MOB.init(Cipher.DECRYPT_MODE, key);
        byte[] x = Base64.decode(data);
        pasByte = Cipher_ECB_MOB.doFinal(x);
        return new String(pasByte, UTF_8);
    }

    private static Key getDESKey(String password) throws Exception {
        byte[] DESkey = password.getBytes(UTF_8);
        DESKeySpec keySpec = new DESKeySpec(DESkey);
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES);
        return keyFactory.generateSecret(keySpec);
    }

    /**
     * AES加密
     *
     * @param data     加密内容
     * @param password 加密密码
     */
    public static String AESencode(String data, String password) throws Exception {
        byte[] result;
        Security.addProvider(ProviderFactory.createBouncyCastleProvider());
        // 创建密码器
        // 初始化为加密模式的密码
        Cipher_ALGORITHM_MODE_PADDING.init(Cipher.ENCRYPT_MODE, getSecretKey(password));
        // 加密
        result = Cipher_ALGORITHM_MODE_PADDING.doFinal(data.getBytes());

        return Base64.encode(result);
    }

    /**
     * AES解密
     *
     * @param base64Data 解密内容
     * @param password   解密密码
     */
    public static String AESdecode(String base64Data, String password) throws Exception {
        byte[] result;
        Security.addProvider(ProviderFactory.createBouncyCastleProvider());
        // 使用密钥初始化，设置为解密模式
        Cipher_ALGORITHM_MODE_PADDING.init(Cipher.DECRYPT_MODE, getSecretKey(password));
        // 执行操作
        result = Cipher_ALGORITHM_MODE_PADDING.doFinal(Base64.decode(base64Data));

        return new String(result, UTF_8);
    }

    /**
     * 生成AES加密秘钥
     *
     * @return
     */
    private static SecretKeySpec getSecretKey(String password) {
        return new SecretKeySpec(SecureUtil.md5(password).toLowerCase().getBytes(), AES);
    }

    /**
     * 对字符串 MD5 无盐值加密
     *
     * @param plainText 传入要加密的字符串
     * @return MD5加密后生成32位(小写字母 + 数字)字符串
     */
    public static String MD5Lower(String plainText) {
        // 使用指定的字节更新摘要
        MessageDigest_MD5.update(plainText.getBytes());
        // digest()最后确定返回md5 hash值，返回值为8位字符串。因为md5 hash值是16位的hex值，实际上就是8位的字符
        // BigInteger函数则将8位的字符串转换成16位hex值，用字符串来表示；得到字符串形式的hash值。1 固定值
        return new BigInteger(1, MessageDigest_MD5.digest()).toString(16);
    }


    /**
     * 对字符串 MD5 加密
     *
     * @param plainText 传入要加密的字符串
     * @return MD5加密后生成32位(大写字母 + 数字)字符串
     */
    public static String MD5Upper(String plainText) {
        // 使用指定的字节更新摘要
        MessageDigest_MD5.update(plainText.getBytes());
        // 获得密文
        byte[] mdResult = MessageDigest_MD5.digest();
        // 把密文转换成十六进制的字符串形式
        return getHexString(hexDigitsLower, mdResult);
    }

    /**
     * 对字符串 MD5 加盐值加密
     *
     * @param plainText 传入要加密的字符串
     * @param saltValue 传入要加的盐值
     * @return MD5加密后生成32位(小写字母 + 数字)字符串
     */
    public static String MD5Lower(String plainText, String saltValue) {
        // 使用指定的字节更新摘要
        MessageDigest_MD5.update(plainText.getBytes());
        MessageDigest_MD5.update(saltValue.getBytes());

        // digest()最后确定返回md5 hash值，返回值为8位字符串。因为md5 hash值是16位的hex值，实际上就是8位的字符
        // BigInteger函数则将8位的字符串转换成16位hex值，用字符串来表示；得到字符串形式的hash值。1 固定值
        return new BigInteger(1, MessageDigest_MD5.digest()).toString(16);
    }

    /**
     * 对字符串 MD5 加盐值加密
     *
     * @param plainText 传入要加密的字符串
     * @param saltValue 传入要加的盐值
     * @return MD5加密后生成32位(大写字母 + 数字)字符串
     */
    public static String MD5Upper(String plainText, String saltValue) {

        // 使用指定的字节更新摘要
        MessageDigest_MD5.update(plainText.getBytes());
        MessageDigest_MD5.update(saltValue.getBytes());

        // 获得密文
        byte[] mdResult = MessageDigest_MD5.digest();
        // 把密文转换成十六进制的字符串形式
        return getHexString(hexDigitsLower, mdResult);

    }

    /**
     * MD5加密后生成32位(小写字母+数字)字符串
     * 同 MD5Lower() 一样
     */
    public static String MD5(String plainText) {
        return getMessageDigestString(plainText, hexDigitsLower, MessageDigest_MD5);
    }

    /**
     * MD5加密后生成16位(小写字母+数字)字符串
     * 同 MD5Lower() 一样
     */
    public static String MD5_16(String plainText) {
        return getMessageDigestString(plainText, hexDigitsLower, MessageDigest_MD5).substring(8, 24);
    }

    /**
     * 校验MD5码
     *
     * @param text 要校验的字符串
     * @param md5  md5值
     * @return 校验结果
     */
    public static boolean MD5valid(String text, String md5) throws Exception {
        return md5.equals(MD5(text)) || md5.equals(MD5(text).toUpperCase());
    }

    /**
     * 校验MD5码
     *
     * @param text 要校验的字符串
     * @param md5  md5值
     * @param salt 加密盐
     * @return 校验结果
     */
    public static boolean MD5valid(String text, String md5, String salt) {
        return md5.equals(MD5Lower(text, salt)) || md5.equals(MD5Upper(text, salt).toUpperCase());
    }

    /**
     * RSA2签名
     *
     * @param content    待签名的字符串
     * @param privateKey rsa私钥字符串
     * @param charset    字符集编码
     * @return 签名结果
     */
    public static String rsaSign(String content, String privateKey, String charset) throws Exception {
        PrivateKey priKey = getPrivateKeyFromPKCS8(RSA, new ByteArrayInputStream(privateKey.getBytes()));
        Signature_SHA256_RSA.initSign(priKey);
        if (StringUtils.isBlank(charset)) {
            Signature_SHA256_RSA.update(content.getBytes());
        } else {
            Signature_SHA256_RSA.update(content.getBytes(charset));
        }
        byte[] signed = Signature_SHA256_RSA.sign();
        return new String(Base64.encode(signed));
    }

    /**
     * RSA2验签
     *
     * @param content   被签名的内容
     * @param sign      签名后的结果
     * @param publicKey rsa公钥
     * @param charset   字符集编码
     * @return 验签结果
     */
    public static boolean doCheck(String content, String sign, String publicKey, String charset) throws Exception {
        PublicKey pubKey = getPublicKeyFromX509(RSA, new ByteArrayInputStream(publicKey.getBytes()));
        Signature_SHA256_RSA.initVerify(pubKey);
        Signature_SHA256_RSA.update(getContentBytes(content, charset));
        return Signature_SHA256_RSA.verify(Base64.decode(sign));
    }

    /**
     * 获取私钥对象
     *
     * @param algorithm 签名方式
     * @param ins       私钥流
     * @return
     * @throws Exception
     */
    private static PrivateKey getPrivateKeyFromPKCS8(String algorithm, InputStream ins) throws Exception {
        if (ins == null || StringUtils.isEmpty(algorithm)) {
            return null;
        }
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        byte[] encodedKey = readText(ins, UTF_8, true).getBytes();
        encodedKey = Base64.decode(Arrays.toString(encodedKey));
        return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
    }

    /**
     * 获取公钥对象
     *
     * @param algorithm 签名方式
     * @param ins       公钥流
     * @return
     * @throws Exception
     */
    private static PublicKey getPublicKeyFromX509(String algorithm, InputStream ins) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
        StringWriter writer = new StringWriter();
        io(new InputStreamReader(ins), writer, true, true);
        byte[] encodedKey = writer.toString().getBytes();
        // 先base64解码
        encodedKey = Base64.decode(Arrays.toString(encodedKey));
        return keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
    }

    /**
     * 获取字符串对应编码的字节
     *
     * @param content 字符串内容
     * @param charset 字符集编码
     * @return
     * @throws UnsupportedEncodingException
     */
    private static byte[] getContentBytes(String content, String charset) throws UnsupportedEncodingException {
        if (StringUtils.isEmpty(charset)) {
            return content.getBytes();
        }
        return content.getBytes(charset);
    }

    /**
     * 将指定输入流的所有文本全部读出到一个字符串中
     *
     * @param in      输入流
     * @param charset 字符集编码
     * @param closeIn 是否关闭流
     * @return
     * @throws IOException
     */
    private static String readText(InputStream in, Charset charset, boolean closeIn) throws IOException {
        Reader reader = charset == null ? new InputStreamReader(in) : new InputStreamReader(in, charset);
        return readText(reader, closeIn);
    }

    /**
     * 将指定<code>Reader</code>的所有文本全部读出到一个字符串中
     *
     * @param in      输入流
     * @param closeIn 是否关闭流
     * @return
     * @throws IOException
     */
    private static String readText(Reader in, boolean closeIn) throws IOException {
        StringWriter out = new StringWriter();
        io(in, out, closeIn, true);
        return out.toString();
    }

    /**
     * 从输入流读取内容，写入到输出流中
     *
     * @param in       输入流
     * @param out      输出流
     * @param closeIn  是否关闭流
     * @param closeOut 是否关闭流
     * @throws IOException
     */
    private static void io(Reader in, Writer out, boolean closeIn, boolean closeOut) {
        int bufferSize = 8192 >> 1;
        char[] buffer = new char[bufferSize];
        int amount;
        try {
            while ((amount = in.read(buffer)) >= 0) {
                out.write(buffer, 0, amount);
            }
            out.flush();
        } catch (Exception e) {
            logger.error("从输入流读取内容，写入到输出流中异常:{}", e.getMessage(), e);
        } finally {
            if (closeIn) {
                try {
                    in.close();
                } catch (IOException e) {
                    logger.error("从输入流读取内容，写入到输出流中异常:{}", e.getMessage(), e);
                }
            }
            if (closeOut) {
                try {
                    out.close();
                } catch (IOException e) {
                    logger.error("从输入流读取内容，写入到输出流中异常:{}", e.getMessage(), e);
                }
            }
        }
    }

    private static String getMessageDigestString(String str, char[] hexDigits, MessageDigest messageDigest) {
        messageDigest.update(str.getBytes(UTF_8));

        byte[] md = messageDigest.digest();
        return getHexString(hexDigits, md);
    }

    private static String getHexString(char[] hexDigits, byte[] md) {
        int j = md.length;
        char[] buf = new char[j * 2];
        int k = 0;
        for (byte byteO : md) {
            buf[k++] = hexDigits[byteO >>> 4 & 0xf];
            buf[k++] = hexDigits[byteO & 0xf];
        }
        return new String(buf);
    }
}

