附录 8
QR 生成
嵌入代码
- PIN Code
- Truncated e-Signature
6 位 PIN 用于防止发票被未授权人员认领。PIN 可在销售时由客户自行选择、随机生成,或在禁用 PIN 保护时设为 000000。随机生成时,使用 UUID 种子的伪随机数生成器产生 6 位数值。
import java.util.UUID;
public class RandomPinGenerator {
public static String generatePin() {
UUID uuid = UUID.randomUUID();
long seed = uuid.getMostSignificantBits() ^ uuid.getLeastSignificantBits();
java.util.Random random = new java.util.Random(seed);
int pin = random.nextInt(900000) + 100000;
return String.format("%06d", pin);
}
public static void main(String[] args) {
System.out.println("Generated PIN: " + generatePin());
}
}
16 字符 PUK(格式为 xxxx-xxxx-xxxx-xxxx)用于在多次 PIN 输入失败后解锁被锁定的电子发票。PUK 由卖家 ID 和唯一发票 ID 通过加密过程推导得出,该过程同时生成 QR 字符串中使用的截断电子签名。
推导步骤
- 对卖家 ID 与唯一发票 ID 的拼接值计算 SHA-256 MAC。
- 使用 ECDSA P-521 曲线对 MAC 进行签名,生成电子签名。
- 取电子签名的最后 10 字节,编码为 ZBase32,并格式化为 16 字符带连字符的字符串 — 即为 PUK Code。
- 取电子签名的最后 16 字节,编码为 Base64,并截断为 16 字符 — 即为截断电子签名。
import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
import java.math.BigInteger;
import java.util.Base64;
import java.util.Arrays;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
public class Sample {
public static void main(String[] args) {
try {
String sellerID = "188888";
String invID = "20251004142345";
// Step 1: Concatenate Seller ID and Unique Invoice ID, then hash with SHA-256
String uniqueCode = sellerID + invID;
String hashedText = hash(uniqueCode);
// Generate an ECDSA key pair (secp521r1) and derive both key strings
KeyPair keyPair = generateECDSAKeyPair();
String privateKeyStr = getPrivateKeyStr(keyPair);
String publicKeyStr = getPublicKeyStr(keyPair);
PrivateKey privateKey = convertToPrivateKey(privateKeyStr);
// Step 2: Sign the hash via ECDSA P-521 to produce the e-Signature
byte[] signature = signMessage(hashedText, privateKey);
// Step 3: Create PUK Code
byte[] last10Bytes = new byte[10];
System.arraycopy(signature, signature.length - 10, last10Bytes, 0, 10);
String PUK = encode(last10Bytes);
System.out.println("PUK : " + formatPuk(PUK));
// Step 4: Create truncated e-Signature
byte[] last16Bytes = new byte[16];
System.arraycopy(signature, signature.length - 16, last16Bytes, 0, 16);
String truncatedESignature = Base64.getEncoder().encodeToString(last16Bytes).substring(0, 16);
} catch (Exception e) {
e.printStackTrace();
}
}
// Function to generate ECDSA key pair with secp521r1 curve
public static KeyPair generateECDSAKeyPair() throws NoSuchAlgorithmException,
InvalidAlgorithmParameterException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp521r1");
keyGen.initialize(ecSpec);
return keyGen.generateKeyPair();
}
// Function to sign a message using ECDSA with secp521r1
public static byte[] signMessage(String message, PrivateKey privateKey) throws Exception {
Signature ecdsaSign = Signature.getInstance("SHA512withECDSA");
ecdsaSign.initSign(privateKey);
byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);
ecdsaSign.update(messageBytes);
return ecdsaSign.sign();
}
public static String hash(String input) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
BigInteger number = new BigInteger(1, hashBytes);
StringBuilder hexString = new StringBuilder(number.toString(16));
while (hexString.length() < 64) {
hexString.insert(0, '0');
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String getPublicKeyStr(KeyPair keyPair) {
return Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded());
}
public static String getPrivateKeyStr(KeyPair keyPair) {
return Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded());
}
public static PrivateKey convertToPrivateKey(String base64PrivateKey) throws Exception {
byte[] privateKeyBytes = Base64.getDecoder().decode(base64PrivateKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
return privateKey;
}
private static final String ZBASE32_ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769";
private static final int[] ZBASE32_LOOKUP_TABLE = new int[128];
static {
Arrays.fill(ZBASE32_LOOKUP_TABLE, -1);
for (int i = 0; i < ZBASE32_ALPHABET.length(); i++) {
ZBASE32_LOOKUP_TABLE[ZBASE32_ALPHABET.charAt(i)] = i;
}
}
public static String encode(byte[] data) {
StringBuilder encoded = new StringBuilder((data.length * 8 + 4) / 5);
int buffer = data[0];
int next = 1;
int bitsLeft = 8;
while (bitsLeft > 0 || next < data.length) {
if (bitsLeft < 5) {
if (next < data.length) {
buffer <<= 8;
buffer |= (data[next++] & 0xff);
bitsLeft += 8;
} else {
int pad = 5 - bitsLeft;
buffer <<= pad;
bitsLeft += pad;
}
}
int index = 0x1f & (buffer >> (bitsLeft - 5));
bitsLeft -= 5;
encoded.append(ZBASE32_ALPHABET.charAt(index));
}
return encoded.toString();
}
public static byte[] decode(String encoded) {
byte[] decoded = new byte[encoded.length() * 5 / 8];
int buffer = 0;
int bitsLeft = 0;
int index = 0;
for (char c : encoded.toCharArray()) {
int lookup = ZBASE32_LOOKUP_TABLE[c];
if (lookup == -1) {
throw new IllegalArgumentException("Invalid character in zbase32 string: " + c);
}
buffer <<= 5;
buffer |= lookup;
bitsLeft += 5;
if (bitsLeft >= 8) {
decoded[index++] = (byte) (buffer >> (bitsLeft - 8));
bitsLeft -= 8;
}
}
return decoded;
}
public static String formatPuk(String input) {
StringBuilder sb = new StringBuilder(input.length() + input.length() / 4);
for (int i = 0; i < input.length(); i++) {
sb.append(input.charAt(i));
// Add a hyphen after every 4 characters, except at the end
if ((i + 1) % 4 == 0 && (i + 1) != input.length()) {
sb.append("-");
}
}
return sb.toString();
}
}