HMAC-MD5签名的Java实现
- 协议标准
- 准备内容
-
- MD5工具类Md5Utils
- Hex编码工具类HexUtils
- 公共部分
-
- AbstractHMacMd5Utils类
- 第一次实现-简单的HMacMd5Utils类
-
- 算法中的掩码
- 密钥处理
- "`istr`"和"`ostr`"的生成
- 签名的实现
- 测试过程
- 尚未实现的功能
- JCE接口实现
-
- 密钥
-
- 类HMacMd5KeySpec
- 类HMacMd5Key
- 类HMacMd5KeyFactory
- 签名算法实现
- Provider类
- JCE接口的测试
- 重新封装工具类
-
- HMacMd5Utils2类
- 测试
协议标准
中电联的协议《T/CEC 102.4—2016 电动汽车充换电服务信息交换 第4部分:数据传输及安全》中描述了一个名为HMAC-MD5的签名方法,具体如下图:
用于数据传输过程的报文签名。本文是记录如何实现这个签名。
(顺便吐槽一下这个协议中算法的文字描述部分非常不严谨,让人很疑惑。描述中总是用字符串代替字节数组,让我以为要把数据转为字符呢。结果根本不是。最后是看着公式实现的。)
准备内容
MD5工具类Md5Utils
根据协议描述,我们需要Md5的实现。我们这里直接使用之前的工具类,不再这里讨论。
这个工具类类图如下:
代码:
package net.wangds.utils;import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;/*** md5工具.*/
public class Md5Utils {
/*** 字符串的md5.* @param plainText 输入字符串.* @return md5(utf8编码).*/public static String md5(String plainText) {
return md5(plainText, StandardCharsets.UTF_8);}/*** 字符串的md5.* @param plainText 输入字符串.* @param encode 字符集.* @return md5.* @see #md5(String, Charset)*/@Deprecatedpublic static String md5(String plainText, String encode) {
try {
return md5(plainText.getBytes(encode));} catch (UnsupportedEncodingException e) {
return "";}}/*** 字符串的md5.* @param plainText 输入字符串.* @param charset 字符集.* @return md5.*/public static String md5(String plainText, Charset charset) {
return md5(plainText.getBytes(charset));}/*** 字符串的md5.* @param plainText 输入字符串.* @return md5(utf8编码).*/public static String md5_16(String plainText) {
return md5_16(plainText, StandardCharsets.UTF_8);}/*** 字符串的md5.* @param plainText 输入字符串.* @param encode 字符集.* @return md5.* @see #md5(String, Charset)*/@Deprecatedpublic static String md5_16(String plainText, String encode) {
try {
return md5(plainText.getBytes(encode)).substring(8,24);} catch (UnsupportedEncodingException e) {
return "";}}/*** 字符串的16位md5.* @param plainText 输入字符串.* @param charset 字符集.* @return md5.*/public static String md5_16(String plainText, Charset charset){
return md5(plainText, charset).substring(8,24);}/*** 数据的md5.* @param data 输入字符串.* @return md5.*/public static byte[] md5Bytes(byte[] data) {
try {
// 生成一个MD5加密计算摘要MessageDigest md = MessageDigest.getInstance("MD5");//对字符串进行加密md.update(data);//获得加密后的数据return md.digest();} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("没有md5这个算法");}}/*** 数据的md5.* @param data 输入字符串.* @return md5.*/public static String md5(byte[] data) {
return HexUtils.bytes2HexString(md5Bytes(data));}}
Hex编码工具类HexUtils
同时,测试等工作也需要一个Hex编码工具,类图如下:
HexUtils的代码如下:
package net.wangds.utils;@SuppressWarnings("unused")
public class HexUtils {
/*** bytes2HexString.* 字节数组转16进制字符串.* @param bs 字节数组.* @return 16进制字符串.*/public static String bytes2HexString(byte[] bs) {
StringBuilder result = new StringBuilder();for (byte b1 : bs) {
result.append(String.format("%02X", b1));}return result.toString();}/*** 字节数组转大写16进制字符串.* @param bs 字节数组.* @return 16进制字符串.*/public static String bytes2UpperCaseHexString(byte[] bs) {
return bytes2HexString(bs).toUpperCase();}/*** 字节数组转小写16进制字符串.* @param bs 字节数组.* @return 16进制字符串.*/public static String bytes2LowerCaseHexString(byte[] bs) {
return bytes2HexString(bs).toLowerCase();}/*** bytes2HexString.* 字节数组转16进制字符串.* @param bs 字节数组.* @return 16进制字符串.*/public static String bytes2HexString(byte[] bs, int offset, int length) {
StringBuilder result = new StringBuilder();for (int i=0; i<length; i++) {
byte b1 = bs[offset+i];result.append(String.format("%02X", b1));}return result.toString();}/*** hexString2Bytes.* 16进制字符串转字节数组.* @param src 16进制字符串.* @return 字节数组.* */public static byte[] hexString2Bytes(String src) {
int l = src.length() / 2;byte[] ret = new byte[l];for (int i = 0; i < l; i++) {
ret[i] = Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue();}return ret;}/*** string2HexUTF8.* 字符UTF8串转16进制字符串.* @param strPart 字符串.* @return 16进制字符串.* */public static String string2HexUTF8(String strPart) {
return string2HexString(strPart,"UTF-8");}/*** string2HexUTF8.* 字符UTF-16LE串转16进制字符串,此UTF-16LE等同于C#中的Unicode* @param strPart 字符串.* @return 16进制字符串.* */public static String string2HexUTF16LE(String strPart) {
return string2HexString(strPart,"UTF-16LE");}/*** string2HexUnicode.* 字符Unicode串转16进制字符串.* @param strPart 字符串.* @return 16进制字符串.* */public static String string2HexUnicode(String strPart) {
return string2HexString(strPart,"Unicode");}/*** string2HexGBK.* 字符GBK串转16进制字符串.* @param strPart 字符串.* @return 16进制字符串.* */public static String string2HexGBK(String strPart) {
return string2HexString(strPart,"GBK");}/*** string2HexString.* 字符串转16进制字符串.* @param strPart 字符串.* @param tochartype hex目标编码.* @return 16进制字符串.* */public static String string2HexString(String strPart,String tochartype) {
try{
return bytes2HexString(strPart.getBytes(tochartype));}catch (Exception e){
return "";}}/*** hexUTF82String.* 16进制UTF-8字符串转字符串.* @param src 16进制字符串.* @return 字节数组.* */public static String hexUTF82String(String src) {
return hexString2String(src,"UTF-8","UTF-8");}/*** hexUTF16LE2String.* 16进制UTF-8字符串转字符串,,此UTF-16LE等同于C#中的Unicode.* @param src 16进制字符串.* @return 字节数组.* */public static String hexUTF16LE2String(String src) {
return hexString2String(src,"UTF-16LE","UTF-8");}/*** hexGBK2String.* 16进制GBK字符串转字符串.* @param src 16进制字符串.* @return 字节数组.* */public static String hexGBK2String(String src) {
return hexString2String(src,"GBK","UTF-8");}/*** hexUnicode2String.* 16进制Unicode字符串转字符串.* @param src 16进制字符串.* @return 字节数组.* */public static String hexUnicode2String(String src) {
return hexString2String(src,"Unicode","UTF-8");}/*** hexString2String 16进制字符串转字符串.* @param src 16进制字符串.* @return 字节数组.* */public static String hexString2String(String src,String oldchartype, String chartype) {
byte[] bts=hexString2Bytes(src);try{
if(oldchartype.equals(chartype))return new String(bts,oldchartype);elsereturn new String(new String(bts,oldchartype).getBytes(),chartype);}catch (Exception e){
return"";}}}
公共部分
AbstractHMacMd5Utils类
- 协议中的报文组装过程
从协议应用场景上看,用于充电桩的信息交换,而且示例中,直接给了运营商Id,信息,时间戳,序列号等字段内容。
这些字段如何使用并没有在描述HMAC-MD5的附录C中说明,而是在协议正文。这部分内容如下图:
- 报文组装过程的实现
我们在这里用AbstractHMacMd5Utils
类封装这个组装报文的功能。类图如下:
这个类是后续开发的工具类的基类,在用工具类中可以方便地调用组装报文的方法assembleData
。
类代码如下:
package net.wangds.cnpg.cpmn.utils;import java.nio.charset.Charset;/*** .* <p></p>** @author eastone 2020/10/21 11:45.*/
public class AbstractHMacMd5Utils {public static byte[] assembleData(String operatorID, String data, String timestamp, String seq, Charset charset){return String.format("%s%s%s%s", operatorID,data, timestamp, seq).getBytes(charset);}}
第一次实现-简单的HMacMd5Utils类
算法中的掩码
根据算法定义,需要有两个用于异或操作的掩码opad和ipad。
同时定义了一个常量keyLen用来代表密钥长度。
密钥处理
算法中的密钥,长度为64字节,也就是64*8=512 bit。
根据算法要求,密钥长度小于64位的时候,要补"\0
"。协议描述给的示例密钥是"1234567890abcdef
",所以最开始补了字符"0
",结果不对,要用"\0
"才可以。
另外,当密钥字符串长度大于64字节的时候,我们这里选择用密钥的md5散列码作为真实密钥。
关于补0的问题,我们这里没有使用补零的过程,因为Java中byte默认是0。密钥的处理过程用到了ByteBuffer
,创建固定大小的ByteBuffer对象,从起始位置写入密钥,未写入的部分自然是"\0
".
实现后,类图如下:
代码:
/*** 处理密钥.* <p>密钥补为64字节;长于64字节的用md5散列结果.</p>* @param key 签名密钥.* @return 结果.*/private static byte[] generateKey(String key) {
ByteBuffer buf = ByteBuffer.allocate(keyLen);if(key.length()>keyLen){
buf.put(Md5Utils.md5Bytes(key.getBytes(StandardCharsets.UTF_8))) ;}else{
buf.put(key.getBytes());}return buf.array();}
"istr
“和”ostr
"的生成
istr和ostr是密钥数组分别和ipad和opad常数做异或得到的结果。类图如下:
generateIstr(key:byte[]): byte[]
的实现如下:
/*** 生成istr数据.* @param key 签名密钥.* <p>64字节长.</p>* @param out 输出数组.* @return 结果.*/public static byte[] generateIstr(byte[] key, byte[] out){
for(int i=0;i<keyLen;i++){
out[i]=(byte)(key[i]^ipad);}return out;}
generateOstr(key:byte[]): byte[]
的实现如下:
/*** 生成ostr数据.* @param key 签名密钥.* <p>64字节长.</p>* @param out 输出数组.* @return 结果.*/private static byte[] generateOstr(byte[] key, byte[] out) {
for(int i=0;i<keyLen;i++){
out[i]=(byte)(key[i]^opad);}return out;}`
签名的实现
在以上内容实现之后,签名过程的开发就相对简单了,流程如下:
代码如下:
/*** 数据签名.* @param key 签名密钥.* @param data 数据.* @return 签名.*/public static byte[] sign(String key, byte[] data){
byte[] k = generateKey(key);byte[] str = new byte[keyLen];byte[] data1 = new byte[keyLen+data.length];System.arraycopy(generateIstr(k, str), 0, data1, 0, keyLen);System.arraycopy(data, 0, data1, keyLen, data.length);byte[] md5One = Md5Utils.md5Bytes(data1);byte[] data2 = new byte[keyLen+md5One.length];System.arraycopy(generateOstr(k, str), 0, data2, 0, keyLen);System.arraycopy(md5One, 0, data2, keyLen, md5One.length);return Md5Utils.md5Bytes(data2);}
这里特别的地方是str这个变量,用来保存istr和ostr。因为这两个变量不是同时使用的,所以用了同一块内存来保存,不用分配两个字节数组。虽然对于java来讲这么操作不会有什么影响,不过还是尽量节约内存,养成良好习惯。
为了调用方便,我们增加两个常用接口:
/*** 生成签名字符串.* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @return 签名.* <p>Hex格式, e.g.: 745166E8C43C84D37FFEC0F529C4136F</p>*/public static String signHex(String key, String operatorID, String data, String timestamp, String seq){
return HexUtils.bytes2HexString(sign(key, operatorID, data, timestamp, seq));}/*** 生成签名.* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @return 签名.* <p>e.g.: 745166E8C43C84D37FFEC0F529C4136F(Hex格式)</p>*/public static byte[] sign(String key, String operatorID, String data, String timestamp, String seq){
return sign(key, assembleData(operatorID,data, timestamp, seq, StandardCharsets.UTF_8));}
类图如下:
测试过程
测试过程用JUnit实现,代码如下:
String want = "745166E8C43C84D37FFEC0F529C4136F";String key = "1234567890abcdef";String opId = "123456789";String data = "il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=";String ts = "20160729142400";String seq = "0001";@Testpublic void test(){
TestCase.assertEquals(want, HMacMd5Utils.signHex(key, opId, data, ts, seq));}
尚未实现的功能
HMAC-MD5作为一个签名算法,当然应该有校验的过程。不过,我们这里暂时不实现校验,因为后面还有其他的工作。如果有需要,可以尝试自己实现校验过程。
JCE接口实现
JCE(Java Cryptography Extension)是Java的加密算法支持,提供了统一的加密、解密、散列、签名、密钥算法。例如,我们MD5工具类就使用了JCE的实现,如下:
/*** 数据的md5.* @param data 输入字符串.* @return md5.*/public static byte[] md5Bytes(byte[] data) {
try {
// 生成一个MD5加密计算摘要MessageDigest md = MessageDigest.getInstance("MD5");//对字符串进行加密md.update(data);//获得加密后的数据return md.digest();} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("没有md5这个算法");}}
我们的HMAC-MD5现在只能作为一个工具类使用。假如有另外一段代码已经用了JCE实现的其他方式签名,现在要改成HMAC-MD5,那么他需要重写算法。而如果我们能够提供JCE接口的HMAC-MD5算法。那么他就能很容易的切换算法。
密钥
密钥相关的部分分为几个内容:
- KeySpec: 用来保存密钥
- PublicKey:加密公钥
- PrivateKey:解密私钥
- Key:密钥
- KeyFactory: 密钥工厂
- 首先HMacMd5KeySpec用于保存密钥数据。
- 因为HMAC-MD5算法不区分公钥/私钥,所以HMacMd5Key同时实现PublicKey合PrivateKey接口。
- HMacMd5KeyFactory类用于生成HMacMd5相关的密钥。
类HMacMd5KeySpec
本类代码如下:
package net.wangds.tcec.tcec102d4.messagedigest.hmacmd5;import net.wangds.utils.Md5Utils;import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.spec.KeySpec;/*** .* <p></p>** @author wangds 2020/10/20 12:31.*/
public class HMacMd5KeySpec implements KeySpec {
private static final int LEN = 64;ByteBuffer buf = ByteBuffer.allocate(LEN);public HMacMd5KeySpec(String key){
this(key.getBytes(StandardCharsets.ISO_8859_1));}public HMacMd5KeySpec(byte[] key){
buf.position(0);buf.limit(buf.capacity());if(key.length>LEN){
key = Md5Utils.md5Bytes(key);}buf.put(key);buf.flip();buf.rewind();}public static KeySpec of(String s) {
return new HMacMd5KeySpec(s);}public byte[] toBytes(){
return buf.array();}}
可以看到这个类主要是通过一个字节缓存保存密钥数据。
类HMacMd5Key
这个类用于实现PublicKey、PrivateKey接口,是签名、验证过程中设置密钥时所需的对象类型。
package net.wangds.tcec.tcec102d4.messagedigest.hmacmd5;import java.security.PrivateKey;
import java.security.PublicKey;/*** .* <p></p>** @author wangds 2020/10/20 12:45.*/
public class HMacMd5Key implements PublicKey, PrivateKey {
private final HMacMd5KeySpec spec;public HMacMd5Key(HMacMd5KeySpec spec){
this.spec = spec;}@Overridepublic String getAlgorithm() {
return "HMacMd5";}@Overridepublic String getFormat() {
return "NOPadding";}@Overridepublic byte[] getEncoded() {
return spec.toBytes();}
}
可以看到,类中关联了一个KeySpec的属性用于保存密钥,并指定改了密钥的名称和格式。
类HMacMd5KeyFactory
这里需要特殊说明一下,此类的父类时KeyFactorySpi,而不是KeyFactory。JCE的工厂类会将Spi类包装为Factory类,这个过程不用我们实现,我们实现KeyFacotrySpi就可以了。
代码如下:
package net.wangds.tcec.tcec102d4.messagedigest.hmacmd5;import org.apache.commons.lang3.StringUtils;import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;/*** .* <p></p>** @author wangds 2020/10/20 12:34.*/
public class HMacMd5KeyFactory extends KeyFactorySpi {
@Overrideprotected PublicKey engineGeneratePublic(KeySpec keySpec) throws InvalidKeySpecException {
if(keySpec instanceof HMacMd5KeySpec) {
return new HMacMd5Key((HMacMd5KeySpec) keySpec);}throw new InvalidKeySpecException("只支持HMacMd5KeySpec");}@Overrideprotected PrivateKey engineGeneratePrivate(KeySpec keySpec) throws InvalidKeySpecException {
if(keySpec instanceof HMacMd5KeySpec) {
return new HMacMd5Key((HMacMd5KeySpec) keySpec);}throw new InvalidKeySpecException("只支持HMacMd5KeySpec");}@Override@SuppressWarnings({
"unchecked","cast"})protected <T extends KeySpec> T engineGetKeySpec(Key key, Class<T> keySpec) throws InvalidKeySpecException {
if(StringUtils.equals(keySpec.getClass().getName(), HMacMd5KeySpec.class.getName())){
return (T)new HMacMd5KeySpec(key.getEncoded());}throw new InvalidKeySpecException("只支持HMacMd5KeySpec");}@Overrideprotected Key engineTranslateKey(Key key) throws InvalidKeyException {
byte[] keyData = key.getEncoded();return new HMacMd5Key(new HMacMd5KeySpec(keyData));}
}
通过上面代码,我们可以看到KeyFactorySpi这个接口主要是提供Key的生成和转换功能。我们这个代码本身不用考虑Key的那么多兼容性,毕竟应用场景只是行业内,所以在代码中看到异常情况处理基本是抛错提示。
签名算法实现
签名算法是通过一个SignatureSpi类的子类HMacMd5SignSpi实现的。如下图:
在这个类中,通过engineInitSign()、engineSign()等方法实现了签名过程;通过engineInitVerify()和engineVerify()等方法实现类验证过程。
算法可参考之前Utils类的算法,这里只提供代码。
package net.wangds.tcec.tcec102d4.messagedigest.hmacmd5;import net.wangds.utils.HexUtils;
import net.wangds.utils.Md5Utils;
import org.apache.commons.lang3.StringUtils;import java.nio.ByteBuffer;
import java.security.*;/*** HMAC-MD5签名算法实现.* <p></p>** @author wangds 2020/10/20 12:51.*/
public class HMacMd5SignSpi extends SignatureSpi {
private static final byte opad = 0x5c;private static final byte ipad = 0x36;private static final byte KEN_LEN = 64;private Key key;private ByteBuffer buf = ByteBuffer.allocate(KEN_LEN);@Overrideprotected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
this.key = new HMacMd5KeyFactory().engineTranslateKey(publicKey);}@Overrideprotected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
this.key = new HMacMd5KeyFactory().engineTranslateKey(privateKey);}@Overrideprotected void engineUpdate(byte b) {
buf.rewind();buf.put(b);buf.flip();}@Overrideprotected void engineUpdate(byte[] b, int off, int len) {
if(buf.capacity()<len){
buf = ByteBuffer.allocate(len);}buf.rewind();buf.put(b, off, len);buf.flip();}@Overrideprotected byte[] engineSign() {
return calcSigin();}private byte[] calcSigin() {
byte[] data = new byte[buf.limit()];byte[] k = key.getEncoded();int keyLen = k.length;byte[] str = new byte[keyLen];buf.rewind();buf.get(data);byte[] data1 = new byte[keyLen+ data.length];System.arraycopy(generateIstr(k, str), 0, data1, 0, keyLen);System.arraycopy(data, 0, data1, keyLen, data.length);byte[] md5One = Md5Utils.md5Bytes(data1);byte[] data2 = new byte[keyLen+md5One.length];System.arraycopy(generateOstr(k, str), 0, data2, 0, keyLen);System.arraycopy(md5One, 0, data2, keyLen, md5One.length);return Md5Utils.md5Bytes(data2);}@Overrideprotected boolean engineVerify(byte[] inSign) {
byte[] calcSign = this.calcSigin();String hexCalcSign = HexUtils.bytes2HexString(calcSign);String hexInSign = HexUtils.bytes2HexString(inSign);return StringUtils.equals(hexCalcSign, hexInSign);}@Overrideprotected void engineSetParameter(String param, Object value) throws InvalidParameterException {
//skip}@Overrideprotected Object engineGetParameter(String param) throws InvalidParameterException {
return null;}/*** 生成istr数据.* @param key 签名密钥.* <p>64字节长.</p>* @param out 输出数组.* @return 结果.*/public static byte[] generateIstr(byte[] key, byte[] out){
for(int i = 0; i< KEN_LEN; i++){
out[i]=(byte)(key[i]^ipad);}return out;}/*** 生成ostr数据.* @param key 签名密钥.* <p>64字节长.</p>* @param out 输出数组.* @return 结果.*/private static byte[] generateOstr(byte[] key, byte[] out) {
for(int i = 0; i< KEN_LEN; i++){
out[i]=(byte)(key[i]^opad);}return out;}
}
Provider类
Provider类是算法注册的接口。通过Provider,将算法注册到JCE中。
如下:
package net.wangds.tcec.tcec102d4.messagedigest.hmacmd5;import java.security.Provider;
import java.security.Security;/*** .* <p></p>** @author wangds 2020/10/21 10:52.*/
public final class HMacMd5Provider extends Provider {static {Security.addProvider(new HMacMd5Provider());}/*** Constructs a provider with the specified name, version number,* and information.**/protected HMacMd5Provider() {super("HMacMd5", 1.0f, "HMAC-MD5 Provider v1.0");put("KeyFactory.HMacMd5", "net.wangds.tcec.tcec102d4.messagedigest.hmacmd5.HMacMd5KeyFactory");put("Signature.HMacMd5", "net.wangds.tcec.tcec102d4.messagedigest.hmacmd5.HMacMd5SignSpi");}
}
Provider类在使用时有两种方式,一种是通过修改JRE配置文件实现的;另外一种就是和JDBC中数据库驱动类似的方式,通过反射接口东财注入的。
动态注入的代码片段如下:
static {
try {
Class.forName("net.wangds.tcec.tcec102d4.messagedigest.hmacmd5.HMacMd5Provider");} catch (ClassNotFoundException e) {
LogHelper.error(e);}}
JCE接口的测试
代码如下:
/*** 测试直接使用jce接口的方式调用.*/@Testpublic void test1(){
try {
Class.forName("net.wangds.tcec.tcec102d4.messagedigest.hmacmd5.HMacMd5Provider");} catch (ClassNotFoundException e) {
LogHelper.error(e);}try {
Provider prov = Security.getProvider("HMacMd5");LogHelper.dev("prov:"+prov);KeyFactory fac = KeyFactory.getInstance("HMacMd5");LogHelper.error("fac:"+fac);Signature sign = Signature.getInstance("HMacMd5");LogHelper.error("sign:"+sign);try {
KeySpec keySpec = HMacMd5KeySpec.of(key);PrivateKey pk = fac.generatePrivate(keySpec);sign.initSign(pk);sign.update(HMacMd5Utils.assembleData(opId, data, ts, seq, StandardCharsets.UTF_8));byte[] res = sign.sign();byte[] bsSign = new byte[16];System.arraycopy(res, res.length-16, bsSign , 0,16);LogHelper.dev("sign length:"+res.length);String hex = HexUtils.bytes2HexString(bsSign);TestCase.assertEquals(want, hex);PublicKey pub = fac.generatePublic(keySpec);sign.initVerify(pub);sign.update(HMacMd5Utils.assembleData(opId, data, ts, seq, StandardCharsets.UTF_8));TestCase.assertTrue(sign.verify(res));} catch (InvalidKeyException e) {
e.printStackTrace();} catch (InvalidKeySpecException | SignatureException e) {
e.printStackTrace();}} catch (NoSuchAlgorithmException e) {
e.printStackTrace();}}
注意这里也只测试了加密过程。
重新封装工具类
HMacMd5Utils2类
这个接口通过调用JCE风格的实现,便于以后算法的升级和变换。
代码如下:
package net.wangds.cnpg.cpmn.utils;import net.wangds.log.helper.LogHelper;
import net.wangds.tcec.tcec102d4.messagedigest.hmacmd5.HMacMd5KeySpec;
import net.wangds.utils.HexUtils;import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.function.BiFunction;/*** .* <p></p>** @author 王东石 2020/10/21 11:46.*/
public final class HMacMd5Utils2 extends AbstractHMacMd5Utils {
static {
try {
Class.forName("net.wangds.tcec.tcec102d4.messagedigest.hmacmd5.HMacMd5Provider");} catch (ClassNotFoundException e) {
LogHelper.error(e);}}/*** 生成签名.* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @param charset 字符集.* @return 签名.* <p>e.g.: 745166E8C43C84D37FFEC0F529C4136F(Hex格式)</p>*/public static byte[] sign(String key, String operatorID, String data, String timestamp, String seq, Charset charset){
return sign(key, assembleData(operatorID,data, timestamp, seq, charset));}/*** 生成签名Hex字符串.* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @param charset 字符集.* @return 签名.* <p>根据规范要求,字母大写。e.g.: 745166E8C43C84D37FFEC0F529C4136F</p>*/public static String signHex(String key, String operatorID, String data, String timestamp, String seq, Charset charset){
return HexUtils.bytes2HexString(sign(key, operatorID, data, timestamp, seq, charset)).toUpperCase();}/*** 生成签名.* <p>默认字符集:UTF-8</p>* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @return 签名.* <p>e.g.: 745166E8C43C84D37FFEC0F529C4136F(Hex格式)</p>*/public static byte[] sign(String key, String operatorID, String data, String timestamp, String seq){
return sign(key, assembleData(operatorID,data, timestamp, seq, StandardCharsets.UTF_8));}/*** 生成签名Hex字符串.* <p>默认字符集:UTF-8</p>* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @return 签名.* <p>e.g.: 745166E8C43C84D37FFEC0F529C4136F</p>*/public static String signHex(String key, String operatorID, String data, String timestamp, String seq){
return HexUtils.bytes2HexString(sign(key, operatorID, data, timestamp, seq)).toUpperCase();}/*** 生成签名.* @param key 签名密钥.* @param data 数据.* @return 签名.* <p>e.g.: 745166E8C43C84D37FFEC0F529C4136F(Hex格式)</p>*/public static byte[] sign(String key, byte[] data){
return withSign((fac, sign)->{
try {
KeySpec keySpec = HMacMd5KeySpec.of(key);PrivateKey pk = fac.generatePrivate(keySpec);sign.initSign(pk);sign.update(data);return sign.sign();} catch (InvalidKeyException|InvalidKeySpecException | SignatureException e) {
throw new RuntimeException(e);}});}/*** 生成签名Hex字符串.* @param key 签名密钥.* @param data 数据.* @return 签名.* <p>e.g.: 745166E8C43C84D37FFEC0F529C4136F</p>*/public static String signHex(String key, byte[] data){
return HexUtils.bytes2HexString(sign(key, data)).toUpperCase();}/*** 验证Hex格式签名.* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param sign 签名.* <p>e.g.:745166E8C43C84D37FFEC0F529C4136F</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @return 验证结果.*/public static boolean verifyHex(String key, String sign, String operatorID, String data, String timestamp, String seq){
return verify(key, HexUtils.hexString2Bytes(sign), operatorID, data, timestamp, seq);}/*** 验证签名.* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param sign 签名.* <p>e.g.:745166E8C43C84D37FFEC0F529C4136F(Hex格式)</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @return 验证结果.*/public static boolean verify(String key, byte[] sign, String operatorID, String data, String timestamp, String seq){
return verify(key, sign, assembleData(operatorID,data, timestamp, seq, StandardCharsets.UTF_8));}/*** 验证Hex格式签名.* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param sign 签名.* <p>e.g.:745166E8C43C84D37FFEC0F529C4136F(Hex格式)</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @param charset 字符集.** @return 验证结果.*/public static boolean verifyHex(String key, String sign, String operatorID, String data, String timestamp, String seq, Charset charset){
return verify(key, HexUtils.hexString2Bytes(sign), operatorID,data, timestamp, seq, charset);}/*** 验证签名.* @param key 签名密钥.* <p>e.g.: 1234567890abcdef</p>* @param sign 签名.* <p>e.g.:745166E8C43C84D37FFEC0F529C4136F(Hex格式)</p>* @param operatorID 运营商Id.* <p>e.g.: 123456789</p>* @param data 数据.* <p>e.g.: il7B0BSEjFdzpyKzfOFpvg/Se1CP802RItKYFPfSLRxJ3jf0bVl9hvYOEktPAYW2nd7S8MBcyHYyacHKbISq5iTmDzG+ivnR+SZJv3USNTYVMz9rCQVSxd0cLlqsJauko79NnwQJbzDTyLooYoIwz75qBOH2/xOMirpeEqRJrF/EQjWekJmGk9RtboXePu2rka+Xm51syBPhiXJAq0GfbfaFu9tNqs/e2Vjja/ltE1M0lqvxfXQ6da6HrThsm5id4ClZFIi0acRfrsPLRixS/IQYtksxghvJwbqOsbIsITail9Ayy4tKcogeEZiOO+4Ed264NSKmk7l3wKwJLAFjCFogBx8GE3OBz4pqcAn/ydA=</p>* @param timestamp 时间戳.* <p>e.g.: 20160729142400</p>* @param seq 序列.* <p>e.g.: 0001</p>* @param charset 字符集.* @return 验证结果.*/public static boolean verify(String key, byte[] sign, String operatorID, String data, String timestamp, String seq, Charset charset){
return verify(key, sign, assembleData(operatorID,data, timestamp, seq, charset));}/*** 验证数据包.* @param key 密钥.* @param signData 签名.* @param data 数据.* @return 验证是否成功.*/public static boolean verify(String key, byte[] signData, byte[] data){
return withSign((fac, sign)->{
try {
KeySpec keySpec = HMacMd5KeySpec.of(key);PublicKey pub = fac.generatePublic(keySpec);sign.initVerify(pub);sign.update(data);return sign.verify(signData);} catch (InvalidKeyException|InvalidKeySpecException | SignatureException e) {
throw new RuntimeException(e);}});}/*** 验证数据包.* @param key 密钥.* @param signData 签名.* @param data 数据.* @return 验证是否成功.*/public static boolean verifyHex(String key, String signData, byte[] data){
return verify(key, HexUtils.hexString2Bytes(signData), data);}/*** 在密钥工厂和签名算法确定的条件下,执行回到函数.* @param callback 要执行的回调函数.* @param <T> 回调函数返回值类型.* @return 回调函数返回值.*/private static <T> T withSign(BiFunction<KeyFactory, Signature, T> callback){
try {
KeyFactory fac = KeyFactory.getInstance("HMacMd5");Signature sign = Signature.getInstance("HMacMd5");return callback.apply(fac, sign);} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);}}}
测试
测试过程代码如下:
@Testpublic void test3(){
TestCase.assertEquals(want, HMacMd5Utils2.signHex(key, opId, data, ts, seq));TestCase.assertEquals(want, HMacMd5Utils2.signHex(key, opId, data, ts, seq, StandardCharsets.UTF_8));TestCase.assertTrue(HMacMd5Utils2.verifyHex(key, want, opId, data, ts, seq));TestCase.assertTrue(HMacMd5Utils2.verifyHex(key, want, opId, data, ts, seq, StandardCharsets.UTF_8));}
END