当前位置: 代码迷 >> 综合 >> HMAC-MD5签名的Java实现
  详细解决方案

HMAC-MD5签名的Java实现

热度:64   发布时间:2024-03-08 16:26:17.0

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的实现。我们这里直接使用之前的工具类,不再这里讨论。

这个工具类类图如下:
Md5Utils类图
代码:

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的类图

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类封装这个组装报文的功能。类图如下:

AbstractHMacMd5Utils assembleData(opId:String, data:String, ts:String, seq:String,charset:Charset)

这个类是后续开发的工具类的基类,在用工具类中可以方便地调用组装报文的方法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。

AbstractHMacMd5Utils HMacMd5Utils <static> <final> - opad : byte = 0x5c; <static> <final> - ipad : byte = 0x36; <static> <final> - keyLen: byte = 64;

同时定义了一个常量keyLen用来代表密钥长度。

密钥处理

算法中的密钥,长度为64字节,也就是64*8=512 bit。

根据算法要求,密钥长度小于64位的时候,要补"\0"。协议描述给的示例密钥是"1234567890abcdef",所以最开始补了字符"0",结果不对,要用"\0"才可以。

另外,当密钥字符串长度大于64字节的时候,我们这里选择用密钥的md5散列码作为真实密钥。

关于补0的问题,我们这里没有使用补零的过程,因为Java中byte默认是0。密钥的处理过程用到了ByteBuffer,创建固定大小的ByteBuffer对象,从起始位置写入密钥,未写入的部分自然是"\0".

Created with Rapha?l 2.2.0 开始 初始化buf:ByteBuffer变量。 参数密钥长度是否大于64字节? 向buf中写入密钥的md5散列码 结束:返回buf中的数组 将密钥写入buf对象 yes no

实现后,类图如下:

AbstractHMacMd5Utils HMacMd5Utils <static> <final> - opad : byte = 0x5c; <static> <final> - ipad : byte = 0x36; <static> <final> - keyLen: byte = 64; static> - generateKey(key:String)

代码:

    /*** 处理密钥.* <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常数做异或得到的结果。类图如下:

AbstractHMacMd5Utils HMacMd5Utils <static> <final> - opad : byte = 0x5c; <static> <final> - ipad : byte = 0x36; <static> <final> - keyLen: byte = 64; static> - generateKey(key:String) static> - generateIstr(key: byte[]) static> - generateOstr(key: byte[])
  • 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;}`

签名的实现

在以上内容实现之后,签名过程的开发就相对简单了,流程如下:

Created with Rapha?l 2.2.0 开始: 参数key为密钥文本;参数data为要加密的数据 <<参数>> key:String // 密钥文本 <<参数>> data: byte[] // 签名数据 调用generateKey(密钥文本)方法,生成密钥数据k 变量k:密钥:byte[] 变量data1:byte[] - 第一次md5计算的输入数据 计算istr,并将istr写入data1中 将参数data继续写入data1中 计算data1的md5值,并保存在变量md5One中 变量md5One:byte[] //第一次md5计算结果 变量data2 //第二次md5计算的输入数据 计算ostr并写入data2中 计算第二次md5结果res 返回值 res:byte[] 第二次md5计算的结果 结束

代码如下:

    /*** 数据签名.* @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));}

类图如下:

AbstractHMacMd5Utils HMacMd5Utils <static> <final> - opad : byte = 0x5c; <static> <final> - ipad : byte = 0x36; <static> <final> - keyLen: byte = 64; static> - generateKey(key:String) static> - generateIstr(key: byte[]) static> - generateOstr(key: byte[]) static> + sign(key: String, data: byte[]) static> + sign(key: String, operatorId:String, data:String, ts:String, seq:String) static> + signHex(key: String, operatorId:String, data:String, ts:String, seq:String)

测试过程

测试过程用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: 密钥工厂

密钥相关类

  1. 首先HMacMd5KeySpec用于保存密钥数据。
  2. 因为HMAC-MD5算法不区分公钥/私钥,所以HMacMd5Key同时实现PublicKey合PrivateKey接口。
  3. 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实现的。如下图:

SignSpi的实现
在这个类中,通过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

  相关解决方案