From: Xia Kaixiang Date: Mon, 17 Feb 2020 10:00:03 +0000 (+0800) Subject: 车载码初步实现 X-Git-Tag: 1.0.12^2~10 X-Git-Url: https://source.supwisdom.com/gerrit/gitweb?a=commitdiff_plain;h=651763c81ba69fe9eb3c6016bd20c561da38f36a;p=epayment%2Ffood_payapi.git 车载码初步实现 --- diff --git a/payapi/build.gradle b/payapi/build.gradle index 76f80e3c..0a7d16ce 100644 --- a/payapi/build.gradle +++ b/payapi/build.gradle @@ -104,6 +104,7 @@ dependencies { implementation 'log4j:log4j:1.2.17' implementation 'com.alibaba:fastjson:1.2.60' + implementation 'com.eatthepath:java-otp:0.1.0' implementation project(':payapi-common') /*支付宝SDK*/ implementation group: 'com.alipay.sdk', name: 'alipay-sdk-java', version: '3.7.110.ALL' diff --git a/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/BinUtil.java b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/BinUtil.java new file mode 100644 index 00000000..5b8f43b7 --- /dev/null +++ b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/BinUtil.java @@ -0,0 +1,67 @@ +package com.supwisdom.dlpay.busQRcode; + +import org.apache.commons.codec.binary.Base64; + +public class BinUtil { + public static byte[] decodeHex(String data) { + if (data == null || data.length() % 2 != 0) { + throw new RuntimeException("decodeHex data length must be divided by 2"); + } + byte[] result = new byte[data.length() / 2]; + for (int i = 0; i < data.length(); i += 2) { + result[i / 2] = (byte) Integer.parseInt(data.substring(i, i + 2), 16); + } + return result; + } + + public static String encodeHex(byte[] data) { + if (data == null) { + throw new RuntimeException("encodeHex data must be not null"); + } + StringBuilder sb = new StringBuilder(); + for (byte datum : data) { + sb.append(String.format("%02x", datum & 0xFF)); + } + return sb.toString(); + } + + public static byte[] encodeBCD(String data) { + if (data == null || data.length() % 2 != 0) { + throw new RuntimeException("encodeBCD data length must be divided by 2"); + } + byte[] result = new byte[data.length() / 2]; + int base = (int)'0'; + for (int i = 0; i < data.length(); i += 2) { + int t = (int)data.charAt(i) - base; + int t1 = (int)data.charAt(i + 1) - base; + if (t < 0 || t > 9 || t1 < 0 || t1 > 9) { + throw new RuntimeException("encodeBCD char must be '0'~'9'"); + } + result[i / 2] = (byte) (((t << 4) | t1) & 0xFF); + } + return result; + } + + public static String decodeBCD(byte[] data) { + if (data == null) { + throw new RuntimeException("decodeBCD data must be not null"); + } + int base = (int)'0'; + StringBuilder sb = new StringBuilder(); + for (byte datum : data) { + int t1 = ((datum >> 4) & 0x0F) + base; + int t2 = (datum & 0x0F) + base; + sb.append((char) t1) + .append((char) t2); + } + return sb.toString(); + } + + public static String encodeBase64(byte[] data) { + return Base64.encodeBase64String(data); + } + + public static byte[] decodeBase64(String data) { + return Base64.decodeBase64(data); + } +} diff --git a/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/CryptoUtil.java b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/CryptoUtil.java new file mode 100644 index 00000000..75c4d082 --- /dev/null +++ b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/CryptoUtil.java @@ -0,0 +1,117 @@ +package com.supwisdom.dlpay.busQRcode; + +import javax.crypto.*; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.DESedeKeySpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Arrays; + +public class CryptoUtil { + + private static byte[] doCipher(Cipher cipher, SecretKey key, int mode, byte[] data) { + try { + cipher.init(mode, key); + return cipher.doFinal(data); + } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + e.printStackTrace(); + } + return null; + } + + private static byte[] doDesCipher(byte[] key, int cipherMode, byte[] data) { + try { + String mode; + KeySpec keySpec; + if (key.length == 8) { + mode = "DES"; + keySpec = new DESKeySpec(key); + } else if (key.length == 16) { + mode = "DESede"; + byte[] keyEDE = new byte[24]; + System.arraycopy(key, 0, keyEDE, 0, 16); + System.arraycopy(key, 0, keyEDE, 16, 8); + keySpec = new DESedeKeySpec(keyEDE); + } else { + throw new RuntimeException("DES Key length must be 8 or 16 bytes"); + } + Cipher keyCipher = Cipher.getInstance(mode + "/ECB/NoPadding"); + SecretKey secret = SecretKeyFactory.getInstance(mode).generateSecret(keySpec); + return doCipher(keyCipher, secret, cipherMode, data); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeySpecException | InvalidKeyException e) { + e.printStackTrace(); + } + return null; + } + + public static byte[] desEncryptECB(byte[] key, byte[] plain) { + return doDesCipher(key, Cipher.ENCRYPT_MODE, plain); + } + + public static byte[] desDecryptECB(byte[] key, byte[] cipher) { + return doDesCipher(key, Cipher.DECRYPT_MODE, cipher); + } + + private static byte[] messageDigest(String alg, byte[] data) { + try { + MessageDigest md = MessageDigest.getInstance(alg); + md.update(data); + return md.digest(); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return null; + } + + public static byte[] md5(byte[] data) { + return messageDigest("MD5", data); + } + + public static byte[] sha256(byte[] data) { + return messageDigest("SHA-256", data); + } + + public static byte[] sha1(byte[] data) { + return messageDigest("SHA-1", data); + } + + private static byte[] doAESCipher(byte[] key, byte[] iv, byte[] data, int mode) { + try { + Cipher keyCipher = Cipher.getInstance("AES/CFB/NoPadding"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + SecretKeySpec secret = new SecretKeySpec(key, "AES"); + keyCipher.init(mode, secret, ivSpec); + return keyCipher.doFinal(data); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) { + e.printStackTrace(); + } + return null; + } + + public static byte[] aesEncryptCFB(byte[] key, byte[] plain) { + byte[] iv = new byte[16]; + Arrays.fill(iv, (byte) 0); + return aesEncryptCFB(key, plain, iv); + } + + public static byte[] aesEncryptCFB(byte[] key, byte[] plain, byte[] iv) { + return doAESCipher(key, iv, plain, Cipher.ENCRYPT_MODE); + } + + public static byte[] aesDecryptCFB(byte[] key, byte[] cipher, byte[] iv) { + return doAESCipher(key, iv, cipher, Cipher.DECRYPT_MODE); + } + + public static byte[] aesDecryptCFB(byte[] key, byte[] cipher) { + byte[] iv = new byte[16]; + Arrays.fill(iv, (byte) 0); + return aesDecryptCFB(key, cipher, iv); + } +} diff --git a/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/PbocAlgorithem.java b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/PbocAlgorithem.java new file mode 100644 index 00000000..f63b4f1c --- /dev/null +++ b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/PbocAlgorithem.java @@ -0,0 +1,93 @@ +package com.supwisdom.dlpay.busQRcode; + +import java.util.Arrays; + +import static com.supwisdom.dlpay.busQRcode.CryptoUtil.desDecryptECB; +import static com.supwisdom.dlpay.busQRcode.CryptoUtil.desEncryptECB; + + +public class PbocAlgorithem { + private static final byte[] PADDING = new byte[]{(byte) 0x80, 0, 0, 0, 0, 0, 0, 0}; + + private static byte[] doDeliveryKey(byte[] key, byte[] factor) { + if (key.length == 16) { + byte[] factor1 = new byte[8]; + for (int i = 0; i < 8; i++) { + factor1[i] = (byte) ((~((int) factor[i] & 0xFF)) & 0xFF); + } + byte[] d1 = desEncryptECB(key, factor); + byte[] d2 = desEncryptECB(key, factor1); + byte[] result = new byte[16]; + System.arraycopy(d1, 0, result, 0, 8); + System.arraycopy(d2, 0, result, 8, 8); + return result; + } else if (key.length == 8) { + return desEncryptECB(key, factor); + } else { + throw new RuntimeException("Des key length must be 8 or 16"); + } + } + + public static byte[] deliveryKey(byte[] rootKey, byte[] factory) { + if (factory.length % 8 != 0) { + throw new RuntimeException("delivery key factory length must be divived by 8"); + } + if (factory.length / 8 > 3) { + throw new RuntimeException("delivery key factor length must be less or equal 24"); + } + + byte[] deliveryKey = rootKey; + + for (int i = 0; i < factory.length; i += 8) { + deliveryKey = doDeliveryKey(deliveryKey, Arrays.copyOfRange(factory, i, i + 8)); + } + return deliveryKey; + } + + private static byte[] doDesMac(byte[] key, byte[] init, byte[] data) { + byte[] temp = Arrays.copyOf(init, 8); + for (int offset = 0; offset < data.length; offset += 8) { + int i; + for (i = 0; i < 8 && i + offset < data.length; ++i) { + temp[i] = (byte) (((int) temp[i] & 0xFF) ^ ((int) data[i + offset] & 0xFF)); + } + for (int j = i; i < 8; ++i) { + temp[i] = (byte) (((int) temp[i] & 0xFF) ^ ((int) PADDING[i - j] & 0xFF)); + } + temp = desEncryptECB(key, temp); + } + return temp; + } + + public static byte[] desMac(byte[] key, byte[] init, byte[] data) { + byte[] buffer = Arrays.copyOfRange(doDesMac(key, init, data), 0, 4); + if (data.length % 8 == 0) { + buffer = doDesMac(key, buffer, PADDING); + } + return Arrays.copyOfRange(buffer, 0, 4); + } + + public static byte[] desMac(byte[] key, byte[] data) { + byte[] init = new byte[8]; + Arrays.fill(init, (byte) 0); + return desMac(key, init, data); + } + + public static byte[] des3Mac(byte[] key, byte[] init, byte[] data) { + byte[] k1 = Arrays.copyOfRange(key, 0, 8); + byte[] k2 = Arrays.copyOfRange(key, 8, 16); + byte[] buffer = doDesMac(k1, init, data); + if (data.length % 8 == 0) { + buffer = doDesMac(key, buffer, PADDING); + } + buffer = desDecryptECB(k2, buffer); + buffer = desEncryptECB(k1, buffer); + return Arrays.copyOfRange(buffer, 0, 4); + } + + public static byte[] des3Mac(byte[] key, byte[] data) { + byte[] init = new byte[8]; + Arrays.fill(init, (byte) 0); + return des3Mac(key, init, data); + } +} diff --git a/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/QrCode.java b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/QrCode.java new file mode 100644 index 00000000..35a50e0d --- /dev/null +++ b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/QrCode.java @@ -0,0 +1,280 @@ +package com.supwisdom.dlpay.busQRcode; + +import com.eatthepath.otp.TimeBasedOneTimePasswordGenerator; +import com.supwisdom.dlpay.util.TOTP; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static com.supwisdom.dlpay.busQRcode.BinUtil.*; +import static com.supwisdom.dlpay.busQRcode.CryptoUtil.*; + +public class QrCode { + private static Logger logger = LoggerFactory.getLogger(QrCode.class); + + public static final String DELIMITER = ":"; + public static final String SCOPE_DELIMITER = ","; + public static final String DEFAULT_SCOPE = "1"; //默认仅支持 1-大理车载消费场景 + public static final int TOTP_OFFSET = 30; + +// static final ArrayList DEFAULT_SCOPES = new ArrayList<>(); +// static { +// DEFAULT_SCOPES.add("1"); //默认仅支持 1-大理车载消费场景 +// } + + private Builder qrBuilder; + + private QrCode(Builder b) { + this.qrBuilder = b; + } + + public static class Builder { + private String appid; + private String uid; + private String scope; + private String seed = "a8aae1a41ffab089428e8e21381cc34af3b614ac7b7cba50f58470b8e50c6072"; + private boolean resetSeed = false; + private byte[] iv = decodeHex("55b6f5b3287c535f8274a99354676d0b"); + private boolean resetIv = false; + private byte[] rootKey; + private byte[] sKey; + private byte[] des3Key; + private String prefix; + private boolean debug; + + public QrCode create() { + return new QrCode(this); + } + + public Builder appid(String appid) { + if (StringUtils.isBlank(appid) || !StringUtils.isAsciiPrintable(appid)) { + throw new RuntimeException("appid is invalid"); + } + this.appid = appid; + this.des3Key = md5(appid.getBytes()); + return this; + } + + public Builder uid(String uid) { + if (StringUtils.isBlank(uid) || !StringUtils.isAsciiPrintable(uid)) { + throw new RuntimeException("uid is invalid"); + } + this.uid = uid; + return this; + } + + public Builder scope(String scope) { + if (StringUtils.isBlank(scope)) { + throw new RuntimeException("scope is empty"); + } + this.scope = scope; + return this; + } + + public Builder rootkey(String key) { + this.rootKey = decodeHex(key); + return this; + } + + public Builder rootkey(String appid, String skey){ + if (StringUtils.isBlank(appid) || !StringUtils.isAsciiPrintable(appid)) { + throw new RuntimeException("appid is invalid"); + } + if (StringUtils.isBlank(skey)) { + throw new RuntimeException("sKey is empty"); + } + this.appid = appid; + this.sKey = decodeHex(skey); + this.des3Key = md5(appid.getBytes()); + this.rootKey = desEncryptECB(this.des3Key, this.sKey); + return this; + } + + public Builder seed(String s) { + if (StringUtils.isBlank(s)) { + throw new RuntimeException("seed is empty"); + } + this.seed = s; + this.resetSeed = true; + return this; + } + + public Builder ivHex(String v) { + this.iv = decodeHex(v); + this.resetIv = true; + return this; + } + + public Builder prefix(String p) { + if (p == null) { + throw new RuntimeException("prefix must not be null"); + } + this.prefix = p; + return this; + } + + public Builder debug(boolean on) { + this.debug = on; + return this; + } + } + + public static Builder builder() { + return new Builder(); + } + + private static String getRandomString(int length) { + String base = "abcdefghijklmnopqrstuvwxyz234567"; + Random random = new Random(System.currentTimeMillis()); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + int number = random.nextInt(base.length()); + sb.append(base.charAt(number)); + } + return sb.toString(); + } + + private String genTOTPWithSeed(String seed, int pl) { + final TimeBasedOneTimePasswordGenerator alg = totpGenerator(pl); + final SecretKey secretKey = new SecretKeySpec(decodeHex(seed), + TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256); + try { + Date now = Calendar.getInstance().getTime(); + return StringUtils.leftPad(String.format("%d", alg.generateOneTimePassword(secretKey, now)), + pl, '0'); + } catch (InvalidKeyException e) { + throw new RuntimeException("TOTP Error", e); + } + } + + private TimeBasedOneTimePasswordGenerator totpGenerator(int passwordLength) { + try { + return new TimeBasedOneTimePasswordGenerator(30, TimeUnit.SECONDS, passwordLength, + TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("TOTP Error", e); + } + } + + private boolean checkScope(List scopes) { + return scopes.contains(DEFAULT_SCOPE); + } + + private boolean verifyTotp(String totp, String secret, int offset) { + String second = "30"; + String seed = secret; + long T0 = 0; + String[] keys = new String[offset * 2 + 1]; + long time = Calendar.getInstance().getTime().getTime() / 1000; + for (int i = 0; i < keys.length; i++) { + String steps = "0"; + try { + long T = (time - T0) / Long.parseLong(second); + steps = Long.toHexString(T + (i - offset)).toUpperCase(); + while (steps.length() < 16) { + steps = "0" + steps; + } + String key = TOTP.generateTOTP(secret, steps, "8", TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256); + keys[i] = key; + } catch (final Exception e) { + System.out.println("Error : " + e); + } + } + for (String key : keys) { + if (key.equals(totp)) { + return true; + } + } + return false; + } + + public String qrcode() { + if (qrBuilder.debug) { + logger.info("appid=" + qrBuilder.appid); + logger.info("uid=" + qrBuilder.uid); + logger.info("scope=" + qrBuilder.scope); + if (qrBuilder.resetSeed) logger.info("seed=" + qrBuilder.seed); + if (qrBuilder.resetIv) logger.info("iv=" + encodeHex(qrBuilder.iv)); + if (null != qrBuilder.sKey) logger.info("sKey=" + encodeHex(qrBuilder.sKey)); + if (null != qrBuilder.prefix) logger.info("prefix=" + qrBuilder.prefix); + + logger.info("rootkey=" + encodeHex(qrBuilder.rootKey)); + logger.info("======================================================="); + } + + final String totp = genTOTPWithSeed(qrBuilder.seed, 8); + final String encDataPlain = new StringJoin(DELIMITER) + .add(qrBuilder.uid) + .add(qrBuilder.scope) + .add(totp).toString(); + + final byte[] encData = aesEncryptCFB(qrBuilder.rootKey, encDataPlain.getBytes(), qrBuilder.iv); + final String code = encodeBase64(encData); + if (qrBuilder.debug) { + logger.info("encdata plain= " + encDataPlain); + } + + if (qrBuilder.prefix != null) { + return qrBuilder.prefix + code; + } + return code; + } + + public String decodeUid(String qrcode) { + if (qrBuilder.debug) { + logger.info("appid=" + qrBuilder.appid); + if (qrBuilder.resetSeed) logger.info("seed=" + qrBuilder.seed); + if (qrBuilder.resetIv) logger.info("iv=" + encodeHex(qrBuilder.iv)); + if (null != qrBuilder.sKey) logger.info("sKey=" + encodeHex(qrBuilder.sKey)); + if (null != qrBuilder.prefix) logger.info("prefix=" + qrBuilder.prefix); + logger.info("======================================================="); + } + if (null == qrBuilder.rootKey) { + throw new RuntimeException("qr decode root key must not be null"); + } + if (qrBuilder.prefix != null) { + qrcode = qrcode.substring(qrBuilder.prefix.length()); + } + final String encDataPlain = new String(aesDecryptCFB(qrBuilder.rootKey, decodeBase64(qrcode), qrBuilder.iv)); + if (qrBuilder.debug) { + logger.info("Decode data : <" + encDataPlain + ">"); + } + String[] fields = encDataPlain.split(DELIMITER); + if (fields.length < 3) { + throw new RuntimeException("qrcode plain text format error!"); + } + String uid = fields[0]; + String[] scopes = fields[1].split(SCOPE_DELIMITER); + String totp = fields[2]; + + if (!verifyTotp(totp, qrBuilder.seed, TOTP_OFFSET)) { + throw new RuntimeException("qrcode is invalid!"); //TOTP校验 + } + + if (!checkScope(Arrays.asList(scopes))) { + throw new RuntimeException("qrcode is not supported!"); //应用场景校验 + } + + return uid; + } + + public String tac(String uid, Integer amount, String transdate, String transtime) { + if (null == qrBuilder.appid) { + throw new RuntimeException("appid is null"); + } + if (null == qrBuilder.sKey) { + throw new RuntimeException("sKey is null"); + } + final String encdata = uid + qrBuilder.appid + amount + transdate + transtime + "{" + encodeHex(qrBuilder.sKey) + "}"; + return encodeHex(sha256(encdata.getBytes())).toUpperCase(); + } + +} diff --git a/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/StringJoin.java b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/StringJoin.java new file mode 100644 index 00000000..fdc5ca87 --- /dev/null +++ b/payapi/src/main/java/com/supwisdom/dlpay/busQRcode/StringJoin.java @@ -0,0 +1,30 @@ +package com.supwisdom.dlpay.busQRcode; + +public class StringJoin { + private String delimiter; + private StringBuilder builder; + + public StringJoin(String delimiter) { + if(delimiter == null) { + throw new RuntimeException("Delimiter must not be null"); + } + this.delimiter = delimiter; + builder = new StringBuilder(); + } + + public StringJoin add(String field) { + if(field == null) { + throw new RuntimeException("added value must not be null"); + } + if(builder.length() > 0) { + builder.append(this.delimiter); + } + builder.append(field); + return this; + } + + @Override + public String toString() { + return builder.toString(); + } +}