Created
September 14, 2020 06:34
-
-
Save tsenger/3d6e264e75f179cbecd66b690c2ab1d1 to your computer and use it in GitHub Desktop.
Decoder für Kassenbon QR-Codes nach DSFinV-K (www.bzst.de/DE/Unternehmen/Aussenpruefungen/DigitaleSchnittstelleFinV/digitaleschnittstellefinv_node.html)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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