import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
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 = 10; //TODO: 前后偏移10步,30*10=300s=5m 约前后5分钟内totp有效
-
-// static final ArrayList<String> DEFAULT_SCOPES = new ArrayList<>();
-// static {
-// DEFAULT_SCOPES.add("1"); //默认仅支持 1-大理车载消费场景
-// }
+ public static final String FIELD_UID = "uid";
+ public static final String FIELD_CARDNO = "cardNo";
+ public static final String FIELD_CARDTYPE = "cardType";
+ private static final String FIELD_TOTP = "totp";
+ private static final String FIELD_RANDOM = "random";
+ private static final String FIELD_SIGN = "sign";
+ private static final String DELIMITER = ":";
+ public static final long timeStep = 5; //totp步长 5s
+ public static final int totpDigits = 8; //totp取的位数
+
+ static final ArrayList<String> qrDataKeys = new ArrayList<>();
+
+ static {
+ qrDataKeys.add(FIELD_UID);
+ qrDataKeys.add(FIELD_CARDNO);
+ qrDataKeys.add(FIELD_CARDTYPE);
+ qrDataKeys.add(FIELD_TOTP);
+ qrDataKeys.add(FIELD_RANDOM);
+ qrDataKeys.add(FIELD_SIGN);
+ }
private Builder qrBuilder;
}
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 String uid; //手机注册的uid 系统用户唯一ID
+ private String cardNo; //市民卡号
+ private String cardType; //公交卡类别
+ private String prefix; //码前缀
+ private String mac;
+
private byte[] rootKey;
- private byte[] sKey;
- private byte[] des3Key;
- private String prefix;
- private boolean debug;
+ private byte[] iv = decodeHex("55b6f5b3287c535f8274b99354676d0e");
+ private String seed = "125ea2f97689988b6501";
+ private boolean rootKeySet = false;
+ private boolean resetIv = false;
+ private boolean resetSeed = false;
+
+ private boolean debug = false;
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");
+ public Builder rootKey(String rootKey) {
+ if (StringUtils.isBlank(rootKey)) {
+ throw new RuntimeException("rootKey is empty");
}
- this.appid = appid;
- this.des3Key = md5(appid.getBytes());
+ this.rootKey = decodeBase64(rootKey);
+ rootKeySet = true;
return this;
}
- public Builder uid(String uid) {
- if (StringUtils.isBlank(uid) || !StringUtils.isAsciiPrintable(uid)) {
- throw new RuntimeException("uid is invalid");
+ public Builder iv(String iv) {
+ if (StringUtils.isBlank(iv)) {
+ throw new RuntimeException("iv is empty");
}
- this.uid = uid;
+ this.iv = decodeHex(iv);
+ resetIv = true;
return this;
}
- public Builder scope(String scope) {
- if (StringUtils.isBlank(scope)) {
- throw new RuntimeException("scope is empty");
+ public Builder seed(String seed) {
+ if (StringUtils.isBlank(seed)) {
+ throw new RuntimeException("seed is empty");
}
- this.scope = scope;
+ this.seed = seed;
+ this.resetSeed = true;
return this;
}
- public Builder rootkey(String key) {
- this.rootKey = decodeHex(key);
+ 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 rootkey(String appid, String skey){
- if (StringUtils.isBlank(appid) || !StringUtils.isAsciiPrintable(appid)) {
- throw new RuntimeException("appid is invalid");
+ public Builder cardNo(String cardNo) {
+ if (StringUtils.isBlank(cardNo) || !StringUtils.isAsciiPrintable(cardNo)) {
+ throw new RuntimeException("cardNo 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);
+ this.cardNo = cardNo;
return this;
}
- public Builder seed(String s) {
- if (StringUtils.isBlank(s)) {
- throw new RuntimeException("seed is empty");
+ public Builder cardType(String cardType) {
+ if (StringUtils.isBlank(cardType) || !StringUtils.isAsciiPrintable(cardType)) {
+ throw new RuntimeException("cardType is invalid");
}
- this.seed = s;
- this.resetSeed = true;
+ this.cardType = cardType;
return this;
}
- public Builder ivHex(String v) {
- this.iv = decodeHex(v);
- this.resetIv = true;
+ public Builder card(String cardNo, String cardType) {
+ if (StringUtils.isBlank(cardNo) || !StringUtils.isAsciiPrintable(cardNo)) {
+ throw new RuntimeException("cardNo is invalid");
+ }
+ if (StringUtils.isBlank(cardType) || !StringUtils.isAsciiPrintable(cardType)) {
+ throw new RuntimeException("cardType is invalid");
+ }
+ this.cardNo = cardNo;
+ this.cardType = cardType;
return this;
}
private TimeBasedOneTimePasswordGenerator totpGenerator(int passwordLength) {
try {
- return new TimeBasedOneTimePasswordGenerator(30, TimeUnit.SECONDS, passwordLength,
+ return new TimeBasedOneTimePasswordGenerator(timeStep, TimeUnit.SECONDS, passwordLength,
TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("TOTP Error", e);
}
}
- private boolean checkScope(List<String> scopes) {
- return scopes.contains(DEFAULT_SCOPE);
- }
-
- private boolean verifyTotp(String totp, String secret, int offset) {
- String second = "30";
- String seed = secret;
+ private boolean verifyTotp(String totp, String seed, int offset) {
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);
+ long T = (time - T0) / timeStep;
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);
+ String key = TOTP.generateTOTP(seed, steps, String.valueOf(totpDigits), TimeBasedOneTimePasswordGenerator.TOTP_ALGORITHM_HMAC_SHA256);
keys[i] = key;
} catch (final Exception e) {
System.out.println("Error : " + e);
return false;
}
+ private byte[] getSignFactor(String data) {
+ return sha256(("{dlsmk_}" + data).getBytes());
+ }
+
+ private byte[] byteConcat(byte[] first, byte[] second) {
+ try {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ output.write(first);
+ output.write(second);
+ return output.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException("byte concat method error!!!");
+ }
+ }
+
+ private byte[] byteConcat(byte[] first, byte[] second, byte[] third) {
+ try {
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ output.write(first);
+ output.write(second);
+ output.write(third);
+ return output.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException("byte concat method error!!!");
+ }
+ }
+
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).toUpperCase());
+ logger.info("cardNo=" + qrBuilder.cardNo);
+ logger.info("cardType=" + qrBuilder.cardNo);
if (null != qrBuilder.prefix) logger.info("prefix=" + qrBuilder.prefix);
- logger.info("rootkey=" + encodeHex(qrBuilder.rootKey).toUpperCase());
+ if (qrBuilder.resetSeed) logger.info("seed=" + qrBuilder.seed);
+ if (qrBuilder.rootKeySet) logger.info("rootKey=" + encodeBase64(qrBuilder.rootKey));
+ if (qrBuilder.resetIv) logger.info("iv=" + encodeHex(qrBuilder.iv));
logger.info("=======================================================");
}
- final String randomStr = getRandomString(6); //随机数
- final String totp = genTOTPWithSeed(qrBuilder.seed, 8);
- final String encDataPlain = new StringJoin(DELIMITER)
+ if (null == qrBuilder.uid || null == qrBuilder.cardNo || null == qrBuilder.cardType) {
+ throw new RuntimeException("QR code params is null");
+ }
+
+ if (!qrBuilder.rootKeySet) {
+ throw new RuntimeException("QR decode root key must be set!");
+ }
+
+ final String randomStr = getRandomString(2); //随机数
+ final String totp = genTOTPWithSeed(qrBuilder.seed, totpDigits);
+ final byte[] factor = getSignFactor(qrBuilder.uid);
+ final String qrData = new StringJoin(DELIMITER)
.add(qrBuilder.uid)
- .add(qrBuilder.scope)
+ .add(qrBuilder.cardNo)
+ .add(qrBuilder.cardType)
.add(totp)
.add(randomStr).toString();
-
- final byte[] encData = aesEncryptCFB(qrBuilder.rootKey, encDataPlain.getBytes(), qrBuilder.iv);
- final String code = encodeBase64(encData);
if (qrBuilder.debug) {
- logger.info("encdata plain= " + encDataPlain);
+ logger.info("qrData = " + qrData);
}
-
+ final byte[] sign = sha256(byteConcat(qrData.getBytes(), factor));
+ final byte[] encDataPlain = byteConcat(qrData.getBytes(), DELIMITER.getBytes(), sign);
+ final byte[] encData = aesEncryptCFB(qrBuilder.rootKey, encDataPlain, qrBuilder.iv);
+ final String code = encodeBase64(encData);
+ String result = code;
if (qrBuilder.prefix != null) {
- return qrBuilder.prefix + code;
+ result = qrBuilder.prefix + code;
+ }
+ if (qrBuilder.debug) {
+ logger.info("QR code = " + result);
}
- return code;
+ return result;
}
- public String decodeUid(String qrcode) {
+ private Map<String, String> decode(String qrcode) {
if (qrBuilder.debug) {
- logger.info("appid=" + qrBuilder.appid);
+ logger.info("Start parsing QR code= "+ qrcode);
+ if (null != qrBuilder.prefix) logger.info("prefix=" + qrBuilder.prefix);
+
if (qrBuilder.resetSeed) logger.info("seed=" + qrBuilder.seed);
+ if (qrBuilder.rootKeySet) logger.info("rootKey=" + encodeBase64(qrBuilder.rootKey));
if (qrBuilder.resetIv) logger.info("iv=" + encodeHex(qrBuilder.iv));
- if (null != qrBuilder.sKey) logger.info("sKey=" + encodeHex(qrBuilder.sKey).toUpperCase());
- 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 (null == qrcode) {
+ throw new RuntimeException("QR code must not be null");
+ }
+
+ if (!qrBuilder.rootKeySet) {
+ throw new RuntimeException("QR decode root key must be set!");
}
+
if (qrBuilder.prefix != null) {
qrcode = qrcode.substring(qrBuilder.prefix.length());
}
- final String encDataPlain = new String(aesDecryptCFB(qrBuilder.rootKey, decodeBase64(qrcode), qrBuilder.iv));
+
+ byte[] code = aesDecryptCFB(qrBuilder.rootKey, decodeBase64(qrcode), qrBuilder.iv);
+ if (null == code) {
+ throw new RuntimeException("Unable to recognize QR code!");
+ }
+ final String encData = new String(code);
+ if (qrBuilder.debug) {
+ logger.info("QR code data: " + encData );
+ }
+ final String[] fields = encData.split(DELIMITER);
+ if (fields.length < 6) {
+ throw new RuntimeException("QR code plain text format error!");
+ }
+ Map<String, String> result = new HashMap<>();
+ for (int i = 0; i < fields.length && i < qrDataKeys.size(); ++i) {
+ result.put(qrDataKeys.get(i), fields[i]);
+ }
+ final String qrData;
+ {
+ final StringJoin sj = new StringJoin(DELIMITER);
+ for (int i = 0; i < 5; ++i) {
+ sj.add(result.get(qrDataKeys.get(i)));
+ }
+ qrData = sj.toString();
+ }
+ final String uid = result.get(FIELD_UID);
+ final byte[] factor = getSignFactor(uid);
+ final byte[] calcSign = sha256(byteConcat(qrData.getBytes(), factor));
if (qrBuilder.debug) {
- logger.info("Decode data : <" + encDataPlain + ">");
+ logger.info("calcSign=" + new String(calcSign));
+ logger.info("sign=" + result.get(FIELD_SIGN));
}
- String[] fields = encDataPlain.split(DELIMITER);
- if (fields.length < 4) {
- throw new RuntimeException("qrcode plain text format error!");
+ if (!new String(calcSign).equalsIgnoreCase(result.get(FIELD_SIGN))) {
+ throw new RuntimeException("QR code sign was not matched!");
}
- 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校验
+ return result;
+ }
+
+ /**
+ * 返回码数据不校验totp
+ * */
+ public Map<String, String> decodeWithoutTotpCheck(String qrcode) {
+ final Map<String, String> data = decode(qrcode);
+ if (null == data || data.isEmpty()) {
+ throw new RuntimeException("QR code parsing failure!");
}
- if (!checkScope(Arrays.asList(scopes))) {
- throw new RuntimeException("qrcode is not supported!"); //应用场景校验
+ this.qrBuilder.mac = new StringJoin(DELIMITER)
+ .add(data.get(FIELD_UID))
+ .add(data.get(FIELD_CARDNO))
+ .add(data.get(FIELD_CARDTYPE))
+ .add(data.get(FIELD_TOTP))
+ .add(data.get(FIELD_RANDOM)).toString(); //计算流水TAC的因子
+
+ Map<String, String> result = new HashMap<>();
+ result.put(FIELD_UID, data.get(FIELD_UID));
+ result.put(FIELD_CARDNO, data.get(FIELD_CARDNO));
+ result.put(FIELD_CARDTYPE, data.get(FIELD_CARDTYPE));
+ result.put(FIELD_TOTP, data.get(FIELD_TOTP)); //不校验totp,直接返回
+ return result;
+ }
+
+ /**
+ * 返回码数据校验totp
+ * */
+ public Map<String, String> decodeWithTotpCheck(String qrcode) {
+ return decodeWithTotpCheck(qrcode, 3); //默认15s失效
+ }
+
+ /**
+ * 返回码数据校验totp
+ * */
+ public Map<String, String> decodeWithTotpCheck(String qrcode, int offset) {
+ final Map<String, String> data = decode(qrcode);
+ if (null == data || data.isEmpty()) {
+ throw new RuntimeException("QR code parsing failure!");
+ }
+
+ //校验totp
+ if (!verifyTotp(data.get(FIELD_TOTP), this.qrBuilder.seed, offset)) {
+ throw new RuntimeException("qrcode is invalid!"); //二维码已无效
}
- return uid;
+ this.qrBuilder.mac = new StringJoin(DELIMITER)
+ .add(data.get(FIELD_UID))
+ .add(data.get(FIELD_CARDNO))
+ .add(data.get(FIELD_CARDTYPE))
+ .add(data.get(FIELD_TOTP))
+ .add(data.get(FIELD_RANDOM)).toString(); //计算流水TAC的因子
+
+ Map<String, String> result = new HashMap<>();
+ result.put(FIELD_UID, data.get(FIELD_UID));
+ result.put(FIELD_CARDNO, data.get(FIELD_CARDNO));
+ result.put(FIELD_CARDTYPE, data.get(FIELD_CARDTYPE));
+ return result;
}
- public String tac(String uid, Integer amount, String transdate, String transtime) {
- if (null == qrBuilder.appid) {
- throw new RuntimeException("appid is null");
+ public String tac(Integer amount, String transDate, String transTime) {
+ if (null == this.qrBuilder.mac) {
+ throw new RuntimeException("Please decode QR code first!");
}
- if (null == qrBuilder.sKey) {
- throw new RuntimeException("sKey is null");
+ if (this.qrBuilder.debug) {
+ logger.info("mac = " + this.qrBuilder.mac);
}
- final String encdata = uid + qrBuilder.appid + amount + transdate + transtime + "{" + encodeHex(qrBuilder.sKey).toUpperCase() + "}";
- return encodeHex(sha256(encdata.getBytes())).toUpperCase();
+
+ final String encData = this.qrBuilder.mac + amount + transDate + transTime + "{dlsmk}";
+ return encodeBase64(sha256(encData.getBytes())).toLowerCase();
}
}