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[]
?还是 JavaKey
对象),另外还有是否需要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 证书的相关封装的,主要还未有证书给我测试,就暂时不搞咯。
如果有任何问题或不足,敬请提出!