at.asitplus.regkassen.core.DemoCashBox.java Source code

Java tutorial

Introduction

Here is the source code for at.asitplus.regkassen.core.DemoCashBox.java

Source

/*
 * Copyright (C) 2015
 * A-SIT Plus GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package at.asitplus.regkassen.core;

import at.asitplus.regkassen.core.base.cashboxsimulation.CashBoxInstruction;
import at.asitplus.regkassen.core.base.cashboxsimulation.TypeOfReceipt;
import at.asitplus.regkassen.core.base.receiptdata.ReceiptPackage;
import at.asitplus.regkassen.core.base.receiptdata.ReceiptRepresentationForSignature;
import at.asitplus.regkassen.core.base.receiptdata.SimplifiedReceipt;
import at.asitplus.regkassen.core.base.rksuite.RKSuite;
import at.asitplus.regkassen.core.base.util.CashBoxUtils;
import at.asitplus.regkassen.core.base.util.CryptoUtil;
import at.asitplus.regkassen.core.modules.DEP.DEPExportFormat;
import at.asitplus.regkassen.core.modules.init.CashBoxParameters;
import at.asitplus.regkassen.core.modules.print.ReceiptPrintType;
import at.asitplus.regkassen.core.modules.signature.jws.JWSModule;
import org.apache.commons.math3.util.Precision;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.*;
import java.util.Date;
import java.util.List;

/**
 * Simple demonstration CashBox, can be initialized with different modules (signature, DEP, print)
 */
public class DemoCashBox {
    //parameters for cashbox initialisation (AES key, cashbox ID etc.)
    protected CashBoxParameters cashBoxParameters;

    //internal turnover counter... starts with 0
    protected long turnoverCounter = 0;

    public DemoCashBox(CashBoxParameters cashBoxParameters) {
        this.cashBoxParameters = cashBoxParameters;
    }

    /**
     * @return cashboxparameter set, used to setup/initialize the cashbox
     */
    public CashBoxParameters getCashBoxParameters() {
        return cashBoxParameters;
    }

    /**
     * since the 0.6 version of the demo code, this is the only way to run the cashbox,
     *
     * @param cashBoxInstructions list of instructions which tell the cashbox to create the specified receipts
     */
    public void executeSimulation(List<CashBoxInstruction> cashBoxInstructions) {
        for (CashBoxInstruction cashBoxInstruction : cashBoxInstructions) {
            createStoreAndSignReceiptPackage(cashBoxInstruction);
        }
    }

    /**
     * export of the DEP
     * @return DEP Export
     */
    public DEPExportFormat exportDEP() {
        return cashBoxParameters.getDepModul().exportDEP();
    }

    /**
     * get all receipts from DEP
     *
     * @return receipts stored in DEP
     */
    public List<ReceiptPackage> getStoredReceipts() {
        return cashBoxParameters.getDepModul().getStoredReceipts();
    }

    /**
     * print a given receipt
     *
     * @param receiptPackage   receipt data structure
     * @param receiptPrintType type of printed receipt (QR-code, OCR-code)
     * @return receipt as PDF-blob
     */
    public byte[] printReceipt(ReceiptPackage receiptPackage, ReceiptPrintType receiptPrintType) {
        return cashBoxParameters.getPrinterModule().printReceipt(receiptPackage, receiptPrintType);
    }

    public List<byte[]> printReceipt(List<ReceiptPackage> receiptPackageList, ReceiptPrintType receiptPrintType) {
        return cashBoxParameters.getPrinterModule().printReceipt(receiptPackageList, receiptPrintType);
    }

    /**
     * create and store a receipt according to the data in the instruction
     *
     * @param cashBoxInstruction
     */
    protected void createStoreAndSignReceiptPackage(CashBoxInstruction cashBoxInstruction) {
        //get signature device and used RKSUITE
        //as of version 6 the cashbox can be instructed to use a specific signature device
        JWSModule signatureDevice = cashBoxParameters.getJwsSignatureModules()
                .get(cashBoxInstruction.getUsedSignatureDevice());
        RKSuite rkSuiteOfSignatureDevice = signatureDevice.getRKSuite();

        //prepare elements of the machine code representation
        RKSuite el1_rkSuite;
        String el2_cashboxID;
        String el3_receiptID;
        Date el4_timeAndData;
        long el5_taxSet_NORMAL;
        long el6_taxSet_ERMAESSIGT1;
        long el7_taxSet_ERMAESSIGT2;
        long el8_taxSet_NULL;
        long el9_taxSet_BESONDERS;
        String el10_encryptedTurnOverValue;
        String el11_certificateSerialNumberOrCompanyAndKeyID;
        String el12_chainValue = "";

        //setup common values that need to be set for every type of receipt
        el1_rkSuite = signatureDevice.getRKSuite();
        el2_cashboxID = cashBoxParameters.getCashBoxId();
        el3_receiptID = cashBoxInstruction.getReceiptIdentifier();
        el4_timeAndData = new Date();

        //if we have an open system, the serial number of the certificate is used
        //for a closed system, the companyID and the key id of the signing key is used
        el11_certificateSerialNumberOrCompanyAndKeyID = signatureDevice.getSerialNumberOfKeyID();

        //extract type of receipt that needs to be generated
        TypeOfReceipt typeOfReceipt = cashBoxInstruction.getTypeOfReceipt();

        //get basic receipt data (tax sets)
        SimplifiedReceipt simplifiedReceipt = cashBoxInstruction.getSimplifiedReceipt();

        //generate receipt according to its type
        //this demo cash box is highly simplified and does not execute validity checks (e.g. it is not checked whether all tax sets of a START_BELEG are set to 0,00)
        //wrong cashbox instructions might lead to a crash, or a a wrong result
        //all checks are carried out by the validation tool
        //reason: this code as been strongly simplified in version 0.6 to highlight the essential procedures for each type of receipt
        if (typeOfReceipt == TypeOfReceipt.START_BELEG) {
            //in case of a START_BELEG all tax sets are set to 0.0
            el5_taxSet_NORMAL = 0;
            el6_taxSet_ERMAESSIGT1 = 0;
            el7_taxSet_ERMAESSIGT2 = 0;
            el8_taxSet_NULL = 0;
            el9_taxSet_BESONDERS = 0;

            //no need to update the turnover counter

            el10_encryptedTurnOverValue = encryptTurnOverCounter(cashBoxParameters.getCashBoxId(), el3_receiptID,
                    rkSuiteOfSignatureDevice);
            //there is no previous receipt for chain value calculation, the cashboxID is used instead
            el12_chainValue = calculateChainValue(null, rkSuiteOfSignatureDevice);
        } else if (typeOfReceipt == TypeOfReceipt.STANDARD_BELEG) {
            el5_taxSet_NORMAL = simplifiedReceipt.getTaxSetNormal();
            el6_taxSet_ERMAESSIGT1 = simplifiedReceipt.getTaxSetErmaessigt1();
            el7_taxSet_ERMAESSIGT2 = simplifiedReceipt.getTaxSetErmaessigt2();
            el8_taxSet_NULL = simplifiedReceipt.getTaxSetNull();
            el9_taxSet_BESONDERS = simplifiedReceipt.getTaxSetBesonders();

            //update turnover counter by adding all tax sets
            updateTurnOverCounter(simplifiedReceipt);
            el10_encryptedTurnOverValue = encryptTurnOverCounter(cashBoxParameters.getCashBoxId(), el3_receiptID,
                    rkSuiteOfSignatureDevice);
            if (getStoredReceipts().size() > 0) {
                el12_chainValue = calculateChainValue(
                        getStoredReceipts().get(getStoredReceipts().size() - 1).getJwsCompactRepresentation(),
                        rkSuiteOfSignatureDevice);
            }
        } else if (typeOfReceipt == TypeOfReceipt.STORNO_BELEG) {
            el5_taxSet_NORMAL = simplifiedReceipt.getTaxSetNormal();
            el6_taxSet_ERMAESSIGT1 = simplifiedReceipt.getTaxSetErmaessigt1();
            el7_taxSet_ERMAESSIGT2 = simplifiedReceipt.getTaxSetErmaessigt2();
            el8_taxSet_NULL = simplifiedReceipt.getTaxSetNull();
            el9_taxSet_BESONDERS = simplifiedReceipt.getTaxSetBesonders();

            updateTurnOverCounter(simplifiedReceipt);
            el10_encryptedTurnOverValue = CashBoxUtils.base64Encode("STO".getBytes(), false);
            if (getStoredReceipts().size() > 0) {
                el12_chainValue = calculateChainValue(
                        getStoredReceipts().get(getStoredReceipts().size() - 1).getJwsCompactRepresentation(),
                        rkSuiteOfSignatureDevice);
            }
        } else if (typeOfReceipt == TypeOfReceipt.TRAINING_BELEG) {
            el5_taxSet_NORMAL = simplifiedReceipt.getTaxSetNormal();
            el6_taxSet_ERMAESSIGT1 = simplifiedReceipt.getTaxSetErmaessigt1();
            el7_taxSet_ERMAESSIGT2 = simplifiedReceipt.getTaxSetErmaessigt2();
            el8_taxSet_NULL = simplifiedReceipt.getTaxSetNull();
            el9_taxSet_BESONDERS = simplifiedReceipt.getTaxSetBesonders();

            //turnover counter is not updated!

            el10_encryptedTurnOverValue = CashBoxUtils.base64Encode("TRA".getBytes(), false);
            if (getStoredReceipts().size() > 0) {
                el12_chainValue = calculateChainValue(
                        getStoredReceipts().get(getStoredReceipts().size() - 1).getJwsCompactRepresentation(),
                        rkSuiteOfSignatureDevice);
            }
        } else if (typeOfReceipt == TypeOfReceipt.NULL_BELEG) {
            //no need to update the turnover counter
            el5_taxSet_NORMAL = 0;
            el6_taxSet_ERMAESSIGT1 = 0;
            el7_taxSet_ERMAESSIGT2 = 0;
            el8_taxSet_NULL = 0;
            el9_taxSet_BESONDERS = 0;

            //turnover counter is not updated, since all tax-sets of a Nullbeleg are set to 0

            el10_encryptedTurnOverValue = encryptTurnOverCounter(cashBoxParameters.getCashBoxId(), el3_receiptID,
                    rkSuiteOfSignatureDevice);
            if (getStoredReceipts().size() > 0) {
                el12_chainValue = calculateChainValue(
                        getStoredReceipts().get(getStoredReceipts().size() - 1).getJwsCompactRepresentation(),
                        rkSuiteOfSignatureDevice);
            }
        } else {
            return;
        }

        //create data structure that contains the data that needs to be signed
        ReceiptRepresentationForSignature receiptRepresentationForSignature = new ReceiptRepresentationForSignature();
        receiptRepresentationForSignature.setCashBoxID(el2_cashboxID);
        receiptRepresentationForSignature.setReceiptIdentifier(el3_receiptID);
        receiptRepresentationForSignature.setReceiptDateAndTime(el4_timeAndData);
        receiptRepresentationForSignature.setSumTaxSetNormal(el5_taxSet_NORMAL);
        receiptRepresentationForSignature.setSumTaxSetErmaessigt1(el6_taxSet_ERMAESSIGT1);
        receiptRepresentationForSignature.setSumTaxSetErmaessigt2(el7_taxSet_ERMAESSIGT2);
        receiptRepresentationForSignature.setSumTaxSetNull(el8_taxSet_NULL);
        receiptRepresentationForSignature.setSumTaxSetBesonders(el9_taxSet_BESONDERS);
        receiptRepresentationForSignature.setEncryptedTurnoverValue(el10_encryptedTurnOverValue);
        receiptRepresentationForSignature
                .setSignatureCertificateSerialNumber(el11_certificateSerialNumberOrCompanyAndKeyID);
        receiptRepresentationForSignature.setSignatureValuePreviousReceipt(el12_chainValue);

        //prepare signature data
        String plainData = receiptRepresentationForSignature.getDataToBeSigned(el1_rkSuite);

        //sign receipt
        String signedJWSCompactRep = signatureDevice.signMachineCodeRepOfReceipt(plainData,
                cashBoxInstruction.isSignatureDeviceDamaged());

        //store receipt to DEP
        ReceiptPackage receiptPackage = new ReceiptPackage();
        receiptPackage.setCertificateChain(signatureDevice.getSignatureModule().getCertificateChain());
        receiptPackage.setSigningCertificate(signatureDevice.getSignatureModule().getSigningCertificate());
        receiptPackage.setJwsCompactRepresentation(signedJWSCompactRep);
        cashBoxParameters.getDepModul().storeReceipt(receiptPackage);
    }

    /**
     * update turnover counter
     *
     * @param SimplifiedReceipt
     */
    protected void updateTurnOverCounter(SimplifiedReceipt SimplifiedReceipt) {
        //if we have a receipt for training purposes, we don't change the turnover counter
        double sumTaxTypeNormal = Precision.round(SimplifiedReceipt.getTaxSetNormal(), 2);
        double sumTaxTypeErmaessigt1 = Precision.round(SimplifiedReceipt.getTaxSetErmaessigt1(), 2);
        double sumTaxTypeErmaessigt2 = Precision.round(SimplifiedReceipt.getTaxSetErmaessigt2(), 2);
        double sumTaxTypeNull = Precision.round(SimplifiedReceipt.getTaxSetNull(), 2);
        double sumTaxTypeBesonders = Precision.round(SimplifiedReceipt.getTaxSetBesonders(), 2);

        //add all taxset sums
        double tempSum = 0.0;
        tempSum += sumTaxTypeNormal;
        tempSum += sumTaxTypeErmaessigt1;
        tempSum += sumTaxTypeErmaessigt2;
        tempSum += sumTaxTypeNull;
        tempSum += sumTaxTypeBesonders;

        //convert sum to -cent and add to turnover counter
        turnoverCounter += (tempSum * 100);
    }

    /**
     * encrypt the current turnover counter
     *
     * @param cashBoxIDUTF8String
     * @param receiptIdentifierUTF8String
     * @param rkSuite
     * @return
     */
    protected String encryptTurnOverCounter(String cashBoxIDUTF8String, String receiptIdentifierUTF8String,
            RKSuite rkSuite) {
        try {
            //encrypt turnover counter and store the encrypted value in the data-to-be-signed package

            //prepare IV for encryption process, the Initialisation Vector (IV) is calculating by concatenating and then
            //hashing the
            //receipt-identifier (Belegnummer) and
            //the cashbox-ID (Kassen-ID)

            //Get UTF-8 String representation of cashBox-ID (Kassen-ID), STRING in Java are already UTF-8 encoded, thus no
            //encoding transformation is done here
            //IMPORTANT HINT: NEVER EVER use the same "Kassen-ID" and "Belegnummer" for different receipts!!!!
            String IVUTF8StringRepresentation = cashBoxIDUTF8String + receiptIdentifierUTF8String;

            ///hash the String with the hash-algorithm defined in the cashbox-algorithm-suite
            MessageDigest messageDigest = MessageDigest
                    .getInstance(rkSuite.getHashAlgorithmForPreviousSignatureValue());
            byte[] hashValue = messageDigest.digest(IVUTF8StringRepresentation.getBytes());
            byte[] concatenatedHashValue = new byte[16];
            System.arraycopy(hashValue, 0, concatenatedHashValue, 0, 16);

            //encrypt the turnover counter using the AES key
            //Note: 3 AES encryption methods are provided for demonstration purposes,
            //which all use a different mode of operation (CTR, CFB, or ECB).
            //All three methods provided yield the same result. Still, they are provided here to
            //demonstrate the use of different modes of operation for encryption. This can be useful,
            //if AES functionality is re-implemented in another programming language that does
            //support selected AES modes of operation only. Please refer to
            //https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation for more details
            //on different modes of operation for block ciphers
            String base64EncryptedTurnOverValue1 = null;

            base64EncryptedTurnOverValue1 = CryptoUtil.encryptCTR(concatenatedHashValue, turnoverCounter,
                    cashBoxParameters.getTurnOverCounterAESKey());

            String base64EncryptedTurnOverValue2 = CryptoUtil.encryptCFB(concatenatedHashValue, turnoverCounter,
                    cashBoxParameters.getTurnOverCounterAESKey());
            String base64EncryptedTurnOverValue3 = CryptoUtil.encryptECB(concatenatedHashValue, turnoverCounter,
                    cashBoxParameters.getTurnOverCounterAESKey());
            if (!base64EncryptedTurnOverValue1.equals(base64EncryptedTurnOverValue2)) {
                System.out.println("ENCRYPTION ERROR IN METHOD updateTurnOverCounter, MUST NOT HAPPEN");
                System.exit(-1);
            }
            if (!base64EncryptedTurnOverValue1.equals(base64EncryptedTurnOverValue3)) {
                System.out.println("ENCRYPTION ERROR IN METHOD updateTurnOverCounter, MUST NOT HAPPEN");
                System.exit(-1);
            }

            //THE FOLLOWING CODE IS ONLY FOR DEMONSTRATION PURPOSES
            //decryption and reconstruction of the turnover value
            //this is just here for demonstration purposes (so that the whole encryption/decryption process can be found in one place)
            //and not needed for that function
            //IV needs to be setup the same way as above
            //encryptedTurnOverValue needs to be reconstructed as described in the used utility method
            //Note: 3 AES decryption methods are provided for demonstration purposes,
            //which all use a different mode of operation (CTR, CFB, or ECB).
            //All three methods provided yield the same result. Still, they are provided here to
            //demonstrate the use of different modes of operation for decryption. This can be useful, if
            //AES functionality is re-implemented in another programming language that does
            //support selected AES modes of operation only. Please refer to
            //https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation for more details
            //on different modes of operation for block ciphers
            long testPlainOverTurnOverReconstructed1 = CryptoUtil.decryptCTR(concatenatedHashValue,
                    base64EncryptedTurnOverValue1, cashBoxParameters.getTurnOverCounterAESKey());
            long testPlainOverTurnOverReconstructed2 = CryptoUtil.decryptCFB(concatenatedHashValue,
                    base64EncryptedTurnOverValue2, cashBoxParameters.getTurnOverCounterAESKey());
            long testPlainOverTurnOverReconstructed3 = CryptoUtil.decryptECB(concatenatedHashValue,
                    base64EncryptedTurnOverValue3, cashBoxParameters.getTurnOverCounterAESKey());
            if (testPlainOverTurnOverReconstructed1 != testPlainOverTurnOverReconstructed2) {
                System.out.println("DECRYPTION ERROR IN METHOD updateTurnOverCounter, MUST NOT HAPPEN");
                System.exit(-1);
            }

            if (testPlainOverTurnOverReconstructed1 != testPlainOverTurnOverReconstructed3) {
                System.out.println("DECRYPTION ERROR IN METHOD updateTurnOverCounter, MUST NOT HAPPEN");
                System.exit(-1);
            }

            if (turnoverCounter != testPlainOverTurnOverReconstructed1) {
                System.out.println("DECRYPTION ERROR IN METHOD updateTurnOverCounter, MUST NOT HAPPEN");
                System.exit(-1);
            }

            return base64EncryptedTurnOverValue1;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchProviderException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * calculate cryptographic chain value
     *
     * @param previousReceiptJWSRepresentation previous receipt for chain value calculation, if null, the cashboxID is used
     * @param rkSuite                          rksuite that contains information of the to-be-used HASH-algorithm
     * @return
     */
    protected String calculateChainValue(String previousReceiptJWSRepresentation, RKSuite rkSuite) {
        try {
            String inputForChainCalculation;

            //Detailspezifikation Abs 4 "Sig-Voriger-Beleg"
            //if the first receipt is stored, then the cashbox-identifier is hashed and is used as chaining value
            //otherwise the complete last receipt is hased and the result is used as chaining value
            if (previousReceiptJWSRepresentation == null) {
                inputForChainCalculation = cashBoxParameters.getCashBoxId();
            } else {
                inputForChainCalculation = previousReceiptJWSRepresentation;
            }

            //set hash algorithm from RK suite, in this case SHA-256
            MessageDigest md = MessageDigest.getInstance(rkSuite.getHashAlgorithmForPreviousSignatureValue());

            //calculate hash value
            md.update(inputForChainCalculation.getBytes());
            byte[] digest = md.digest();

            //extract number of bytes (N, defined in RKsuite) from hash value
            int bytesToExtract = rkSuite.getNumberOfBytesExtractedFromPrevSigHash();
            byte[] conDigest = new byte[bytesToExtract];
            System.arraycopy(digest, 0, conDigest, 0, bytesToExtract);

            //encode value as BASE64 String ==> chainValue
            return CashBoxUtils.base64Encode(conDigest, false);

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }
}