三方平台AES加密解密抽象工具类

133 阅读13分钟

三方平台AES加密解密

三方平台包括:微信、抖音、百度、小红书

调用示例

public static void main(String[] args) {
    String encryptedData = "";
    String iv = "";
    String sessionKey = "";
    String decrypt = WxBizMsgCrypt.decrypt(encryptedData, iv, sessionKey);
    System.out.println(decrypt);
}

基础代码

maven依赖

<!--Base64Utils依赖,可自行替换成其他类库-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.3.25</version>
</dependency>

<!--XML解析包-->
<dependency>
    <groupId>org.jdom</groupId>
    <artifactId>jdom</artifactId>
    <version>1.1.3</version>
</dependency>

抽象工具类

package com.honyee.app.utils.crypt;

import org.apache.commons.codec.digest.DigestUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Base64Utils;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.Security;
import java.util.Arrays;
import java.util.Base64;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 解密工具公共方法
 * @author honyee
 */
public class BizMsgCryptUtil {

    private static final Charset CHARSET = StandardCharsets.UTF_8;

    private static final Logger log = LoggerFactory.getLogger(BizMsgCryptUtil.class);

    private BizMsgCryptUtil() {
    }

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * 对密文进行解密,主要用于服务端推送消息解密
     *
     * @param appid       需要匹配的小程序appid
     * @param encryptData 密文
     * @param aesKey      密钥
     * @param pos         移位
     * @return 明文
     */
    public static String decrypt(String appid, String encryptData, byte[] aesKey, int pos) {
        byte[] original;
        try {
            // 设置解密模式为AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(Cipher.DECRYPT_MODE, keySpec, iv);

            // 使用BASE64对密文进行解码
            byte[] encrypted = Base64.getDecoder().decode(encryptData);

            // 解密
            original = cipher.doFinal(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("解密失败:" + e.getMessage());
        }
        String content;
        String fromAppid;
        try {
            // ==== 分离随机字符串,网络字节序和AppId
            // 去除补位字符
            byte[] bytes = PKCS7.decode(original);
            // 获取内容长度,byte数组的第pos 至 pos+4 个元素代表了消息体的真实字符个数,也就是长度
            int contentLength = recoverNetworkBytesOrder(Arrays.copyOfRange(bytes, pos, pos + 4));
            // 内容
            content = new String(Arrays.copyOfRange(bytes, pos + 4, pos + 4 + contentLength), CHARSET);
            // 携带的appid
            fromAppid = new String(Arrays.copyOfRange(bytes, pos + 4 + contentLength, bytes.length), CHARSET);
        } catch (Exception e) {
            throw new RuntimeException("解密失败:" + e.getMessage());
        }
        // appid不相同的情况
        if (!fromAppid.equals(appid)) {
            throw new RuntimeException("解密失败:appid不相同");
        }
        return content;
    }

    // 生成4个字节的网络字节序
    public static byte[] getNetworkBytesOrder(int sourceNumber) {
        ByteBuffer b = ByteBuffer.wrap(new byte[4]);
        b.asIntBuffer().put(sourceNumber);
        return b.array();
    }

    // 生成4个字节的网络字节序 - 只是换了种实现方式
    public static byte[] getNetworkBytesOrder2(int sourceNumber) {
        byte[] orderBytes = new byte[4];
        orderBytes[3] = (byte) (sourceNumber & 0xFF);
        orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
        orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
        orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
        return orderBytes;
    }

    // 还原4个字节的网络字节序
    public static int recoverNetworkBytesOrder(byte[] orderBytes) {
        ByteBuffer buf = ByteBuffer.wrap(orderBytes);
        buf.order(ByteOrder.BIG_ENDIAN);
        return buf.getInt();
    }

    // 还原4个字节的网络字节序 - 只是换了种实现方式
    public static int recoverNetworkBytesOrder2(byte[] orderBytes) {
        int sourceNumber = 0;
        for (int i = 0; i < 4; i++) {
            sourceNumber <<= 8;
            sourceNumber |= orderBytes[i] & 0xff;
        }
        return sourceNumber;
    }

    // 随机生成16位字符串
    public static String getRandomStr() {
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        Random random = ThreadLocalRandom.current();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 16; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }

    /**
     * 对明文进行加密.
     *
     * @param text 需要加密的明文
     * @return 加密后base64编码的字符串
     */
    public static String encrypt(String appid, byte[] aesKey, String randomStr, String text) {
        ByteGroup byteCollector = new ByteGroup();
        byte[] randomStrBytes = randomStr.getBytes(CHARSET);
        byte[] textBytes = text.getBytes(CHARSET);
        byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
        byte[] appidBytes = appid.getBytes(CHARSET);
        // randomStr + networkBytesOrder + text + appid
        byteCollector.addBytes(randomStrBytes);
        byteCollector.addBytes(networkBytesOrder);
        byteCollector.addBytes(textBytes);
        byteCollector.addBytes(appidBytes);

        // ... + pad: 使用自定义的填充方式对明文进行补位填充
        byte[] padBytes = PKCS7.encode(byteCollector.size());
        byteCollector.addBytes(padBytes);

        // 获得最终的字节流, 未加密
        byte[] unencrypted = byteCollector.toBytes();

        try {
            // 设置加密模式为AES的CBC模式
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);

            // 加密
            byte[] encrypted = cipher.doFinal(unencrypted);

            // 使用BASE64对加密后的字符串进行编码
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("加密失败:" + e.getMessage());
        }
    }

    /**
     * 对密文进行解密,主要用于移动端数据解密
     *
     * @param encryptData 密文
     * @param iv          向量
     * @param sessionKey  密钥
     * @return 解密后的bytes,可能需要分离随机字符串后再转String
     */
    public static byte[] decrypt(String encryptData, String iv, String sessionKey) {
        byte[] aesKey = Base64Utils.decodeFromString(sessionKey);
        byte[] aesIV = Base64Utils.decodeFromString(iv);
        byte[] aesEncryptedData = Base64Utils.decodeFromString(encryptData);

        try {

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
            SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
            AlgorithmParameters params = AlgorithmParameters.getInstance("AES");
            params.init(new IvParameterSpec(aesIV));
            cipher.init(Cipher.DECRYPT_MODE, keySpec, params);
            return cipher.doFinal(aesEncryptedData);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return null;
    }

    /**
     * 生成签名
     *
     * @param messageValidToken 消息验证token
     * @param timestamp         时间戳,对应URL参数的timestamp
     * @param nonce             随机串,对应URL参数的nonce
     * @param encryptData       加密的数据或者随机串
     * @return 签名
     */
    public static String getSignature(String messageValidToken, String timestamp, String nonce, String encryptData) {
        String[] array = {messageValidToken, timestamp, nonce, encryptData};
        // 字符串排序
        Arrays.sort(array);
        return DigestUtils.sha1Hex(String.join("", array));
    }

    /**
     * 验证
     *
     * @param messageValidToken 消息验证token
     * @param msgSignature      签名串,对应URL参数的msg_signature
     * @param timestamp         时间戳,对应URL参数的timestamp
     * @param nonce             随机串,对应URL参数的nonce
     * @param encryptData       加密的数据或者随机串
     */
    public static void verify(String messageValidToken, String msgSignature, String timestamp, String nonce, String encryptData) {
        if (!getSignature(messageValidToken, timestamp, nonce, encryptData).equals(msgSignature)) {
            throw new RuntimeException("验证失败");
        }
    }

    /**
     * AES 解密
     *
     * @param messageEncodeDecodeKey 消息加密密钥
     * @param encryptData            解密的数据
     * @return 明文
     */
    public static String decrypt(String messageEncodeDecodeKey, String encryptData) {
        byte[] key = messageEncodeDecodeKey.getBytes(StandardCharsets.UTF_8);
        key = Arrays.copyOfRange(key, 0, 16);
        SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
        IvParameterSpec iv = new IvParameterSpec(key);
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
            // 使用BASE64对密文进行解码
            byte[] encrypted = Base64.getDecoder().decode(encryptData);
            // 解密
            byte[] original = cipher.doFinal(encrypted);
            // 去除补位字符
            byte[] bytes = PKCS7.decode(original);
            // 内容
            return new String(bytes, CHARSET);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return null;
    }
}

ByteGroup

package com.honyee.app.utils.crypt;

import java.util.ArrayList;

public class ByteGroup {
	ArrayList<Byte> byteContainer = new ArrayList<>();

	public byte[] toBytes() {
		byte[] bytes = new byte[byteContainer.size()];
		for (int i = 0; i < byteContainer.size(); i++) {
			bytes[i] = byteContainer.get(i);
		}
		return bytes;
	}

	public ByteGroup addBytes(byte[] bytes) {
		for (byte b : bytes) {
			byteContainer.add(b);
		}
		return this;
	}

	public int size() {
		return byteContainer.size();
	}
}

PKCS7

package com.honyee.app.utils.crypt;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
 * 提供基于PKCS7算法的加解密接口.
 */
public class PKCS7 {

    private PKCS7() {}

    private static final Charset CHARSET = StandardCharsets.UTF_8;
    private static final int BLOCK_SIZE = 32;

    /**
     * 获得对明文进行补位填充的字节.
     *
     * @param count 需要进行填充补位操作的明文字节个数
     * @return 补齐用的字节数组
     */
    static byte[] encode(int count) {
        // 计算需要填充的位数
        int amountToPad = BLOCK_SIZE - (count % BLOCK_SIZE);
        if (amountToPad == 0) {
            amountToPad = BLOCK_SIZE;
        }
        // 获得补位所用的字符
        char padChr = chr(amountToPad);
        String tmp = new String();
        for (int index = 0; index < amountToPad; index++) {
            tmp += padChr;
        }
        return tmp.getBytes(CHARSET);
    }

    /**
     * 删除解密后明文的补位字符
     *
     * @param decrypted 解密后的明文
     * @return 删除补位字符后的明文
     */
    static byte[] decode(byte[] decrypted) {
        int padding = decrypted[decrypted.length - 1];
        if (padding < 1 || padding > 32) {
            padding = 0;
        }
        return Arrays.copyOfRange(decrypted, 0, decrypted.length - padding);
    }

    /**
     * 将数字转化成ASCII码对应的字符,用于对明文进行补码
     *
     * @param a 需要转化的数字
     * @return 转化得到的字符
     */
    static char chr(int a) {
        byte target = (byte) (a & 0xFF);
        return (char) target;
    }
}

XmlUtil

package com.honyee.app.utils.crypt;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * xml 解析和生成
 */
public class XmlUtil {

    private static final Logger log = LoggerFactory.getLogger(XmlUtil.class);

    private XmlUtil() {
    }

    /**
     * 解析xml 字节点
     */
    public static Map<String, String> doXMLChildParse(Object strxml) throws IOException, JDOMException {
        return doXMLParse(String.format("<xml>%s</xml>", strxml));
    }

    public static Map<String, String> doXMLParseChild(Map<String, String> xmlMap, String childKey) throws IOException, JDOMException {
        return doXMLParse("<xml>" + xmlMap.getOrDefault(childKey, "") + "</xml>");
    }

    public static Map<String, String> doXMLParseChild(String strxml) throws IOException, JDOMException {
        return doXMLParse("<xml>" + strxml + "</xml>");
    }

    /**
     * 解析xml
     */
    public static Map<String, String> doXMLParse(String strxml) throws IOException, JDOMException {
        if (strxml == null) {
            return Collections.emptyMap();
        }
        strxml = strxml.replaceFirst("encoding=\".*\"", "encoding=\"UTF-8\"");
        if ("".equals(strxml)) {
            return Collections.emptyMap();
        }
        Map<String, String> map = new HashMap<>();
        try (InputStream in = new ByteArrayInputStream(strxml.getBytes(StandardCharsets.UTF_8))) {
            SAXBuilder builder = new SAXBuilder();
            Document doc = builder.build(in);
            Element root = doc.getRootElement();
            for (Object o : root.getChildren()) {
                Element e = (Element) o;
                String k = e.getName();
                List<?> children = e.getChildren();
                if (children.isEmpty()) {
                    map.put(k, e.getTextNormalize());
                } else {
                    map.put(k, getChildrenText(children));
                }
            }
        }
        return map;
    }

    /**
     * 获取子结点的xml
     */
    public static String getChildrenText(List<?> children) {
        StringBuilder sb = new StringBuilder();
        if (children != null && !children.isEmpty()) {
            for (Object child : children) {
                Element e = (Element) child;
                String name = e.getName();
                String value = e.getTextNormalize();
                List<?> list = e.getChildren();
                sb.append(String.format("<%s>", name));
                if (!list.isEmpty()) {
                    sb.append(getChildrenText(list));
                }
                sb.append(value);
                sb.append(String.format("</%s>", name));
            }
        }
        return sb.toString();
    }

    @SuppressWarnings("unused")
    public static String getRequestXml(SortedMap<String, String> parameters) {
        StringBuilder sb = new StringBuilder();
        sb.append("<xml>");
        Set<Map.Entry<String, String>> es = parameters.entrySet();
        for (Map.Entry<String, String> e : es) {
            String k = e.getKey();
            String v = e.getValue();
            if ("attach".equalsIgnoreCase(k) || "body".equalsIgnoreCase(k) || "sign".equalsIgnoreCase(k)) {
                sb.append(String.format("<%s><![CDATA[%s]]></%s>", k, v, k));
            } else {
                sb.append(String.format("<%s>%s</%s>", k, v, k));
            }
        }
        sb.append("</xml>");
        return sb.toString();
    }

    @SuppressWarnings("unchecked")
    public static Object[] parseXmlToList(String xml) {
        List<Map<String, String>> argMapList = new ArrayList<>();
        Map<String, String> retMap = new HashMap<>();
        try (StringReader read = new StringReader(xml)) {
            // 创建新的输入源SAX 解析器将使用 InputSource 对象来确定如何读取 XML 输入
            InputSource source = new InputSource(read);
            // 创建一个新的SAXBuilder
            SAXBuilder sb = new SAXBuilder();
            // 通过输入源构造一个Document
            Document doc = sb.build(source);
            Element root = doc.getRootElement(); // 指向根节点
            List<Element> es = root.getChildren();
            if (es != null && !es.isEmpty()) {
                for (Element element : es) {
                    retMap.put(element.getName(), element.getText());
                }
            }
            argMapList.add(retMap);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return new Object[]{argMapList};
    }


    /**
     * 提取出xml数据包中的加密消息
     *
     * @param xmltext 待提取的xml字符串
     * @return 提取出的加密消息字符串
     */
    public static Object[] extract(String xmltext) {
        Object[] result = new Object[3];
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
            dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            dbf.setXIncludeAware(false);
            dbf.setExpandEntityReferences(false);
            DocumentBuilder db = dbf.newDocumentBuilder();
            StringReader sr = new StringReader(xmltext);
            InputSource is = new InputSource(sr);
            org.w3c.dom.Document document = db.parse(is);

            org.w3c.dom.Element root = document.getDocumentElement();
            NodeList nodelist1 = root.getElementsByTagName("Encrypt");
            NodeList nodelist2 = root.getElementsByTagName("ToUserName");
            result[0] = 0;
            result[1] = nodelist1.item(0).getTextContent();
            if (nodelist2 != null && nodelist2.item(0) != null) {
                result[2] = nodelist2.item(0).getTextContent();
            }
            return result;
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new RuntimeException("xml解析失败");
        }
    }

    /**
     * 生成xml消息
     *
     * @param encrypt   加密后的消息密文
     * @param signature 安全签名
     * @param timestamp 时间戳
     * @param nonce     随机字符串
     * @return 生成的xml字符串
     */
    public static String generate(String encrypt, String signature, String timestamp, String nonce) {
        String format =
                "<xml>\n" +
                        "<Encrypt><![CDATA[%1$s]]></Encrypt>\n" +
                        "<MsgSignature><![CDATA[%2$s]]></MsgSignature>\n" +
                        "<TimeStamp>%3$s</TimeStamp>\n" +
                        "<Nonce><![CDATA[%4$s]]></Nonce>\n" +
                        "</xml>";
        return String.format(format, encrypt, signature, timestamp, nonce);
    }
}

加密结果

package com.honyee.app.utils.crypt;

/**
 * 加密结果
 *
 * @author honyee
 */
public class BizMsgEncryptResult {
    
    private String timestamp;
    private String nonce;
    private String signature;
    private String randomStr;
    private String encryptData;
    
    public String getTimestamp() {
        return timestamp;
    }
    
    public void setTimestamp(String timestamp) {
        this.timestamp = timestamp;
    }
    
    public String getNonce() {
        return nonce;
    }
    
    public void setNonce(String nonce) {
        this.nonce = nonce;
    }
    
    public String getSignature() {
        return signature;
    }
    
    public void setSignature(String signature) {
        this.signature = signature;
    }
    
    public String getRandomStr() {
        return randomStr;
    }
    
    public void setRandomStr(String randomStr) {
        this.randomStr = randomStr;
    }
    
    public String getEncryptData() {
        return encryptData;
    }
    
    public void setEncryptData(String encryptData) {
        this.encryptData = encryptData;
    }
    
    public BizMsgEncryptResult timestamp(String timestamp) {
        this.timestamp = timestamp;
        return this;
    }
    
    public BizMsgEncryptResult nonce(String nonce) {
        this.nonce = nonce;
        return this;
    }
    
    public BizMsgEncryptResult signature(String signature) {
        this.signature = signature;
        return this;
    }
    
    public BizMsgEncryptResult randomStr(String randomStr) {
        this.randomStr = randomStr;
        return this;
    }
    
    public BizMsgEncryptResult encryptData(String encryptData) {
        this.encryptData = encryptData;
        return this;
    }
}

对应小程序解密工具类

微信

package com.honyee.app.utils.crypt;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * 微信解密工具
 *
 * @author honyee
 */
public class WxBizMsgCrypt {

    // 解密时使用,这个值一般从各个平台提供的解密demo中获取获取
    private static final int RANDOM_BYTES_POS = 16;

    final byte[] aesKey;
    final String messageValidToken;
    final String appid;

    /**
     * 构造函数
     *
     * @param messageValidToken 公众平台上,开发者设置的 message_valid_token
     * @param encodingAesKey    公众平台上,开发者设置的 message_encode_decode_key
     * @param appid             公众平台appid
     */
    public WxBizMsgCrypt(String messageValidToken, String encodingAesKey, String appid) {
        if (encodingAesKey.length() != 43) {
            throw new RuntimeException("初始化失败:key长度不是43位");
        }

        this.messageValidToken = messageValidToken;
        this.appid = appid;
        aesKey = Base64.getDecoder().decode(encodingAesKey + "=");
    }

    /**
     * 对密文进行解密 用于移动端数据解密
     *
     * @param encryptedData 密文
     * @param iv            向量
     * @param sessionKey    密钥
     * @return 明文
     */
    public static String decrypt(String encryptedData, String iv, String sessionKey) {
        byte[] original = BizMsgCryptUtil.decrypt(encryptedData, iv, sessionKey);
        if (null != original && original.length > 0) {
            return new String(original, StandardCharsets.UTF_8);
        }
        return null;
    }

    /**
     * 将公众平台回复用户的消息加密打包. 用于服务端推送消息加密
     *
     * @param text      公众平台待回复用户的消息,xml格式的字符串
     * @param timestamp 时间戳,可以自己生成,也可以用URL参数的timestamp
     * @param nonce     随机串,可以自己生成,也可以用URL参数的nonce
     * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
     */
    public String encryptMsg(String text, String timestamp, String nonce) {
        String randomStr = BizMsgCryptUtil.getRandomStr();
        // 加密
        String encryptData = BizMsgCryptUtil.encrypt(appid, aesKey, randomStr, text);

        // 生成安全签名
        if ("".equals(timestamp)) {
            timestamp = Long.toString(System.currentTimeMillis());
        }
        String signature = BizMsgCryptUtil.getSignature(messageValidToken, timestamp, nonce, encryptData);

        // 生成发送的xml
        return XmlUtil.generate(encryptData, signature, timestamp, nonce);
    }

    /**
     * 检验消息的真实性,并且获取解密后的明文. 用于服务端推送消息解密
     *
     * @param msgSignature 签名串,对应URL参数的msg_signature
     * @param timestamp    时间戳,对应URL参数的timestamp
     * @param nonce        随机串,对应URL参数的nonce
     * @param postData     密文,对应POST请求的数据
     * @return 解密后的原文
     */
    public String decryptMsg(String msgSignature, String timestamp, String nonce, String postData) {
        // 提取密文
        Object[] encrypt = XmlUtil.extract(postData);
        // 验证安全签名
        BizMsgCryptUtil.verify(messageValidToken, msgSignature, timestamp, nonce, encrypt[1].toString());
        // 解密
        return BizMsgCryptUtil.decrypt(appid, encrypt[1].toString(), aesKey, RANDOM_BYTES_POS);
    }

}

字节跳动(抖音)

package com.honyee.app.utils.crypt;


import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * 抖音解密工具
 *
 * @author honyee
 */
public class BytedanceBizMsgCrypt {

    // 解密时使用,这个值一般从各个平台提供的解密demo中获取获取
    private static final int RANDOM_BYTES_POS = 32;

    final byte[] aesKey;
    final String messageValidToken;
    final String appid;

    /**
     * 构造函数
     *
     * @param messageValidToken 公众平台上,开发者设置的 message_valid_token
     * @param encodingAesKey    公众平台上,开发者设置的 message_encode_decode_key
     * @param appid             公众平台appid
     */
    public BytedanceBizMsgCrypt(String messageValidToken, String encodingAesKey, String appid) {
        if (encodingAesKey.length() != 43) {
            throw new RuntimeException("初始化失败:key长度不是43位");
        }

        this.messageValidToken = messageValidToken;
        this.appid = appid;
        aesKey = Base64.getDecoder().decode(encodingAesKey + "=");
    }

    /**
     * 对明文进行加密.
     * <p>
     * 加密后无法解密,加密pos=32,解密需要16,没找到哪里改
     *
     * @param text 需要加密的明文
     * @return 加密后base64编码的字符串
     */
    public BizMsgEncryptResult encrypt(String text, String timestamp, String nonce) {
        String randomStr = BizMsgCryptUtil.getRandomStr();
        // 加密
        String encryptData = BizMsgCryptUtil.encrypt(appid, aesKey, randomStr, text);

        // 生成安全签名
        if ("".equals(timestamp)) {
            timestamp = Long.toString(System.currentTimeMillis());
        }
        String signature = BizMsgCryptUtil.getSignature(messageValidToken, timestamp, nonce, encryptData);

        return new BizMsgEncryptResult()
                .timestamp(timestamp)
                .nonce(nonce)
                .signature(signature)
                .randomStr(randomStr)
                .encryptData(encryptData);
    }

    /**
     * 对密文进行解密 用于移动端数据解密
     *
     * @param encryptedData 密文
     * @param iv            向量
     * @param sessionKey    密钥
     * @return 明文
     */
    public static String decrypt(String encryptedData, String iv, String sessionKey) {
        byte[] original = BizMsgCryptUtil.decrypt(encryptedData, iv, sessionKey);
        if (null != original && original.length > 0) {
            return new String(original, StandardCharsets.UTF_8);
        }
        return null;
    }

    /**
     * 检验消息的真实性,并且获取解密后的明文. 用于服务端推送消息解密
     *
     * @param msgSignature 签名串,对应URL参数的msg_signature
     * @param timestamp    时间戳,对应URL参数的timestamp
     * @param nonce        随机串,对应URL参数的nonce
     * @param postData     密文,对应POST请求的数据
     * @return 解密后的原文
     */
    public String decryptMsg(String msgSignature, String timestamp, String nonce, String postData) {
        // 验证安全签名
        BizMsgCryptUtil.verify(messageValidToken, msgSignature, timestamp, nonce, postData);
        // 解密
        return BizMsgCryptUtil.decrypt(appid, postData, aesKey, RANDOM_BYTES_POS);
    }
}

百度(智能小程序)

package com.honyee.app.utils.crypt;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;

/**
 * 百度解密工具
 *
 * @author honyee
 */
public class BaiduBizMsgCrypt {

    // 解密时使用,这个值一般从各个平台提供的解密demo中获取获取
    private static final int RANDOM_BYTES_POS = 16;

    final byte[] aesKey;
    final String messageValidToken;
    final String appid;

    /**
     * 构造函数
     *
     * @param messageValidToken 公众平台上,开发者设置的 message_valid_token
     * @param encodingAesKey    公众平台上,开发者设置的 message_encode_decode_key
     * @param appid             公众平台appid
     */
    public BaiduBizMsgCrypt(String messageValidToken, String encodingAesKey, String appid) {
        if (encodingAesKey.length() != 43) {
            throw new RuntimeException("初始化失败:key长度不是43位");
        }

        this.messageValidToken = messageValidToken;
        this.appid = appid;
        aesKey = Base64.getDecoder().decode(encodingAesKey + "=");
    }

    /**
     * 对明文进行加密.
     *
     * @param text      公众平台待回复用户的消息,xml格式的字符串
     * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
     * @param nonce     随机串,可以自己生成,也可以用URL参数的nonce
     * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
     */
    public String encryptMsg(String text, String timeStamp, String nonce) {
        return null;
    }

    /**
     * 对密文进行解密 用于移动端数据解密
     *
     * @param encryptedData 密文
     * @param iv            向量
     * @param sessionKey    密钥
     * @return 明文
     */
    public static String decrypt(String encryptedData, String iv, String sessionKey) {
        byte[] original = BizMsgCryptUtil.decrypt(encryptedData, iv, sessionKey);
        if (null != original && original.length > 0) {
            try {
                // 去除补位字符
                byte[] bytes = PKCS7.decode(original);
                // ==== 分离
                int pos = RANDOM_BYTES_POS;
                // 分离16位随机字符串,网络字节序和ClientId
                byte[] networkOrder = Arrays.copyOfRange(bytes, pos, pos + 4);
                int contentLength = BizMsgCryptUtil.recoverNetworkBytesOrder(networkOrder);
                return new String(Arrays.copyOfRange(bytes, pos + 4, pos + 4 + contentLength), StandardCharsets.UTF_8);
            } catch (Exception e) {
                throw new RuntimeException("解密失败:" + e.getMessage());
            }
        }
        return null;
    }

    /**
     * 检验消息的真实性,并且获取解密后的明文. 用于服务端推送消息解密
     * <ol>
     * 	<li>利用收到的密文生成安全签名,进行签名验证</li>
     * 	<li>若验证通过,则提取xml中的加密消息</li>
     * 	<li>对消息进行解密</li>
     * </ol>
     *
     * @param msgSignature 签名串,对应URL参数的msg_signature
     * @param timestamp    时间戳,对应URL参数的timestamp
     * @param nonce        随机串,对应URL参数的nonce
     * @param postData     密文,对应POST请求的数据
     * @return 解密后的原文
     */
    public String decryptMsg(String msgSignature, String timestamp, String nonce, String postData) {
        // 验证安全签名
        BizMsgCryptUtil.verify(messageValidToken, msgSignature, timestamp, nonce, postData);
        // 解密
        return BizMsgCryptUtil.decrypt(appid, postData, aesKey, RANDOM_BYTES_POS);
    }

    /**
     * 验证
     */
    public void verify(String msgSignature, String timestamp, String nonce, String encryptData) {
        BizMsgCryptUtil.verify(messageValidToken, msgSignature, timestamp, nonce, encryptData);
    }
}

小红书

package com.honyee.app.utils.crypt;

import java.nio.charset.StandardCharsets;


/**
 * 百度解密工具
 *
 * @author honyee
 */
public class XhsBizMsgCrypt {
    // 解密时使用,这个值一般从各个平台提供的解密demo中获取获取
    private static final int RANDOM_BYTES_POS = 16;

    private final byte[] aesKey;
    private final String messageValidToken;
    private final String appid;

    /**
     * 构造函数
     *
     * @param messageValidToken 公众平台上,开发者设置的 message_valid_token
     * @param encodingAesKey    公众平台上,开发者设置的 message_encode_decode_key
     * @param appid             公众平台appid
     */
    public XhsBizMsgCrypt(String messageValidToken, String encodingAesKey, String appid) {
        this.aesKey = java.util.Base64.getDecoder().decode(encodingAesKey + "=");
        ;
        this.messageValidToken = messageValidToken;
        this.appid = appid;
    }

    /**
     * 检验消息的真实性,并且获取解密后的明文. 用于服务端推送消息解密
     * <ol>
     * 	<li>利用收到的密文生成安全签名,进行签名验证</li>
     * 	<li>若验证通过,则提取xml中的加密消息</li>
     * 	<li>对消息进行解密</li>
     * </ol>
     *
     * @param msgSignature 签名串,对应URL参数的msg_signature
     * @param timestamp    时间戳,对应URL参数的timestamp
     * @param nonce        随机串,对应URL参数的nonce
     * @param postData     密文,对应POST请求的数据
     * @return 解密后的原文
     */
    public String decryptMsg(String msgSignature, String timestamp, String nonce, String postData) {
        // 验证安全签名
        BizMsgCryptUtil.verify(messageValidToken, msgSignature, timestamp, nonce, postData);
        // 解密
        return BizMsgCryptUtil.decrypt(appid, postData, aesKey, RANDOM_BYTES_POS);
    }

    /**
     * 对密文进行解密
     *
     * @param encryptedData 密文
     * @param sessionKey    session2Code接口获取到的sessionKey
     * @param iv            向量
     * @return 解密后的明文
     */
    public static String decrypt(String encryptedData, String sessionKey, String iv) {
        byte[] original = BizMsgCryptUtil.decrypt(encryptedData, iv, sessionKey);
        if (null != original && original.length > 0) {
            try {
                // 去除补位字符
                byte[] bytes = PKCS7.decode(original);
                // ==== 分离
                //int pos = RANDOM_BYTES_POS;
                // 分离16位随机字符串,网络字节序和ClientId
                //byte[] networkOrder = Arrays.copyOfRange(bytes, pos, pos + 4);
                //int contentLength = BizMsgCryptUtil.recoverNetworkBytesOrder(networkOrder);
                //return new String(Arrays.copyOfRange(bytes, pos + 4, pos + 4 + contentLength), StandardCharsets.UTF_8);
                return new String(bytes, StandardCharsets.UTF_8);
            } catch (Exception e) {
                throw new RuntimeException("解密失败:" + e.getMessage());
            }
        }
        return null;
    }

}