Appendix 8
QR Generation
Embed Code
- PIN Code
- Truncated e-Signature
A 6-digit PIN protects the invoice from being claimed by an unauthorised person. The PIN may be customer-chosen at the point of sale, randomly generated, or set to 000000 if PIN protection is disabled. When randomly generated, a UUID-seeded pseudo-random number generator is used to produce a 6-digit value.
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());
}
}
A 16-character PUK (formatted as xxxx-xxxx-xxxx-xxxx) lets a customer unlock an e-Invoice that has been locked after repeated failed PIN attempts. The PUK is derived from the Seller ID and Unique Invoice ID using a cryptographic process that also produces the Truncated e-Signature used in the QR string.
Derivation steps
- Compute a SHA-256 MAC over the concatenation of Seller ID and Unique Invoice ID.
- Sign the MAC using ECDSA with the P-521 curve to produce an e-Signature.
- Take the last 10 bytes of the e-Signature, encode as ZBase32, and format as a 16-character string with hyphens — this is the PUK Code.
- Take the last 16 bytes of the e-Signature, encode as Base64, and truncate to 16 characters — this is the Truncated e-Signature.
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();
}
}