一套清晰、简洁的 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 证书的相关封装的,主要还未有证书给我测试,就暂时不搞咯。

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

相关推荐
面向星辰18 分钟前
扣子开始节点和结束节点
java·服务器·前端
已黑化的小白18 分钟前
Rust 的所有权系统,是一场对“共享即混乱”的编程革命
开发语言·后端·rust
烤麻辣烫1 小时前
黑马程序员苍穹外卖(新手)Day1
java·数据库·spring boot·学习·mybatis
失散131 小时前
分布式专题——51 ES 深度分页问题及其解决方案详解
java·分布式·elasticsearch·架构
FreeBuf_1 小时前
思科CCX软件曝高危RCE:攻击者可利用Java RMI和CCX Editor获取root权限
java·网络·安全
_esther_1 小时前
【字符串String类大集合】构造创建_常量池情况_获取方法_截取方法_转换方法_String和基本数据类型互转方法
java
lkbhua莱克瓦241 小时前
Java基础——集合进阶5
java·开发语言·集合·泛型
WZTTMoon2 小时前
Spring 配置解析与 @Value 注入核心流程详解
java·spring boot·spring
程序定小飞2 小时前
基于springboot的健身房管理系统开发与设计
java·spring boot·后端
wxin_VXbishe3 小时前
springboot在线课堂教学辅助系统-计算机毕业设计源码07741
java·c++·spring boot·python·spring·django·php