com.wallellen.wechat.common.util.crypto.WxCryptUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.wallellen.wechat.common.util.crypto.WxCryptUtil.java

Source

/*
 * This file Copyright (c) 2016. Walle.
 * (http://www.wallellen.com). All rights reserved.
 *
 *
 * This file is dual-licensed under both the
 * Walle Agreement (WA) and the GNU General Public License.
 * You may elect to use one or the other of these licenses.
 *
 * This file is distributed in the hope that it will be
 * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
 * Redistribution, except as permitted by whichever of the GPL
 * or WA you select, is prohibited.
 *
 * 1. For the GPL license (GPL), you can redistribute and/or
 * modify this file under the terms of the GNU General
 * Public License, Version 3, as published by the Free Software
 * Foundation.  You should have received a copy of the GNU
 * General Public License, Version 3 along with this program;
 * if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * 2. For the Walle Agreement (WA), this file
 * and the accompanying materials are made available under the
 * terms of the WA which accompanies this distribution, and
 * is available at http://www.wallellen.com/agreement.html
 *
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 */

// ------------------------------------------------------------------------

/**
 * org.apache.commons.codec.binary.Base64
 * ?commons-codec-1.9commons-codec-1.8
 * ?http://commons.apache.org/proper/commons-codec/download_codec.cgi
 */
package com.wallellen.wechat.common.util.crypto;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.InputSource;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.SortedMap;
import java.util.TreeMap;

public class WxCryptUtil {

    private static final Base64 base64 = new Base64();
    private static final Charset CHARSET = Charset.forName("utf-8");
    private static final String XML_TEMPLATE = "<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>";

    private static final ThreadLocal<DocumentBuilder> builderLocal = new ThreadLocal<DocumentBuilder>() {
        @Override
        protected DocumentBuilder initialValue() {
            try {
                return DocumentBuilderFactory.newInstance().newDocumentBuilder();
            } catch (ParserConfigurationException exc) {
                throw new IllegalArgumentException(exc);
            }
        }
    };

    protected byte[] aesKey;
    protected String token;
    protected String appidOrCorpid;

    public WxCryptUtil() {
        super();
    }

    /**
     * 
     *
     * @param token          ??token
     * @param encodingAesKey ??EncodingAESKey
     * @param appidOrCorpid  ?appid/corpid
     */
    public WxCryptUtil(String token, String encodingAesKey, String appidOrCorpid) {
        this.token = token;
        this.appidOrCorpid = appidOrCorpid;
        this.aesKey = Base64.decodeBase64(encodingAesKey + "=");
    }

    /**
     * ???(?:http://pay.weixin.qq.com/wiki/doc/api/index.php?chapter=4_3)
     *
     * @param packageParams ?
     * @param signKey       Key(? Key)
     * @return ??
     */
    public static String createSign(Map<String, String> packageParams, String signKey) {
        SortedMap<String, String> sortedMap = new TreeMap<>();
        sortedMap.putAll(packageParams);

        List<String> keys = new ArrayList<>(packageParams.keySet());
        Collections.sort(keys);

        StringBuffer toSign = new StringBuffer();
        for (String key : keys) {
            String value = packageParams.get(key);
            if (null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)) {
                toSign.append(key + "=" + value + "&");
            }
        }
        toSign.append("key=" + signKey);
        String sign = DigestUtils.md5Hex(toSign.toString()).toUpperCase();
        return sign;
    }

    static String extractEncryptPart(String xml) {
        try {
            DocumentBuilder db = builderLocal.get();
            Document document = db.parse(new InputSource(new StringReader(xml)));

            Element root = document.getDocumentElement();
            return root.getElementsByTagName("Encrypt").item(0).getTextContent();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * ???.
     * <ol>
     * <li>????AES-CBC</li>
     * <li>???</li>
     * <li>????xml?</li>
     * </ol>
     *
     * @param plainText ???xml?
     * @return ???msg_signature, timestamp, nonce, encryptxml?
     */
    public String encrypt(String plainText) {
        // 
        String encryptedXml = encrypt(genRandomStr(), plainText);

        // ???
        String timeStamp = Long.toString(System.currentTimeMillis() / 1000l);
        String nonce = genRandomStr();

        try {
            String signature = SHA1.gen(token, timeStamp, nonce, encryptedXml);
            return generateXml(encryptedXml, signature, timeStamp, nonce);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * .
     *
     * @param plainText ?
     * @return ?base64?
     */
    protected String encrypt(String randomStr, String plainText) {
        ByteGroup byteCollector = new ByteGroup();
        byte[] randomStringBytes = randomStr.getBytes(CHARSET);
        byte[] plainTextBytes = plainText.getBytes(CHARSET);
        byte[] bytesOfSizeInNetworkOrder = number2BytesInNetworkOrder(plainTextBytes.length);
        byte[] appIdBytes = appidOrCorpid.getBytes(CHARSET);

        // randomStr + networkBytesOrder + text + appid
        byteCollector.addBytes(randomStringBytes);
        byteCollector.addBytes(bytesOfSizeInNetworkOrder);
        byteCollector.addBytes(plainTextBytes);
        byteCollector.addBytes(appIdBytes);

        // ... + pad: ??
        byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
        byteCollector.addBytes(padBytes);

        // ?, 
        byte[] unencrypted = byteCollector.toBytes();

        try {
            // ?AESCBC?
            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.encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * ???.
     * <ol>
     * <li>??????</li>
     * <li>????xml?</li>
     * <li>?</li>
     * </ol>
     *
     * @param msgSignature ??URL?msg_signature
     * @param timeStamp    URL?timestamp
     * @param nonce        ?URL?nonce
     * @param encryptedXml POST?
     * @return ?
     */
    public String decrypt(String msgSignature, String timeStamp, String nonce, String encryptedXml) {
        // ?app corpSecret
        // ???
        String cipherText = extractEncryptPart(encryptedXml);

        try {
            // ???
            String signature = SHA1.gen(token, timeStamp, nonce, cipherText);
            if (!signature.equals(msgSignature)) {
                throw new RuntimeException("???");
            }
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }

        // 
        String result = decrypt(cipherText);
        return result;
    }

    /**
     * .
     *
     * @param cipherText ?
     * @return 
     */
    public String decrypt(String cipherText) {
        byte[] original;
        try {
            // ?AESCBC?
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
            IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
            cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);

            // BASE64?
            byte[] encrypted = Base64.decodeBase64(cipherText);

            // 
            original = cipher.doFinal(encrypted);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        String xmlContent, from_appid;
        try {
            // ?
            byte[] bytes = PKCS7Encoder.decode(original);

            // 16??,?AppId
            byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);

            int xmlLength = bytesNetworkOrder2Number(networkOrder);

            xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
            from_appid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        // appid??
        if (!from_appid.equals(appidOrCorpid)) {
            throw new RuntimeException("AppID?");
        }

        return xmlContent;

    }

    /**
     * ???4?bytes
     *
     * @param number
     */
    private byte[] number2BytesInNetworkOrder(int number) {
        byte[] orderBytes = new byte[4];
        orderBytes[3] = (byte) (number & 0xFF);
        orderBytes[2] = (byte) (number >> 8 & 0xFF);
        orderBytes[1] = (byte) (number >> 16 & 0xFF);
        orderBytes[0] = (byte) (number >> 24 & 0xFF);
        return orderBytes;
    }

    /**
     * 4?bytes?
     *
     * @param bytesInNetworkOrder
     */
    private int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) {
        int sourceNumber = 0;
        for (int i = 0; i < 4; i++) {
            sourceNumber <<= 8;
            sourceNumber |= bytesInNetworkOrder[i] & 0xff;
        }
        return sourceNumber;
    }

    /**
     * ??16?
     *
     * @return
     */
    private String genRandomStr() {
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        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();
    }

    /**
     * ?xml?
     *
     * @param encrypt   ??
     * @param signature ??
     * @param timestamp 
     * @param nonce     ?
     * @return ?xml
     */
    private String generateXml(String encrypt, String signature, String timestamp, String nonce) {
        return String.format(XML_TEMPLATE, encrypt, signature, timestamp, nonce);
    }
}