一套清晰、简洁的 Java AES/DES/RSA 加密解密 API

JDK 中蕴含了主流的加密解密 API,包括 AES/DES/3DES/RSA,这些功能主要由javax.crypto(JCE, Java Cryptography Extension)和java.security包提供。其中只有 RSA 属于非对称加密(Asymmetric Encryption),其他都是对称加密(Symmetric Encryption),它们之间的异同如下:

  • 之所以被称为"对称加密",即无论加密还是解密都是同一一个密钥(key),即为"对称",反之加密一个密钥、解密另外一个密钥的话,则为"非对称"
  • 对称加密一般执行速度较快,但相对于非对称加密安全性较差;而非对称加密则恰恰相反,安全性较高但执行效率相对差
  • 两者之间没有绝对优劣之分,应视乎场合需求择优选用

源码

最终的代码在这里

使用方式可以参见单测

封装 API

笔者不但自己封装过此类 API,也参考过不少开源的代码,------但终归感觉不太满意。此类代码属于相对底层的工具函数,很自然地采用大量 static 静态函数来设计 API。如下 RSA 中签名校验函数: 如图最后的verify函数为核心函数,其余同名的为重载函数。尽管可以不断通过重载函数来达到入参多样化的目的,但一旦复杂起来,重载函数会变得很复杂混乱,例如有时入参String类型,而要区分普通字符串与要解码的 base64 字符串,此时重载机制已无法满足需求。另外不仅入参要求不同类型的多样化,出参也是多样化的,------如此这般无疑又为 API 的设计增加了一个维度。

可见,普通静态化已经无法满足复杂的 API 设计了,需要另外一套方法论来指导咱们的 API 封装------那到底是什么的方法呢?熟悉 C 语言到 Java 演进历史的朋友(或者 C++与 C 亦然),此刻脑海不正是会浮现那套"面向过程"到"面向对象"方法论的变迁么?

没错!欲解决这个问题,仅依赖 Java 所倡导的面向对象设计即可,------即"类 Class" 去封装便可搞定。类其实我们也不陌生,难就难在我们应该怎么转变思维,设计出更合理、更清晰的代码。这么说,单独去函数封装人人都会,单独去写类、new实例化对象每个人也都会!但把它们掂量起来,什么时候用函数,什么时候用 OO,你有没有纠结过呢?逻辑上讲,用函数风格或者用 OO 风格都可等价,返回一致的结果,但为什么要强调一种而放弃另外一种呢?或者说,什么时候才能体现 OO 的优点呢?这里面蕴含了不少"似是而非"的问题值得我们去考量和细品。同时也正是:没有写过足够的代码量,也无法体会个中设计合适的选择。

文字的描述还是肤浅的,让我们进入代码中一窥其豹,自方可体会。

原始流程

加密最原始的表达是密文=加密函数(明文)。这里顺便说说 MD5,它符合密文=加密函数(明文)的定义,看似对输入进行了加密,实则不然,它是哈希函数的一种,跟加密完全不是一个概念,更准确地说它返回了输入参数的"特征"结果,这个特征是唯一的。又因为每次执行都是返回相同的结果,这样的话,可以构成字典表去查对。只要这个字典表足够大,那么 MD5 是非常不安全的。

于是乎,我们对这个加密过程改造,增加入参 key 密钥,希望根据 key 的不同每次返回的密文也不一样,即密文=加密函数(明文, 密钥)

AES(Advanced Encryption Standard) 对称加密也是符合这个原始的流程,粗糙的 Java API 实现如下:

java 复制代码
/**
 * 是解密模式还是加密模式?
 */
private final int mode;

/**
 * The name of the algorithm
 */
private final String algorithmName;

/**
 * 密钥
 */
private Key key;

private byte[] data;
    
public byte[] doCipher() {
    try {
        Cipher cipher = Cipher.getInstance(algorithmName);

        if (spec != null)
            cipher.init(mode, key, spec);
        else
            cipher.init(mode, key);

        if (associatedData != null)
            cipher.updateAAD(associatedData);

        return cipher.doFinal(data);
    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
        throw new RuntimeException(Constant.NO_SUCH_ALGORITHM + algorithmName, e);
    } catch (IllegalBlockSizeException | BadPaddingException e) {
        throw new RuntimeException("加密原串的长度不能超过214字节", e);
    } catch (InvalidKeyException e) {
        throw new IllegalArgumentException("Invalid Key.", e);
    } catch (InvalidAlgorithmParameterException e) {
        throw new IllegalArgumentException("Invalid Algorithm Parameter.", e);
    }
}

首先可见我们没有采用函数风格去定义doCipher(),而是通过 getter/setter 去入参(前面有 lombak 去生成)。这些入参都是doCipher()执行所需的基本原始数据类型。其次我们抽象一下主要的流程:

一、确定是哪种算法:AES or DES or RSA? 二、确定解密模式还是加密模式? 三、key 传入密钥(是byte[]?还是 Java Key对象),另外还有是否需要AlgorithmParameterSpec spec入参? 四、输入数据 data(是byte[]?还是 String 还是 Base64 String?),然后执行加密/解密 五、原始返回是byte[],那么调用者希望是要直接返回 String,还是 Base64 String,抑或 HexString?

看来加解密过程核心数据类型都是byte[]。无论哪种类型入参都要最终转换到byte[]。每多考虑一种数据类型入参,便利性就多一点(不需要 API 调用者去手动转换),那么相应地就要安排多一个 setter 来进行转换。

大致的代码风格思路确定了,接着就可以着手进行编码了。

AES/DES 加密解密

AES

对称加密多基于javax.crypto包进行封装,封装在类com.ajaxjs.util.cryptography中。先看看 AES 的,

java 复制代码
final String key = "abc";
final String word = "123";

@Test
void testAES() {
    String encWord = Cryptography.AES_encode(word, key);
    assertEquals(word, Cryptography.AES_decode(encWord, key));
}

咦~怎么还是静态方法?噢------对了,我们通过静态方法Cryptography.AES_encode()封装了一层,其实质是:

java 复制代码
public static String AES_encode(String data, String key) {
    Cryptography cryptography = new Cryptography(Constant.AES, Cipher.ENCRYPT_MODE);
    cryptography.setSecretKey(SecretKeyMgr.getSecretKey(Constant.AES, 128, SecretKeyMgr.getRandom(Constant.SECURE_RANDOM_ALGORITHM, key)));
    cryptography.setDataStr(data);

    return cryptography.doCipherAsHexStr();
}

要说每次实例化对象,当然比静态方法耗资源,不过在 Java 编译器优化的今天,这多出了一点的消耗可以忽略不计。

DES/TripleDES

其余 DES/TripleDES 如此类推,只是算法不同~

java 复制代码
@Test
void testDES() {
    String encWord = Cryptography.DES_encode(word, key);
    assertEquals(word, Cryptography.DES_decode(encWord, key));
}

@SuppressWarnings("restriction")
@Test
void test3DES() {
    // 添加新安全算法,如果用 JCE 就要把它添加进去
    // 这里 addProvider 方法是增加一个新的加密算法提供者(个人理解没有找到好的答案,求补充)
//		Security.addProvider(new com.sun.crypto.provider.SunJCE());
    // byte 数组(用来生成密钥的)
    final byte[] keyBytes = {0x11, 0x22, 0x4F, 0x58, (byte) 0x88, 0x10, 0x40, 0x38, 0x28, 0x25, 0x79, 0x51, (byte) 0xCB, (byte) 0xDD, 0x55, 0x66, 0x77, 0x29, 0x74,
            (byte) 0x98, 0x30, 0x40, 0x36, (byte) 0xE2};
    String word = "This is a 3DES test. 测试";

    byte[] encoded = Cryptography.tripleDES_encode(word, keyBytes);

    assertEquals(word, Cryptography.tripleDES_decode(encoded, keyBytes));
}

PBE

这里说说 PBE 算法。PBE 是 DES 的加强,增加一个 Salt 盐值使其更安全。

java 复制代码
byte[] salt = Cryptography.initSalt();
byte[] encData = Cryptography.PBE_encode(word, key, salt);

assertEquals(word, Cryptography.PBE_decode(encData, key, salt));

RSA 加密解密

RSA 非对称加密,事情比较多,可以分解为下面的子任务:

  • 签名,封装在com.ajaxjs.util.cryptography.rsa.DoSignature完成
  • 校验签名,封装在com.ajaxjs.util.cryptography.rsa.DoVerify完成
  • 密钥管理,封装在com.ajaxjs.util.cryptography.rsa.KeyMgr完成
  • 本身的 RSA 加密解密

下面分别进行介绍。

签名

入参包括算法、输入数据及私钥,执行sign()返回签名。涉及的类型如下:

  • 输入数据,可以是byte[]或字符串
  • 私钥,可以是 PrivateKey 对象或者字符串,字符串的话会经过KeyMgr.restoreKey还原为 PrivateKey 对象
  • 返回的签名数据,是byte[],可以调用signToString()返回 base64 编码的字符串
java 复制代码
// 生成公钥私钥
KeyMgr keyMgr = new KeyMgr(Constant.RSA, 1024);
keyMgr.generateKeyPair();
String privateKey = keyMgr.getPrivateKeyStr();

byte[] helloWorlds = new DoSignature(Constant.SHA256_RSA).setStrData("hello world").setPrivateKeyStr(privateKey).sign();
String result = new DoSignature(Constant.SHA256_RSA).setStrData("hello world").setPrivateKeyStr(privateKey).signToString();

assertEquals(EncodeTools.base64EncodeToString(helloWorlds), result);

值得一提的是,私钥哪里来?你可以通过如上的KeyMgr生成。

校验签名

入参包括算法、输入数据、签名数据及公钥,执行verify()返回签名。涉及的类型如下:

  • 输入数据,可以是byte[]或字符串
  • 签名数据,可以是byte[]或 Base64 字符串
  • 公钥,可以是 PublicKey 对象或者字符串,字符串的话会经过KeyMgr.restoreKey还原为 PublicKey 对象
  • 返的签名是否合法,是boolean
java 复制代码
// 生成公钥私钥
KeyMgr keyMgr = new KeyMgr(Constant.RSA, 1024);
keyMgr.generateKeyPair();
String publicKey = keyMgr.getPublicKeyStr(), privateKey = keyMgr.getPrivateKeyStr();
String result = new DoSignature(Constant.SHA256_RSA).setStrData("hello world").setPrivateKeyStr(privateKey).signToString();
boolean verified = new DoVerify(Constant.SHA256_RSA).setStrData("hello world").setPublicKeyStr(publicKey).setSignatureBase64(result).verify();

assertTrue(verified);

值得一提的是,公钥、私钥哪里来?你可以通过如上的KeyMgr生成。

RSA 加密解密

没什么好说的了,直接上 API 例子。

java 复制代码
// 生成公钥私钥
KeyMgr keyMgr = new KeyMgr(Constant.RSA, 1024);
keyMgr.generateKeyPair();
String publicKey = keyMgr.getPublicKeyStr(), privateKey = keyMgr.getPrivateKeyStr();

System.out.println("公钥: \n\r" + publicKey);
System.out.println("私钥: \n\r" + privateKey);
//		System.out.println("公钥加密--------私钥解密");

String word = "你好,世界!";

byte[] encWord = KeyMgr.publicKeyEncrypt(word.getBytes(), publicKey);
String decWord = new String(KeyMgr.privateKeyDecrypt(encWord, privateKey));

String eBody = EncodeTools.base64EncodeToString(encWord);
String decWord2 = new String(KeyMgr.privateKeyDecrypt(EncodeTools.base64Decode(eBody), privateKey));
System.out.println("加密前: " + word + "\n\r密文:" + eBody + "\n解密后: " + decWord2);
assertEquals(word, decWord);

//		System.out.println("私钥加密--------公钥解密");

String english = "Hello, World!";
byte[] encEnglish = KeyMgr.privateKeyEncrypt(english.getBytes(), privateKey);
String decEnglish = new String(KeyMgr.publicKeyDecrypt(encEnglish, publicKey));
//		System.out.println("加密前: " + english + "\n\r" + "解密后: " + decEnglish);

assertEquals(english, decEnglish);
//		System.out.println("私钥签名------公钥验证签名");

// 产生签名
String sign = new DoSignature(Constant.MD5_RSA).setPrivateKeyStr(privateKey).setData(encEnglish).signToString();
//		System.out.println("签名:\r" + sign);
// 验证签名
assertTrue(new DoVerify(Constant.MD5_RSA).setPublicKeyStr(publicKey).setData(encEnglish).setSignatureBase64(sign).verify());

密钥管理

关于密钥的一些工具方法在KeyMgr,包括公钥和私钥的。一般开源的都喜欢把KeyPair封装为 Map,而笔者觉得直接使用KeyPair本身就可以了,如果不太满足,则增加某些方法。例如getPublicKeyBytes()getPublicKeyStr()getPublicToPem(),相比使用 Map 更加清晰。

另外对于密钥本身还可以加密解密,安全性更高。

小结

封装过程到此暂且休息。其实还有针对微信支付 ApiV3 证书的相关封装的,主要还未有证书给我测试,就暂时不搞咯。

如果有任何问题或不足,敬请提出!

相关推荐
用户68545375977693 小时前
💥 栈溢出 VS 内存溢出:别再傻傻分不清楚!
后端
王嘉祥3 小时前
Pangolin:基于零信任理念的反向代理
后端·架构
Yimin3 小时前
2. 这才是你要看的 网络I/O模型
后端
野犬寒鸦3 小时前
从零起步学习MySQL || 第五章:select语句的执行过程是怎么样的?(结合源码深度解析)
java·服务器·数据库·后端·mysql·adb
橘子海全栈攻城狮3 小时前
【源码+文档+调试讲解】基于SpringBoot + Vue的知识产权管理系统 041
java·vue.js·人工智能·spring boot·后端·安全·spring
Chloeis Syntax3 小时前
接10月12日---队列笔记
java·数据结构·笔记·队列
调试人生的显微镜3 小时前
iOS 26 文件导出全攻略,从系统限制到多工具协作实践
后端
yy.y--3 小时前
Java集合操作实战:List工人管理
java
该用户已不存在4 小时前
这6个网站一旦知道就离不开了
前端·后端·github