车载码测试
diff --git a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/PbocAlgorithem.java b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/PbocAlgorithem.java
index 49da114..3c26f8b 100644
--- a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/PbocAlgorithem.java
+++ b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/PbocAlgorithem.java
@@ -2,8 +2,8 @@
 
 import java.util.Arrays;
 
-import static com.supwisdom.dlpay.busQRcode.CryptoUtil.desDecryptECB;
-import static com.supwisdom.dlpay.busQRcode.CryptoUtil.desEncryptECB;
+import static com.supwisdom.dlpay.busqrcode.CryptoUtil.desDecryptECB;
+import static com.supwisdom.dlpay.busqrcode.CryptoUtil.desEncryptECB;
 
 
 public class PbocAlgorithem {
diff --git a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/QrCode.java b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/QrCode.java
index 5f8a7af..93f6b60 100644
--- a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/QrCode.java
+++ b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/QrCode.java
@@ -1,12 +1,11 @@
 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;
@@ -23,7 +22,7 @@
   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;
+  public static final int TOTP_OFFSET = 10; //TODO: 前后偏移10步,30*10=300s=5m 约前后5分钟内totp有效
 
 //  static final ArrayList<String> DEFAULT_SCOPES = new ArrayList<>();
 //  static {
@@ -203,10 +202,10 @@
       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.sKey) logger.info("sKey=" + encodeHex(qrBuilder.sKey).toUpperCase());
       if (null != qrBuilder.prefix) logger.info("prefix=" + qrBuilder.prefix);
 
-      logger.info("rootkey=" + encodeHex(qrBuilder.rootKey));
+      logger.info("rootkey=" + encodeHex(qrBuilder.rootKey).toUpperCase());
       logger.info("=======================================================");
     }
 
@@ -233,7 +232,7 @@
       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.sKey) logger.info("sKey=" + encodeHex(qrBuilder.sKey).toUpperCase());
       if (null != qrBuilder.prefix) logger.info("prefix=" + qrBuilder.prefix);
       logger.info("=======================================================");
     }
@@ -273,7 +272,7 @@
     if (null == qrBuilder.sKey) {
       throw new RuntimeException("sKey is null");
     }
-    final String encdata = uid + qrBuilder.appid + amount + transdate + transtime + "{" + encodeHex(qrBuilder.sKey) + "}";
+    final String encdata = uid + qrBuilder.appid + amount + transdate + transtime + "{" + encodeHex(qrBuilder.sKey).toUpperCase() + "}";
     return encodeHex(sha256(encdata.getBytes())).toUpperCase();
   }
 
diff --git a/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/TOTP.java b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/TOTP.java
new file mode 100644
index 0000000..450ca25
--- /dev/null
+++ b/bus-qrcode/src/main/java/com/supwisdom/dlpay/busqrcode/TOTP.java
@@ -0,0 +1,264 @@
+package com.supwisdom.dlpay.busqrcode;
+
+/**
+ Copyright (c) 2011 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, is permitted pursuant to, and subject to the license
+ terms contained in, the Simplified BSD License set forth in Section
+ 4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
+ (http://trustee.ietf.org/license-info).
+ */
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+
+/**
+ * This is an example implementation of the OATH TOTP algorithm. Visit
+ * www.openauthentication.org for more information.
+ * 
+ * @author Johan Rydell, PortWise, Inc.
+ */
+public class TOTP {
+	private static final int[] DIGITS_POWER
+	// 0 1 2 3 4 5 6 7 8
+	= { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };
+
+	private TOTP() {
+	}
+
+	/**
+	 * This method uses the JCE to provide the crypto algorithm. HMAC computes a
+	 * Hashed Message Authentication Code with the crypto hash algorithm as a
+	 * parameter.
+	 * 
+	 * @param crypto
+	 *            : the crypto algorithm (HmacSHA1, HmacSHA256, HmacSHA512)
+	 * @param keyBytes
+	 *            : the bytes to use for the HMAC key
+	 * @param text
+	 *            : the message or text to be authenticated
+	 */
+	private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
+		try {
+			Mac hmac;
+			hmac = Mac.getInstance(crypto);
+
+			SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
+			hmac.init(macKey);
+
+			return hmac.doFinal(text);
+		} catch (GeneralSecurityException gse) {
+			throw new UndeclaredThrowableException(gse);
+		}
+	}
+
+	/**
+	 * This method converts a HEX string to Byte[]
+	 * 
+	 * @param hex
+	 *            : the HEX string
+	 * 
+	 * @return: a byte array
+	 */
+	private static byte[] hexStr2Bytes(String hex) {
+		// Adding one byte to get the right conversion
+		// Values starting with "0" can be converted
+		byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
+
+		// Copy all the REAL bytes, not the "first"
+		byte[] ret = new byte[bArray.length - 1];
+
+		for (int i = 0; i < ret.length; i++)
+			ret[i] = bArray[i + 1];
+
+		return ret;
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP(String key, String time,
+			String returnDigits) {
+		return generateTOTP(key, time, returnDigits, "HmacSHA1");
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP256(String key, String time,
+			String returnDigits) {
+		return generateTOTP(key, time, returnDigits, "HmacSHA256");
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP512(String key, String time,
+			String returnDigits) {
+		return generateTOTP(key, time, returnDigits, "HmacSHA512");
+	}
+
+	/**
+	 * This method generates a TOTP value for the given set of parameters.
+	 * 
+	 * @param key
+	 *            : the shared secret, HEX encoded
+	 * @param time
+	 *            : a value that reflects a time
+	 * @param returnDigits
+	 *            : number of digits to return
+	 * @param crypto
+	 *            : the crypto function to use
+	 * 
+	 * @return: a numeric String in base 10 that includes
+	 *          {@link truncationDigits} digits
+	 */
+	public static String generateTOTP(String key, String time,
+			String returnDigits, String crypto) {
+		int codeDigits = Integer.decode(returnDigits).intValue();
+		String result = null;
+
+		// Using the counter
+		// First 8 bytes are for the movingFactor
+		// Compliant with base RFC 4226 (HOTP)
+		while (time.length() < 16)
+			time = "0" + time;
+
+		// Get the HEX in a Byte[]
+		byte[] msg = hexStr2Bytes(time);
+		byte[] k = hexStr2Bytes(key);
+		byte[] hash = hmac_sha(crypto, k, msg);
+
+		// put selected bytes into result int
+		int offset = hash[hash.length - 1] & 0xf;
+
+		int binary = ((hash[offset] & 0x7f) << 24)
+				| ((hash[offset + 1] & 0xff) << 16)
+				| ((hash[offset + 2] & 0xff) << 8) | (hash[offset + 3] & 0xff);
+
+		int otp = binary % DIGITS_POWER[codeDigits];
+
+		result = Integer.toString(otp);
+
+		while (result.length() < codeDigits) {
+			result = "0" + result;
+		}
+
+		return result;
+	}
+
+	public static void main(String[] args) {
+		long X = 30;
+		long T0 = 0;
+		String steps = "0";
+		long time = System.currentTimeMillis() / 1000;
+		System.out.println(time);
+		long T = (time - T0) / X;
+		steps = Long.toHexString(T).toUpperCase();
+		while (steps.length() < 16) {
+			steps = "0" + steps;
+		}
+		String seed = "9e8bb3d2df73741c041aef39e37ee015fc98233720a1350fcd67d5c3027896ac";
+		String totp = TOTP.generateTOTP(seed, steps, "8", "HmacSHA256");
+		System.out.println(totp);
+
+		/*
+
+
+		SecureRandom a = new SecureRandom();
+		byte[] b = a.generateSeed(8);
+		// Seed for HMAC-SHA1 - 20 bytes
+		String seed = "3132333435363738393031323334353637383930";
+
+		// Seed for HMAC-SHA256 - 32 bytes
+		//String seed32 = "3132333435363738393031323334353637383930313233343536373839303132";
+		String seed32 = "e5b72370f61687c8075b8383266f4fbcbb3b55da178eed76329666600c134093";
+
+		// Seed for HMAC-SHA512 - 64 bytes
+		String seed64 = "3132333435363738393031323334353637383930"
+				+ "3132333435363738393031323334353637383930"
+				+ "3132333435363738393031323334353637383930" + "31323334";
+		long T0 = 0;
+		long X = 30;
+		long[] testTime = { 59L, 1111111109L, 1111111111L, 1234567890L,
+				2000000000L, 20000000000L };
+
+		String steps = "0";
+		DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+		df.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+		try {
+			System.out.println("+---------------+-----------------------+"
+					+ "------------------+--------+--------+");
+			System.out.println("|  Time(sec)    |   Time (UTC format)   "
+					+ "| Value of T(Hex)  |  TOTP  | Mode   |");
+			System.out.println("+---------------+-----------------------+"
+					+ "------------------+--------+--------+");
+
+			for (int i = 0; i < testTime.length; i++) {
+				long T = (testTime[i] - T0) / X;
+				steps = Long.toHexString(T).toUpperCase();
+
+				while (steps.length() < 16)
+					steps = "0" + steps;
+
+				String fmtTime = String.format("%1$-11s", testTime[i]);
+				String utcTime = df.format(new Date(testTime[i] * 1000));
+				System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | "
+						+ steps + " |");
+				System.out.println(generateTOTP(seed, steps, "8", "HmacSHA1")
+						+ "| SHA1   |");
+				System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | "
+						+ steps + " |");
+				System.out.println(generateTOTP(seed32, steps, "8",
+						"HmacSHA256") + "| SHA256 |");
+				System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | "
+						+ steps + " |");
+				System.out.println(generateTOTP(seed64, steps, "8",
+						"HmacSHA512") + "| SHA512 |");
+
+				System.out.println("+---------------+-----------------------+"
+						+ "------------------+--------+--------+");
+			}
+		} catch (final Exception e) {
+			System.out.println("Error : " + e);
+		}
+
+		*/
+	}
+}
\ No newline at end of file
diff --git a/bus-qrcode/src/test/java/com/supwisdom/dlpay/busqrcode/QrcodeTest.java b/bus-qrcode/src/test/java/com/supwisdom/dlpay/busqrcode/QrcodeTest.java
index e69de29..9e8bae1 100644
--- a/bus-qrcode/src/test/java/com/supwisdom/dlpay/busqrcode/QrcodeTest.java
+++ b/bus-qrcode/src/test/java/com/supwisdom/dlpay/busqrcode/QrcodeTest.java
@@ -0,0 +1,61 @@
+package com.supwisdom.dlpay.busqrcode;
+
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.supwisdom.dlpay.busqrcode.BinUtil.*;
+import static com.supwisdom.dlpay.busqrcode.CryptoUtil.*;
+import static org.junit.Assert.*;
+
+public class QrcodeTest {
+    private Logger logger = LoggerFactory.getLogger(QrcodeTest.class);
+
+    @Test
+    public void testBusQrcode() {
+        String prefix = "dlsmk_";
+        String appid = "10001";
+        String uid = "ff8080816cf13e79016cf15d63f40011";
+        String scope = "1,2";
+        String rkey = "0A5DE6CE985D43989B7EBE64AD8EB9C3";
+        Integer amount = 100;
+        String transdate = "20200220";
+        String transtime = "090921";
+
+        String skey = encodeHex(desDecryptECB(md5(appid.getBytes()), decodeHex(rkey))).toUpperCase();
+        logger.info("skey=[" + skey + "]");
+        String rootKey = encodeHex(desEncryptECB(md5(appid.getBytes()), decodeHex(skey))).toUpperCase();
+        assertEquals("rootkey与skey互为3des加密关系错误!", rkey, rootKey);
+
+        QrCode encQr = QrCode.builder()
+                .appid(appid)
+                .rootkey(rkey)
+                .uid(uid)
+                .scope(scope)
+                .prefix(prefix)
+                .debug(true)
+                .create();
+        String qrcode = encQr.qrcode();
+        logger.info("qrcode=[" + qrcode + "]");
+
+        QrCode decQr = QrCode.builder()
+                .rootkey(appid, skey)
+                .prefix(prefix)
+                .debug(true)
+                .create();
+
+//        try {
+//            Thread.sleep(6*60*1000); //totp测试
+//        } catch (InterruptedException e) {
+//            e.printStackTrace();
+//        }
+
+        String decUid = decQr.decodeUid(qrcode);
+        assertEquals("解码uid错误!", uid, decUid);
+
+        String tac = decQr.tac(uid, amount, transdate, transtime);
+        String tacData = uid + appid + amount + transdate + transtime + "{" + skey + "}";
+        String testTac = encodeHex(sha256(tacData.getBytes())).toUpperCase();
+        assertEquals("tac计算错误", tac, testTac);
+    }
+}
\ No newline at end of file