AES 工具包

CreatedAt: 20200813
JDK Version: Oracle JDK 1.8.0_202

package com.mrathena.toolkit;

import com.mrathena.exception.ServiceException;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Base64;
import java.util.UUID;

/**
 * AES 加密工具
 * <p>
 * 密钥加密/密钥加密
 * 密钥转换/密钥生成
 * <p>
 * *******************************************
 * 字符串格式的密钥在没有特殊说明时都为BASE64编码格式 *
 * 字符串格式的密文在没有特殊说明时都为BASE64编码格式 *
 * *******************************************
 * <p>
 * Java中AES加密默认使用AES/ECB/PKCS5Padding, 即 Cipher.getInstance("AES") 等效于 Cipher.getInstance("AES/ECB/PKCS5Padding")
 * <p>
 * AES密钥长度128/192/256,其中192与256需要配置无政策限制权限文件
 * 填充模式最常用的两种PKCS5Padding和PKCS7Padding,其中后者只有BC独有。
 * <p>
 * CBC加密模式,该模式需要一个初始向量
 * 服务端和我们客户端必须使用一样的密钥和初始向量IV。
 * 服务端和我们客户端必须使用一样的加密模式。
 * 服务端和我们客户端必须使用一样的Padding模式。
 * <p>
 * 要理解AES的加密流程,会涉及到AES加密的五个关键词,分别是:分组密码体制、Padding、密钥、初始向量IV和四种加密模式(不确定是否有更多)
 * 分组密码体制:所谓分组密码体制就是指将明文切成一段一段的来加密,然后再把一段一段的密文拼起来形成最终密文的加密方式。AES采用分组密码体制,即AES加密会首先把明文切成一段一段的,而且每段数据的长度要求必须是128位16个字节,如果最后一段不够16个字节了,就需要用Padding来把这段数据填满16个字节,然后分别对每段数据进行加密,最后再把每段加密数据拼起来形成最终的密文。
 * Padding:Padding就是用来把不满16个字节的分组数据填满16个字节用的,它有三种模式PKCS5、PKCS7和NOPADDING。PKCS5是指分组数据缺少几个字节,就在数据的末尾填充几个字节的几,比如缺少5个字节,就在末尾填充5个字节的5。PKCS7是指分组数据缺少几个字节,就在数据的末尾填充几个字节的0,比如缺少7个字节,就在末尾填充7个字节的0。NoPadding是指不需要填充,也就是说数据的发送方肯定会保证最后一段数据也正好是16个字节。那如果在PKCS5模式下,最后一段数据的内容刚好就是16个16怎么办?那解密端就不知道这一段数据到底是有效数据还是填充数据了,因此对于这种情况,PKCS5模式会自动帮我们在最后一段数据后再添加16个字节的数据,而且填充数据也是16个16,这样解密段就能知道谁是有效数据谁是填充数据了。PKCS7最后一段数据的内容是16个0,也是同样的道理。解密端需要使用和加密端同样的Padding模式,才能准确的识别有效数据和填充数据。我们开发通常采用PKCS7 Padding模式。(好像Java默认是PKCS#5Padding)
 * 初始向量IV:初始向量IV的作用是使加密更加安全可靠,我们使用AES加密时需要主动提供初始向量,而且只需要提供一个初始向量就够了,后面每段数据的加密向量都是前面一段的密文。初始向量IV的长度规定为128位16个字节,初始向量的来源为随机生成。
 * 密钥:AES要求密钥的长度可以是128位16个字节、192位或者256位,位数越高,加密强度自然越大,但是加密的效率自然会低一些,因此要做好衡量。我们开发通常采用128位16个字节的密钥,我们使用AES加密时需要主动提供密钥,而且只需要提供一个密钥就够了,每段数据加密使用的都是这一个密钥,密钥来源为随机生成。
 * 四种加密模式:AES一共有四种加密模式,分别是ECB(电子密码本模式)、CBC(密码分组链接模式)、CFB、OFB,我们一般使用的是CBC模式。四种模式中除了ECB相对不安全之外,其它三种模式的区别并没有那么大,
 * <p>
 * opmode(操作模式)是必须参数,可选值是ENCRYPT_MODE、DECRYPT_MODE、WRAP_MODE和UNWRAP_MODE。
 * Key类型参数如果不是非对称加密,对应的类型是SecretKey,如果是非对称加密,可以是PublicKey或者PrivateKey。
 * SecureRandom是随机源,因为有些算法需要每次加密结果都不相同,这个时候需要依赖系统或者传入的随机源,一些要求每次加解密结果相同的算法如AES不能使用此参数。
 * <p>
 * Java Cryptography Architecture
 * Standard Algorithm Name Documentation for JDK 8
 * Java密码体系结构 JDK 8的标准算法名称文档
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyPairGenerator
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyFactory
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#algspec
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher
 * https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Signature
 * 参考
 * AES加解密算法的模式介绍
 * https://blog.csdn.net/searchsun/article/details/2516191
 * JDK安全模块JCE核心Cipher使用详解
 * https://blog.csdn.net/zcmain/article/details/90640797
 * SecureRandom是随机源,因为有些算法需要每次加密结果都不相同,这个时候需要依赖系统或者传入的随机源,一些要求每次加解密结果相同的算法如AES不能使用此参数
 */
public final class AESKit {

    public static void main(String[] args) {
        String data = "你好aaa";
        String s = generateKeyStr();
        String cipher = encryptToStr(s, data);
        System.out.println(cipher);
        System.out.println(decryptToStr(s, cipher));
    }

    private AESKit() {}

    /**
     * 加密算法
     */
    private static final String ALGORITHM = "AES";
    /**
     * 单例 KeyGenerator
     */
    private static volatile KeyGenerator instance;
    /**
     * 默认密钥长度(位),AES密钥长度可以为128/192/256,其中192和256需要配置无政策限制权限文件
     */
    private static final int DEFAULT_KEY_SIZE = 128;
    /**
     * 默认TRANSFORMATION(转换模式)
     */
    private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
    /**
     * 算法参数透明定义(向量)
     */
    private static final AlgorithmParameterSpec IV_PARAMETER_SPEC = new IvParameterSpec(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
    /**
     * UTF-8编码
     */
    private static final Charset UTF8 = StandardCharsets.UTF_8;

    /**
     * 单例获取 KeyGenerator
     * 默认通过随机源生成128位长度的密钥,如需192/256位长度密钥则需要修改
     */
    private static KeyGenerator getKeyGenerator() {
        try {
            if (null == instance) {
                synchronized (AESKit.class) {
                    if (null == instance) {
                        KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
                        keyGenerator.init(DEFAULT_KEY_SIZE, new SecureRandom(UUID.randomUUID().toString().getBytes()));
                        instance = keyGenerator;
                    }
                }
            }
            return instance;
        } catch (Throwable cause) {
            throw new ServiceException(cause);
        }
    }

    /**
     * 生成密钥
     */
    public static SecretKey generateKey() {
        return getKeyGenerator().generateKey();
    }

    /**
     * 生成密钥
     */
    public static String generateKeyStr() {
        return toKeyStr(generateKey());
    }

    /**
     * 字符串密钥转标准密钥
     */
    public static SecretKey toSecretKey(String secretKeyStr) {
        try {
            return new SecretKeySpec(decode(secretKeyStr), ALGORITHM);
        } catch (Throwable cause) {
            throw new ServiceException(cause);
        }
    }

    /**
     * 标准密钥转字符串密钥
     */
    public static String toKeyStr(SecretKey key) {
        return encode(key.getEncoded());
    }

    /**
     * 加密
     */
    public static byte[] encrypt(SecretKey secretKey, byte[] data) {
        try {
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, IV_PARAMETER_SPEC);
            return cipher.doFinal(data);
        } catch (Throwable cause) {
            throw new ServiceException(cause);
        }
    }

    /**
     * 加密
     */
    public static byte[] encrypt(String secretKeyStr, byte[] data) {
        return encrypt(toSecretKey(secretKeyStr), data);
    }

    /**
     * 加密
     */
    public static byte[] encrypt(SecretKey secretKey, String data) {
        return encrypt(secretKey, data.getBytes(UTF8));
    }

    /**
     * 加密
     */
    public static byte[] encrypt(String secretKeyStr, String data) {
        return encrypt(toSecretKey(secretKeyStr), data.getBytes(UTF8));
    }

    /**
     * 加密
     */
    public static String encryptToStr(SecretKey secretKey, byte[] data) {
        return encode(encrypt(secretKey, data));
    }

    /**
     * 加密
     */
    public static String encryptToStr(String secretKeyStr, byte[] data) {
        return encode(encrypt(toSecretKey(secretKeyStr), data));
    }

    /**
     * 加密
     */
    public static String encryptToStr(SecretKey secretKey, String data) {
        return encode(encrypt(secretKey, data.getBytes(UTF8)));
    }

    /**
     * 加密
     */
    public static String encryptToStr(String secretKeyStr, String data) {
        return encode(encrypt(toSecretKey(secretKeyStr), data.getBytes(UTF8)));
    }

    /**
     * 解密
     */
    public static byte[] decrypt(SecretKey secretKey, byte[] data) {
        try {
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, IV_PARAMETER_SPEC);
            return cipher.doFinal(data);
        } catch (Throwable cause) {
            throw new ServiceException(cause);
        }
    }

    /**
     * 解密
     */
    public static byte[] decrypt(String secretKeyStr, byte[] data) {
        return decrypt(toSecretKey(secretKeyStr), data);
    }

    /**
     * 解密
     */
    public static byte[] decrypt(SecretKey secretKey, String data) {
        return decrypt(secretKey, decode(data));
    }

    /**
     * 解密
     */
    public static byte[] decrypt(String secretKeyStr, String data) {
        return decrypt(toSecretKey(secretKeyStr), decode(data));
    }

    /**
     * 解密
     */
    public static String decryptToStr(SecretKey secretKey, byte[] data) {
        return new String(decrypt(secretKey, data), UTF8);
    }

    /**
     * 解密
     */
    public static String decryptToStr(String secretKeyStr, byte[] data) {
        return new String(decrypt(toSecretKey(secretKeyStr), data), UTF8);
    }

    /**
     * 解密
     */
    public static String decryptToStr(SecretKey secretKey, String data) {
        return new String(decrypt(secretKey, decode(data)), UTF8);
    }

    /**
     * 解密
     */
    public static String decryptToStr(String secretKeyStr, String data) {
        return new String(decrypt(toSecretKey(secretKeyStr), decode(data)), UTF8);
    }

    /**
     * 编码
     */
    private static String encode(byte[] data) {
        return Base64.getEncoder().encodeToString(data);
    }

    /**
     * 解码
     */
    private static byte[] decode(String data) {
        return Base64.getDecoder().decode(data);
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343