Skip to content

Instantly share code, notes, and snippets.

@tsenger
Created September 14, 2020 06:34
Show Gist options
  • Save tsenger/3d6e264e75f179cbecd66b690c2ab1d1 to your computer and use it in GitHub Desktop.
Save tsenger/3d6e264e75f179cbecd66b690c2ab1d1 to your computer and use it in GitHub Desktop.
package de.tsenger.kasper.decoder;
import org.spongycastle.asn1.ASN1GeneralizedTime;
import org.spongycastle.asn1.ASN1Integer;
import org.spongycastle.asn1.ASN1ObjectIdentifier;
import org.spongycastle.asn1.ASN1UTCTime;
import org.spongycastle.asn1.DEROctetString;
import org.spongycastle.asn1.DERPrintableString;
import org.spongycastle.asn1.DERSequence;
import org.spongycastle.asn1.DERTaggedObject;
import org.spongycastle.crypto.digests.SHA256Digest;
import org.spongycastle.util.encoders.Hex;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
public class KaBonDecoder {
final static int LOG_VERSION = 2;
final static String QR_CODE_VERSION = "V0";
static Map<String, String> supportedSigAlgorithms;
static {
supportedSigAlgorithms = new HashMap<>();
supportedSigAlgorithms.put("ecdsa-plain-SHA224","0.4.0.127.0.7.1.1.4.1.2");
supportedSigAlgorithms.put("ecdsa-plain-SHA256","0.4.0.127.0.7.1.1.4.1.3");
supportedSigAlgorithms.put("ecdsa-plain-SHA384","0.4.0.127.0.7.1.1.4.1.4");
supportedSigAlgorithms.put("ecdsa-plain-SHA512","0.4.0.127.0.7.1.1.4.1.5");
supportedSigAlgorithms.put("ecdsa-plain-SHA3-224","0.4.0.127.0.7.1.1.4.1.8");
supportedSigAlgorithms.put("ecdsa-plain-SHA3-256","0.4.0.127.0.7.1.1.4.1.9");
supportedSigAlgorithms.put("ecdsa-plain-SHA3-384","0.4.0.127.0.7.1.1.4.1.10");
supportedSigAlgorithms.put("ecdsa-plain-SHA3-512","0.4.0.127.0.7.1.1.4.1.11");
}
enum QRCodeFields {
qrCodeVersion,
kassenSerienNr,
processType,
processData,
transaktionsNr,
signaturZaehler,
startZeit, // YYYY-MM-DDThh:mm:ss.fffZ
logTime,
sigAlg,
logTimeFormat,
signatur,
publicKey
}
EnumMap<QRCodeFields, String> qrcode = new EnumMap<>(QRCodeFields.class);
public KaBonDecoder(String rawString) {
qrcode = new EnumMap<>(QRCodeFields.class);
decodeRawString(rawString);
}
public byte[] getSignatureBytes() {
return Base64.getDecoder().decode(qrcode.get(QRCodeFields.signatur));
}
public byte[] getPublicKeyBytes() {
return Base64.getDecoder().decode(qrcode.get(QRCodeFields.publicKey));
}
public String getClientId() {
return qrcode.get(QRCodeFields.kassenSerienNr);
}
public String getProcessType() {
return qrcode.get(QRCodeFields.processType);
}
public String getProcessData() {
return qrcode.get(QRCodeFields.processData);
}
public byte[] getProcessDataBytes() {
return qrcode.get(QRCodeFields.processData).getBytes();
}
public long getTransactionNumber() {
try {
return Long.parseUnsignedLong(qrcode.get(QRCodeFields.transaktionsNr).trim());
} catch (NumberFormatException e) {
return -1;
}
}
public String getLogTime() {
return qrcode.get(QRCodeFields.logTime);
}
public String getLogTimeFormat() {
return qrcode.get(QRCodeFields.logTimeFormat);
}
public String getSigAlgorithm() {
return qrcode.get(QRCodeFields.sigAlg);
}
public long getSignatureCounter() {
try {
return Long.parseUnsignedLong(qrcode.get(QRCodeFields.signaturZaehler).trim());
} catch (NumberFormatException e) {
return -1;
}
}
public String getSerialNumberTSE() {
return Hex.toHexString(getTseSerialNumber(getPublicKeyBytes()));
}
public KassenbelegV1 getKassenBeleg() {
return new KassenbelegV1(qrcode.get(QRCodeFields.processData));
}
public String getQRCodeValues() {
return qrcode.toString();
}
/**
* Returns message for signature verification
*
* @return message := version||certifiedDataType||
* certifiedData||serialNumber||
* signatureAlgorithm||seAuditData||
* signatureCounter||logTime
* @throws IOException
*/
public byte[] getMessage() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//version
ASN1Integer logVersion = new ASN1Integer(LOG_VERSION);
baos.write(logVersion.getEncoded());
//certifiedDataType: id-SE-API-transaction-log
ASN1ObjectIdentifier certifiedDataType = new ASN1ObjectIdentifier("0.4.0.127.0.7.3.7.1.1");
baos.write(certifiedDataType.getEncoded());
//certifiedData
baos.write(getCertifiedData());
//serialNumber
DEROctetString serialNumber = new DEROctetString(getTseSerialNumber(getPublicKeyBytes()));
baos.write(serialNumber.getEncoded());
//signatureAlgorithm
ASN1ObjectIdentifier sigAlgId = new ASN1ObjectIdentifier(supportedSigAlgorithms.get(getSigAlgorithm()));
DERSequence sigAlgSeq = new DERSequence(sigAlgId);
baos.write(sigAlgSeq.getEncoded());
//signatureCounter
ASN1Integer signatureCounter = new ASN1Integer(getSignatureCounter());
baos.write(signatureCounter.getEncoded());
//logTime
ZonedDateTime zonedDateTime = ZonedDateTime.parse(getLogTime());
switch (getLogTimeFormat()) {
case "unixTime":
ASN1Integer unixTime = new ASN1Integer(zonedDateTime.toEpochSecond());
baos.write(unixTime.getEncoded());
break;
case "utcTime":
ASN1UTCTime utcTime = new ASN1UTCTime(Date.from(zonedDateTime.toInstant()));
baos.write(utcTime.getEncoded());
break;
case "generalizedTime":
ASN1GeneralizedTime generalizedTime = new ASN1GeneralizedTime(Date.from(zonedDateTime.toInstant()));
baos.write(generalizedTime.getEncoded());
break;
}
return baos.toByteArray();
}
private byte[] getTseSerialNumber(byte[] pubKeyBytes) {
SHA256Digest sha256 = new SHA256Digest();
sha256.update(pubKeyBytes,0,pubKeyBytes.length);
byte[] serialNumberBytes = new byte[32];
sha256.doFinal(serialNumberBytes, 0);
return serialNumberBytes;
}
private byte[] getCertifiedData() throws IOException {
DERTaggedObject obj80 = new DERTaggedObject(false,0, new DERPrintableString("FinishTransaction"));
DERTaggedObject obj81 = new DERTaggedObject(false,1, new DERPrintableString(getClientId()));
DERTaggedObject obj82 = new DERTaggedObject(false,2, new DEROctetString(getProcessDataBytes()));
DERTaggedObject obj83 = new DERTaggedObject(false,3, new DERPrintableString(getProcessType()));
DERTaggedObject obj85 = new DERTaggedObject(false,5, new ASN1Integer(getTransactionNumber()));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(obj80.getEncoded());
baos.write(obj81.getEncoded());
baos.write(obj82.getEncoded());
baos.write(obj83.getEncoded());
baos.write(obj85.getEncoded());
return baos.toByteArray();
}
private static String removeUTF8BOM(String s) {
byte[] rawBytes = s.getBytes();
if (rawBytes[0]==(byte)0xef && rawBytes[1]==(byte)0xbb && rawBytes[2]==(byte)0xbf) {
byte[] newBytes = new byte[rawBytes.length - 3];
System.arraycopy(rawBytes, 3, newBytes, 0, rawBytes.length - 3);
return new String(newBytes, StandardCharsets.US_ASCII);
}
return s;
}
private void decodeRawString(String rawString) {
rawString = removeUTF8BOM(rawString);
// Delimiter
final String DELIMITER = ";";
String[] values = rawString.split(DELIMITER);
if(values.length!=12) throw new IllegalArgumentException("Expecting exactly 12 fields.");
if(!values[0].equals(QR_CODE_VERSION))
throw new IllegalArgumentException("unexpected qr-code-verion: "+values[0]);
qrcode.put(QRCodeFields.qrCodeVersion, values[0]);
qrcode.put(QRCodeFields.kassenSerienNr, values[1]);
qrcode.put(QRCodeFields.processType, values[2]);
qrcode.put(QRCodeFields.processData, values[3]);
qrcode.put(QRCodeFields.transaktionsNr, values[4]);
qrcode.put(QRCodeFields.signaturZaehler, values[5]);
qrcode.put(QRCodeFields.startZeit, values[6]);
qrcode.put(QRCodeFields.logTime, values[7]);
if(!supportedSigAlgorithms.containsKey(values[8]))
throw new IllegalArgumentException("unsupported sig-alg: "+values[8]);
qrcode.put(QRCodeFields.sigAlg, values[8]);
if(!(values[9].equals("utcTime")||values[9].equals("generalizedTime")||values[9].equals("unixTime")))
throw new IllegalArgumentException("unknown log-time-format: "+values[9]);
qrcode.put(QRCodeFields.logTimeFormat, values[9]);
qrcode.put(QRCodeFields.signatur, values[10]);
qrcode.put(QRCodeFields.publicKey, values[11]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment