From 774ee378554f8bcdc4dcf665633841a25f96bf34 Mon Sep 17 00:00:00 2001 From: Peter Teufl Date: Tue, 6 Sep 2016 14:32:44 +0200 Subject: [PATCH] added regkassen-common source code --- regkassen-common/pom.xml | 95 +++ .../regkassen/common/MachineCodeValue.java | 51 ++ .../at/asitplus/regkassen/common/RKSuite.java | 74 +++ .../regkassen/common/RKSuiteIdentifier.java | 94 +++ .../regkassen/common/SignatureDeviceType.java | 23 + .../regkassen/common/SignatureType.java | 33 + .../asitplus/regkassen/common/SystemType.java | 25 + .../at/asitplus/regkassen/common/TaxType.java | 39 ++ .../regkassen/common/TurnoverCounterType.java | 52 ++ .../regkassen/common/TypeOfReceipt.java | 29 + .../regkassen/common/util/CashBoxUtils.java | 488 +++++++++++++++ .../regkassen/common/util/CryptoUtil.java | 567 ++++++++++++++++++ .../regkassen/common/util/CryptoUtilTest.java | 75 +++ 13 files changed, 1645 insertions(+) create mode 100644 regkassen-common/pom.xml create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/MachineCodeValue.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/RKSuite.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/RKSuiteIdentifier.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/SignatureDeviceType.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/SignatureType.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/SystemType.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/TaxType.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/TurnoverCounterType.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/TypeOfReceipt.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/util/CashBoxUtils.java create mode 100644 regkassen-common/src/main/java/at/asitplus/regkassen/common/util/CryptoUtil.java create mode 100644 regkassen-common/src/test/java/at/asitplus/regkassen/common/util/CryptoUtilTest.java diff --git a/regkassen-common/pom.xml b/regkassen-common/pom.xml new file mode 100644 index 0000000..44cd1b1 --- /dev/null +++ b/regkassen-common/pom.xml @@ -0,0 +1,95 @@ + + + + 4.0.0 + + at.asitplus.regkassen + regkassen-common + 0.10 + jar + + + 2.7.2 + + + + + + org.bouncycastle + bcprov-jdk15on + 1.52 + + + org.bouncycastle + bcpkix-jdk15on + 1.52 + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + 2.7.3 + + + + + + + commons-io + commons-io + 2.4 + + + + + + org.apache.commons + commons-math3 + 3.4.1 + + + + + commons-codec + commons-codec + 1.10 + + + + + org.testng + testng + 6.8.21 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.6 + 1.6 + + + + + + + diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/MachineCodeValue.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/MachineCodeValue.java new file mode 100644 index 0000000..75b4646 --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/MachineCodeValue.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common; + +/** + * contains all types of values which are stored in the + * QR-machine-code-representation + */ +public enum MachineCodeValue { + RK_SUITE(0), // Registrierkassenalgorithmuskennzeichen + CASHBOX_ID(1), // Kassen-ID + RECEIPT_IDENTIFIER(2), // Belegnummer + RECEIPT_DATE_AND_TIME(3), // Beleg-Datum-Uhrzeit + SUM_TAX_SET_NORMAL(4), // Betrag-Satz-Normal + SUM_TAX_SET_ERMAESSIGT1(5), // Betrag-Satz-Ermaessigt-1 + SUM_TAX_SET_ERMAESSIGT2(6), // Betrag-Satz-Ermaessigt-2 + SUM_TAX_SET_NULL(7), // Betrag-Satz-Null + SUM_TAX_SET_BESONDERS(8), // Betrag-Satz-Besonders + ENCRYPTED_TURN_OVER_VALUE(9), // Stand-Umsatz-Zaehler-AES256-ICM + CERTIFICATE_SERIAL_NUMBER_OR_COMPANYID_AND_KEYID(10), // Zertifikat-Seriennummer + // (or Ordnungszahl + // Unternehmen plus + // KEY-ID) + CHAINING_VALUE_PREVIOUS_RECEIPT(11), // Sig-Voriger-Beleg + SIGNATURE_VALUE(12); // Signatur + + protected int index; + + public int getIndex() { + return index; + } + + MachineCodeValue(int index) { + this.index = index; + } +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/RKSuite.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/RKSuite.java new file mode 100644 index 0000000..c8acbeb --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/RKSuite.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common; + +public enum RKSuite { + // RK Suite defined in Detailspezifikation/ABS 2 + // suite for a closed system (closed systems are identified by the ZDA-ID + // AT0) + R1_AT0("1", "AT0", "ES256", "SHA-256", 8), + R1_AT1("1", "AT1", "ES256", "SHA-256", 8), + R1_AT2("1", "AT2", "ES256", "SHA-256", 8), + R1_AT3("1", "AT3", "ES256", "SHA-256", 8), + R1_AT4("1", "AT4", "ES256", "SHA-256", 8), + R1_AT5("1", "AT5", "ES256", "SHA-256", 8), + R1_AT6("1", "AT6", "ES256", "SHA-256", 8), + R1_AT7("1", "AT7", "ES256", "SHA-256", 8), + R1_AT8("1", "AT8", "ES256", "SHA-256", 8), + R1_AT9("1", "AT9", "ES256", "SHA-256", 8), + R1_AT10("1", "AT10", "ES256", "SHA-256", 8), + + // suite for an open system (in this case with the virtual ZDA identified by + // AT100) + R1_AT100("1", "AT100", "ES256", "SHA-256", 8); + + protected String suiteID; + protected String zdaID; + protected String jwsSignatureAlgorithm; + protected String hashAlgorithmForPreviousSignatureValue; + protected int numberOfBytesExtractedFromPrevSigHash; + + RKSuite(String suiteID, String zdaID, String jwsSignatureAlgorithm, + String hashAlgorithmForPreviousSignatureValue, int numberOfBytesExtractedFromPrevSigHash) { + this.suiteID = suiteID; + this.zdaID = zdaID; + this.jwsSignatureAlgorithm = jwsSignatureAlgorithm; + this.hashAlgorithmForPreviousSignatureValue = hashAlgorithmForPreviousSignatureValue; + this.numberOfBytesExtractedFromPrevSigHash = numberOfBytesExtractedFromPrevSigHash; + } + + public String getSuiteID() { + return "R" + suiteID + "-" + zdaID; + } + + public String getZdaID() { + return zdaID; + } + + public String getJwsSignatureAlgorithm() { + return jwsSignatureAlgorithm; + } + + public String getHashAlgorithmForPreviousSignatureValue() { + return hashAlgorithmForPreviousSignatureValue; + } + + public int getNumberOfBytesExtractedFromPrevSigHash() { + return numberOfBytesExtractedFromPrevSigHash; + } +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/RKSuiteIdentifier.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/RKSuiteIdentifier.java new file mode 100644 index 0000000..e49dd14 --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/RKSuiteIdentifier.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common; + +public enum RKSuiteIdentifier { + // RK Suite defined in Detailspezifikation/ABS 2 + R1("1", "ES256", "SHA-256", "SHA256withECDSA", "ECDSA", 8); + + public static final String[] SUPPORTED_PREFIXES = { + "R1-AT" }; + + protected final String suiteID; + protected final String jwsSignatureAlgorithm; + protected final String javaSignatureAlgorithm; + protected final String javaPublicKeySpec; + + protected final String hashAlgorithmForPreviousSignatureValue; + protected final int numberOfBytesExtractedFromPrevSigHash; + + RKSuiteIdentifier(String suiteID, String jwsSignatureAlgorithm, + String hashAlgorithmForPreviousSignatureValue, String javaSignatureAlgorithm, String javaPublicKeySpec, + int numberOfBytesExtractedFromPrevSigHash) { + this.suiteID = suiteID; + this.jwsSignatureAlgorithm = jwsSignatureAlgorithm; + this.hashAlgorithmForPreviousSignatureValue = hashAlgorithmForPreviousSignatureValue; + this.numberOfBytesExtractedFromPrevSigHash = numberOfBytesExtractedFromPrevSigHash; + this.javaSignatureAlgorithm = javaSignatureAlgorithm; + this.javaPublicKeySpec = javaPublicKeySpec; + } + + public String getJavaSignatureAlgorithm() { + return javaSignatureAlgorithm; + } + + public String getJavaPublicKeySpec() { + return javaPublicKeySpec; + } + + public String getSuiteID() { + return "R" + suiteID; + } + + public String getJwsSignatureAlgorithm() { + return jwsSignatureAlgorithm; + } + + public String getHashAlgorithmForPreviousSignatureValue() { + return hashAlgorithmForPreviousSignatureValue; + } + + public int getNumberOfBytesExtractedFromPrevSigHash() { + return numberOfBytesExtractedFromPrevSigHash; + } + + public static boolean isSupported(String rk) { + return fromRKString(rk) != null; + } + + public static RKSuiteIdentifier fromRKString(String rk) { + if (rk == null || !prefixSupported(rk)) + return null; + try { + String suiteID = rk.split("-")[0]; + return RKSuiteIdentifier.valueOf(suiteID); + } catch (Exception e) { + return null; + } + } + + private static boolean prefixSupported(String mc) { + if (mc == null) + return false; + for (String p : SUPPORTED_PREFIXES) { + if (mc.startsWith(p)) + return true; + } + return false; + } +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/SignatureDeviceType.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/SignatureDeviceType.java new file mode 100644 index 0000000..a169dd3 --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/SignatureDeviceType.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common; + +public enum SignatureDeviceType { + CERTIFICATE, + PUBLIC_KEY; +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/SignatureType.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/SignatureType.java new file mode 100644 index 0000000..dfb9254 --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/SignatureType.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common; + +public enum SignatureType { + OFFLINE("Beim Erstellen des Belegs war die Sicherheitseinrichtung nicht funktionsfähig."), + NORMAL("Formell valide Signatur"); + + private final String descrption; + + private SignatureType(String description) { + this.descrption = description; + } + + public String getDescrption() { + return descrption; + } +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/SystemType.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/SystemType.java new file mode 100644 index 0000000..c6f10b6 --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/SystemType.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common; + +public enum SystemType { + OPEN, + CLOSED, + + ; +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/TaxType.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/TaxType.java new file mode 100644 index 0000000..4509cea --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/TaxType.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common; + +/** + * Tax-type (Steuersatz) "Betrag-Satz-X" according to Detailspezifikation Abs 4 + */ +public enum TaxType { + SATZ_NORMAL("Satz-Normal"), + SATZ_ERMAESSIGT_1("Satz-Ermaessigt-1"), + SATZ_ERMAESSIGT_2("Satz-Ermaessigt-2"), + SATZ_NULL("Satz-Null"), + SATZ_BESONDERS("Satz-Besonders"); + + protected String taxTypeString; + + TaxType(String taxTypeString) { + this.taxTypeString = taxTypeString; + } + + public String getTaxTypeString() { + return taxTypeString; + } +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/TurnoverCounterType.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/TurnoverCounterType.java new file mode 100644 index 0000000..b9e4591 --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/TurnoverCounterType.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common; + +/** + * enum representing the turnovercounter-type in a receipt, could be + * normal (meaning that encrypted turnover counter is stored in receipt) + * TRA (indicating training receipt) + * STO (indicating "Storno" receipt) + */ +public enum TurnoverCounterType { + NORMAL("", ""), + TRA("VFJB", "TRA"), + STO("U1RP", "STO"); + + private final String encodedValue, decodedValue; + + private TurnoverCounterType(String encodedValue, String decodedValue) { + this.encodedValue = encodedValue; + this.decodedValue = decodedValue; + } + + public static TurnoverCounterType toTurnoverCunterType(String encodedValue) { + for (TurnoverCounterType type : TurnoverCounterType.values()) + if (type.encodedValue.equals(encodedValue)) + return type; + return NORMAL; + } + + public String getEncodedValue() { + return encodedValue; + } + + public String getDecodedValue() { + return decodedValue; + } +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/TypeOfReceipt.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/TypeOfReceipt.java new file mode 100644 index 0000000..38fa0ba --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/TypeOfReceipt.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common; + +/** + * possible receipt types + */ +public enum TypeOfReceipt { + START_BELEG, + STANDARD_BELEG, + STORNO_BELEG, + TRAINING_BELEG, + NULL_BELEG; +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/util/CashBoxUtils.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/util/CashBoxUtils.java new file mode 100644 index 0000000..71bea4f --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/util/CashBoxUtils.java @@ -0,0 +1,488 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common.util; + +import at.asitplus.regkassen.common.MachineCodeValue; +import org.apache.commons.codec.binary.Base32; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.Security; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class CashBoxUtils { + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * Method that converts a Java date to an ISO 8601 string + * + * @param date + * Java date to be converted to an ISO 8601 string + * @return the date converted to an ISO 8601 string + */ + public static String convertDateToISO8601(Date date) { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + return dateFormat.format(date); + } + + /** + * Method that converts a an ISO 8601 string to Java date + * + * @param dateString date as ISO 8601 string + * Java date to be converted to an ISO 8601 string + * @return converted + */ + public static Date convertISO8601toDate(String dateString) throws ParseException { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + Date date = dateFormat.parse(dateString); + return date; + } + + /** + * Helper method for storing printed PDF receipts to files + * + * @param printedReceipts + * binary representation of receipts to be stored + * @param prefix + * prefix for file names + * @param baseDir + * base directory, where files should be written + */ + public static void writeReceiptsToFiles(List printedReceipts, String prefix, File baseDir) { + try { + int index = 1; + for (byte[] printedReceipt : printedReceipts) { + ByteArrayInputStream bIn = new ByteArrayInputStream(printedReceipt); + File receiptFile = new File(baseDir, prefix + "Receipt " + index + ".pdf"); + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream( + new FileOutputStream(receiptFile)); + IOUtils.copy(bIn, bufferedOutputStream); + bufferedOutputStream.close(); + index++; + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * BASE64 encoding helper function + * + * @param data + * binary representation of data to be encoded + * @param isUrlSafe + * indicates whether BASE64 URL-safe encoding should be used + * (required for JWS) + * @return BASE64 encoded representation of input data + */ + public static String base64Encode(byte[] data, boolean isUrlSafe) { + Base64 encoder = new Base64(isUrlSafe); + return new String(encoder.encode(data)).replace("\r\n", ""); + } + + /** + * BASE64 decoder helper function + * + * @param base64Data + * BASE64 encoded data + * @param isUrlSafe + * indicates whether BASE64 URL-safe encoding was used (required for + * JWS) + * @return binary representation of decoded data + */ + public static byte[] base64Decode(String base64Data, boolean isUrlSafe) { + Base64 decoder = new Base64(isUrlSafe); + return decoder.decode(base64Data); + } + + /** + * BASE32 encoding helper (required for OCR representation) + * + * @param data + * binary representation of data to be encoded + * @return BASE32 encoded representation of input data + */ + public static String base32Encode(byte[] data) { + Base32 encoder = new Base32(); + return new String(encoder.encode(data)).replace("\r\n", ""); + } + + /** + * BASE32 decoding helper (required for OCR representation) + * + * @param base32Data + * BASE32 encoded data + * @return binary representation of decoded data + */ + public static byte[] base32Decode(String base32Data) { + Base32 decoder = new Base32(); + return decoder.decode(base32Data); + } + + /** + * get a value from the machine code representation + * + * @param machineCodeRepresentation + * machinecode representation (QR or OCR code) + * @param machineCodeValue + * which value? e.g. signature value, rk-suite etc. + * @return the extracted value as String + */ + public static String getValueFromMachineCode(String machineCodeRepresentation, + MachineCodeValue machineCodeValue) { + // plus 1 due to leading "_" + return machineCodeRepresentation.split("_")[machineCodeValue.getIndex() + 1]; + } + + /** + * convert JWS compact representation to QR-machine-code representation of + * signed receipt + * + * @param jwsCompactRepresentationOfReceipt + * JWS compact representation of signed receipt + * @return the QR-machine-code-representation of signed receipt + */ + public static String getQRCodeRepresentationFromJWSCompactRepresentation( + String jwsCompactRepresentationOfReceipt) { + // get data + String jwsPayloadEncoded = jwsCompactRepresentationOfReceipt.split("\\.")[1]; + String jwsSignatureEncoded = jwsCompactRepresentationOfReceipt.split("\\.")[2]; + + String payload = new String(CashBoxUtils.base64Decode(jwsPayloadEncoded, true), Charset.forName("UTF-8")); + String signature = CashBoxUtils.base64Encode(CashBoxUtils.base64Decode(jwsSignatureEncoded, true), false); + + return payload + "_" + signature; + } + + /** + * convert JWS compact representation to OCR-machine-code representation of + * signed receipt + * + * @param jwsCompactRepresentationOfReceipt + * JWS compact representation of signed receipt + * @return the OCR-machine-code-representation of signed receipt + */ + public static String getOCRCodeRepresentationFromJWSCompactRepresentation( + String jwsCompactRepresentationOfReceipt) { + // Ref: Detailspezifikation Abs 14 + // could be done more efficiently, but in this way the process of converting + // the QR representation to the OCR representation is highlighted + // get QR-Code representation from JWS compact representation + String qrCodeRepresentation = CashBoxUtils + .getQRCodeRepresentationFromJWSCompactRepresentation(jwsCompactRepresentationOfReceipt); + + // extract all elements + String el1_rkSuite = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.RK_SUITE); + String el2_cashboxID = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.CASHBOX_ID); + String el3_receiptIdentifier = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.RECEIPT_IDENTIFIER); + String el4_timeAndDate = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.RECEIPT_DATE_AND_TIME); + String el5_taxSet_NORMAL = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.SUM_TAX_SET_NORMAL); + String el6_taxSet_ERMAESSIGT1 = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.SUM_TAX_SET_ERMAESSIGT1); + String el7_taxSet_ERMAESSIGT2 = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.SUM_TAX_SET_ERMAESSIGT2); + String el8_taxSet_NULL = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.SUM_TAX_SET_NULL); + String el9_taxSet_BESONDERS = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.SUM_TAX_SET_BESONDERS); + String el10_encryptedTurnOverValue = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.ENCRYPTED_TURN_OVER_VALUE); + String el11_certificateSerialNumberOrCompanyAndKeyID = CashBoxUtils.getValueFromMachineCode( + qrCodeRepresentation, MachineCodeValue.CERTIFICATE_SERIAL_NUMBER_OR_COMPANYID_AND_KEYID); + String el12_chainValue = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.CHAINING_VALUE_PREVIOUS_RECEIPT); + String el13_signatureValue = CashBoxUtils.getValueFromMachineCode(qrCodeRepresentation, + MachineCodeValue.SIGNATURE_VALUE); + + // re-encode the following values from BASE64 to BASE32 + el10_encryptedTurnOverValue = CashBoxUtils + .base32Encode(CashBoxUtils.base64Decode(el10_encryptedTurnOverValue, false)); + el12_chainValue = CashBoxUtils.base32Encode(CashBoxUtils.base64Decode(el12_chainValue, false)); + el13_signatureValue = CashBoxUtils.base32Encode(CashBoxUtils.base64Decode(el13_signatureValue, false)); + + // combine all values to OCR representation + String ocrCodeRepresentation = "_" + el1_rkSuite + "_" + el2_cashboxID + "_" + el3_receiptIdentifier + "_" + + el4_timeAndDate + "_" + el5_taxSet_NORMAL + "_" + el6_taxSet_ERMAESSIGT1 + "_" + + el7_taxSet_ERMAESSIGT2 + "_" + el8_taxSet_NULL + "_" + el9_taxSet_BESONDERS + "_" + + el10_encryptedTurnOverValue + "_" + el11_certificateSerialNumberOrCompanyAndKeyID + "_" + + el12_chainValue + "_" + el13_signatureValue; + + return ocrCodeRepresentation; + } + + /** + * extract the payload of the QR-Code (remove signature value) + * + * @param qrCodeRepresentation + * the QR-machine-code-representation of signed receipt + * @return extracted payload of QR-machine-code-representation of signed + * receipt + */ + public static String getPayloadFromQRCodeRepresentation(String qrCodeRepresentation) { + String[] elements = qrCodeRepresentation.split("_"); + String payload = ""; + for (int i = 0; i < 13; i++) { + payload += elements[i]; + if (i < 12) { + payload += "_"; + } + } + return payload; + } + + /** + * convert QR-machine-code representation of signed receipt to JWS compact + * representation + * + * @param qrMachineCodeRepresentation + * the QR-machine-code-representation of signed receipt + * @return JWS compact representation of signed receipt + */ + public static String getJWSCompactRepresentationFromQRMachineCodeRepresentation( + String qrMachineCodeRepresentation) { + String payload = getPayloadFromQRCodeRepresentation(qrMachineCodeRepresentation); + + String jwsPayload = CashBoxUtils.base64Encode(payload.getBytes(Charset.forName("UTF-8")), true); + + String jwsHeader = "eyJhbGciOiJFUzI1NiJ9"; + String jwsSignature = CashBoxUtils.base64Encode(CashBoxUtils.base64Decode( + CashBoxUtils.getValueFromMachineCode(qrMachineCodeRepresentation, MachineCodeValue.SIGNATURE_VALUE), + false), true); + + return jwsHeader + "." + jwsPayload + "." + jwsSignature; + } + + /** + * get double value from arbitrary String represent a double (0,00) or (0.00) + * + * @param taxSetValue + * double value as String + * @return double value + * @throws Exception + */ + public static double getDoubleFromTaxSet(String taxSetValue) throws Exception { + // try format ("0,00") + NumberFormat nf = NumberFormat.getNumberInstance(Locale.GERMAN); + DecimalFormat decimalFormat = (DecimalFormat) nf; + Exception parseException; + try { + return decimalFormat.parse(taxSetValue).doubleValue(); + } catch (ParseException ignored) { + } + // if Austrian/German format fail, try US format (0.00) + nf = NumberFormat.getNumberInstance(Locale.US); + decimalFormat = (DecimalFormat) nf; + try { + return decimalFormat.parse(taxSetValue).doubleValue(); + } catch (ParseException e) { + parseException = e; + } + throw parseException; + } + + /** + * check whether the JWS compact representation of signed receipt contains + * indicator for damaged signature creation device + * + * @param jwsCompactRepresentation + * JWS compact representation of signed receipt + * @return signature device was damaged? + */ + public static boolean checkReceiptForDamagedSigatureCreationDevice(String jwsCompactRepresentation) { + String encodedSignatureValueBase64 = jwsCompactRepresentation.split("\\.")[2]; + String decodedSignatureValue = new String(CashBoxUtils.base64Decode(encodedSignatureValueBase64, true)); + return "Sicherheitseinrichtung ausgefallen".equals(decodedSignatureValue); + } + + /** + * get sum of all tax-set turnover values from QR-machine-code-representation + * of signed receipt + * + * @param qrMachineCodeRepresentation + * QR-machine-code-representation of signed receipt + * @param calcAbsValue + * flag which indicates whether abs(value) should be used, if set, + * this can be used to check whether + * the sum is zero. this is needed for checking the first receipt of + * the DEP or the first receipt after + * recovering from a failed signature creation device. + * @return + * @throws Exception + */ + public static double getTaxSetTurnOverSumFromQRMachineCodeRepresentation(String qrMachineCodeRepresentation, + boolean calcAbsValue) throws Exception { + double currentTaxSetNormal = CashBoxUtils.getDoubleFromTaxSet(CashBoxUtils + .getValueFromMachineCode(qrMachineCodeRepresentation, MachineCodeValue.SUM_TAX_SET_NORMAL)); + double currentTaxSetErmaessigt1 = CashBoxUtils.getDoubleFromTaxSet(CashBoxUtils + .getValueFromMachineCode(qrMachineCodeRepresentation, MachineCodeValue.SUM_TAX_SET_ERMAESSIGT1)); + double currentTaxSetErmaessigt2 = CashBoxUtils.getDoubleFromTaxSet(CashBoxUtils + .getValueFromMachineCode(qrMachineCodeRepresentation, MachineCodeValue.SUM_TAX_SET_ERMAESSIGT2)); + double currentTaxSetNull = CashBoxUtils.getDoubleFromTaxSet( + CashBoxUtils.getValueFromMachineCode(qrMachineCodeRepresentation, MachineCodeValue.SUM_TAX_SET_NULL)); + double currentTaxSetBesonders = CashBoxUtils.getDoubleFromTaxSet(CashBoxUtils + .getValueFromMachineCode(qrMachineCodeRepresentation, MachineCodeValue.SUM_TAX_SET_BESONDERS)); + + if (calcAbsValue) { + return Math.abs(currentTaxSetNormal) + Math.abs(currentTaxSetErmaessigt1) + + Math.abs(currentTaxSetErmaessigt2) + Math.abs(currentTaxSetNull) + + Math.abs(currentTaxSetBesonders); + } else { + return currentTaxSetNormal + currentTaxSetErmaessigt1 + currentTaxSetErmaessigt2 + currentTaxSetNull + + currentTaxSetBesonders; + } + } + + /** + * extract certificates from DEP Export Format String representation + * + * @param base64EncodedCertificate + * BASE64 encoded DER-encoded-certificate + * @return java object for X509Certificate + * @throws CertificateException + */ + public static X509Certificate parseCertificate(String base64EncodedCertificate) + throws CertificateException { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream bIn = new ByteArrayInputStream( + CashBoxUtils.base64Decode(base64EncodedCertificate, false)); + return (X509Certificate) certificateFactory.generateCertificate(bIn); + } + + /** + * extract certificates from DEP Export Format String representation + * + * @param base64EncodedCertificates + * BASE64 encoded DER-encoded-certificates + * @return java objects for X509Certificate + * @throws CertificateException + */ + public static List parseCertificates(String[] base64EncodedCertificates) + throws CertificateException { + List certificates = new ArrayList(); + for (String base64EncodedCertificate : base64EncodedCertificates) { + certificates.add(parseCertificate(base64EncodedCertificate)); + } + return certificates; + } + + /** + * determine whether current receipt is "Trainingsbuchung", check via + * encrypted turnover value, (BASE64 encoding of TRA) + * + * @param jwsCompactRepresentation + * JWS compact representation of signed receipt + * @return is "Trainingsbuchung"? + */ + public static boolean isJWSCompactRepTrainingReceipt(String jwsCompactRepresentation) { + return isQRCodeRepTrainingReceipt( + CashBoxUtils.getQRCodeRepresentationFromJWSCompactRepresentation(jwsCompactRepresentation)); + } + + /** + * see above, same method, here: for machine code rep + * + * @param qrMachineCodeRepresentation + * @return + */ + public static boolean isQRCodeRepTrainingReceipt(String qrMachineCodeRepresentation) { + String encryptedTurnOverCounter = CashBoxUtils.getValueFromMachineCode(qrMachineCodeRepresentation, + MachineCodeValue.ENCRYPTED_TURN_OVER_VALUE); + String decodedTurnOverCounter = new String(CashBoxUtils.base64Decode(encryptedTurnOverCounter, false)); + return "TRA".equals(decodedTurnOverCounter); + } + + /** + * determine wheter current receipt is "Stornobuchung", check via encrypted + * turnover value, (BASE64 encoding of STO) + * + * @param jwsCompactRepresentation + * JWS compact representation of signed receipt + * @return is "Stornobuchung"? + */ + public static boolean isJWSCompactRepStornoReceipt(String jwsCompactRepresentation) { + return isQRCodeRepStornoReceipt( + CashBoxUtils.getQRCodeRepresentationFromJWSCompactRepresentation(jwsCompactRepresentation)); + } + + /* + * see previous method + */ + public static boolean isQRCodeRepStornoReceipt(String qrMachineCodeRepresentation) { + String encryptedTurnOverCounter = CashBoxUtils.getValueFromMachineCode(qrMachineCodeRepresentation, + MachineCodeValue.ENCRYPTED_TURN_OVER_VALUE); + String decodedTurnOverCounter = new String(CashBoxUtils.base64Decode(encryptedTurnOverCounter, false)); + return "STO".equals(decodedTurnOverCounter); + } + + /** + * get two's-complement representation for given long value, result is encoded into byte-array of the given + * length + * @param value long value to be encoded + * @param numberOfBytesFor2ComplementRepresentation length of resulting byte-array + * @return byte array of turnover counter, in two's-complement representation + */ + public static byte[] get2ComplementRepForLong(long value,int numberOfBytesFor2ComplementRepresentation) { + if (numberOfBytesFor2ComplementRepresentation<1 || (numberOfBytesFor2ComplementRepresentation>8)) { + throw new IllegalArgumentException(); + } + + //create byte buffer, max length 8 bytes (equal to long representation) + ByteBuffer byteBuffer = ByteBuffer.allocate(8); + byteBuffer.putLong(value); + byte[] longRep = byteBuffer.array(); + + //if given length for encoding is equal to 8, we are done + if (numberOfBytesFor2ComplementRepresentation==8) { + return longRep; + } + + //if given length of encoding is less than 8 bytes, we truncate the representation (of course one needs to be sure + //that the given long value is not larger than the created byte array + byte[] byteRep = new byte[numberOfBytesFor2ComplementRepresentation]; + + //truncating the 8-bytes long representation + System.arraycopy(longRep,8-numberOfBytesFor2ComplementRepresentation,byteRep,0,numberOfBytesFor2ComplementRepresentation); + return byteRep; + } +} diff --git a/regkassen-common/src/main/java/at/asitplus/regkassen/common/util/CryptoUtil.java b/regkassen-common/src/main/java/at/asitplus/regkassen/common/util/CryptoUtil.java new file mode 100644 index 0000000..f9f4b1a --- /dev/null +++ b/regkassen-common/src/main/java/at/asitplus/regkassen/common/util/CryptoUtil.java @@ -0,0 +1,567 @@ +/* + * Copyright (C) 2015, 2016 + * 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.common.util; + +import at.asitplus.regkassen.common.RKSuiteIdentifier; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequenceGenerator; +import org.bouncycastle.asn1.x9.X9IntegerConverter; +import org.bouncycastle.crypto.CryptoException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; + +import static at.asitplus.regkassen.common.util.CashBoxUtils.get2ComplementRepForLong; + +/** + * Util class for AES encryption and decryption with different modes of + * operation + */ +public class CryptoUtil { + + /** + * Helper method to convert DER-encoded signature values (e.g. used by Java) + * to concatenated signature values + * (as used by the JWS-standard) + * + * @param derEncodedSignatureValue + * DER-encoded signature value + * @return concatenated signature value (as used by JWS standard) + * @throws IOException + */ + public static byte[] convertDEREncodedSignatureToJWSConcatenated(final byte[] derEncodedSignatureValue) + throws IOException { + final ASN1InputStream asn1InputStream = new ASN1InputStream(derEncodedSignatureValue); + final ASN1Primitive asn1Primitive = asn1InputStream.readObject(); + asn1InputStream.close(); + final ASN1Sequence asn1Sequence = (ASN1Sequence.getInstance(asn1Primitive)); + final ASN1Integer rASN1 = (ASN1Integer) asn1Sequence.getObjectAt(0); + final ASN1Integer sASN1 = (ASN1Integer) asn1Sequence.getObjectAt(1); + final X9IntegerConverter x9IntegerConverter = new X9IntegerConverter(); + final byte[] r = x9IntegerConverter.integerToBytes(rASN1.getValue(), 32); + final byte[] s = x9IntegerConverter.integerToBytes(sASN1.getValue(), 32); + + final byte[] concatenatedSignatureValue = new byte[64]; + System.arraycopy(r, 0, concatenatedSignatureValue, 0, 32); + System.arraycopy(s, 0, concatenatedSignatureValue, 32, 32); + + return concatenatedSignatureValue; + } + + /** + * Helper method to convert concatenated signature values (as used by the JWS-standard) to + * DER-encoded signature values (e.g. used by Java) + * + * @param concatenatedSignatureValue + * concatenated signature value (as used by JWS standard) + * @return DER-encoded signature value + * @throws IOException + */ + public static byte[] convertJWSConcatenatedToDEREncodedSignature(final byte[] concatenatedSignatureValue) throws IOException { + + final byte[] r = new byte[33]; + final byte[] s = new byte[33]; + System.arraycopy(concatenatedSignatureValue, 0, r, 1, 32); + System.arraycopy(concatenatedSignatureValue, 32, s, 1, 32); + final BigInteger rBigInteger = new BigInteger(r); + final BigInteger sBigInteger = new BigInteger(s); + + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + final DERSequenceGenerator seqGen = new DERSequenceGenerator(bos); + + seqGen.addObject(new ASN1Integer(rBigInteger.toByteArray())); + seqGen.addObject(new ASN1Integer(sBigInteger.toByteArray())); + seqGen.close(); + bos.close(); + + final byte[] derEncodedSignatureValue = bos.toByteArray(); + + return derEncodedSignatureValue; + } + + /** + * Generates a random AES key for encrypting/decrypting the turnover value + * ATTENTION: In a real cash box this key would be generated during the init + * process and stored in a secure area + * + * @return generated AES key + */ + public static SecretKey createAESKey() { + try { + final KeyGenerator kgen = KeyGenerator.getInstance("AES"); + final int keySize = 256; + kgen.init(keySize); + return kgen.generateKey(); + } catch (final NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return null; + } + + /** + * helper method to check whether the JVM has the unlimited strength policy + * installed + * + * @return + */ + public static boolean isUnlimitedStrengthPolicyAvailable() { + try { + return Cipher.getMaxAllowedKeyLength("AES") >= 256; + } catch (final NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return false; + } + + /** + * convert base64 encoded AES key to JAVA SecretKey + * + * @param base64AESKey + * BASE64 encoded AES key + * @return Java SecretKey representation of encoded AES key + */ + public static SecretKey convertBase64KeyToSecretKey(final String base64AESKey) { + final byte[] rawAesKey = CashBoxUtils.base64Decode(base64AESKey, false); + final SecretKeySpec aesKey = new SecretKeySpec(rawAesKey, "AES"); + return aesKey; + } + + /** + * method for AES encryption in ECB mode + * + * @param concatenatedHashValue + * @param turnoverCounter + * @param symmetricKey + */ + public static String encryptECB(final byte[] concatenatedHashValue, final Long turnoverCounter, final SecretKey symmetricKey,int turnOverCounterLengthInBytes) + throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException { + + // extract bytes 0-15 from hash value + final ByteBuffer byteBufferIV = ByteBuffer.allocate(16); + byteBufferIV.put(concatenatedHashValue); + final byte[] IV = byteBufferIV.array(); + + // prepare data + // block size for AES is 128 bit (16 bytes) + // thus, the turnover counter needs to be inserted into an array of length 16 + + //initialisation of the data which should be encrypted + final ByteBuffer byteBufferData = ByteBuffer.allocate(16); + byteBufferData.putLong(turnoverCounter); + final byte[] data = byteBufferData.array(); + + //now the turnover counter is represented in two's-complement representation (negative values are possible) + //length is defined by the respective implementation (min. 5 bytes) + byte[] turnOverCounterByteRep = get2ComplementRepForLong(turnoverCounter,turnOverCounterLengthInBytes); + + //two's-complement representation is copied to the data array, and inserted at index 0 + System.arraycopy(turnOverCounterByteRep,0,data,0,turnOverCounterByteRep.length); + + // prepare AES cipher with ECB mode, NoPadding is essential for the + // decryption process. Padding could not be reconstructed due + // to storing only 8 bytes of the cipher text (not the full 16 bytes) + // (or 5 bytes if the mininum turnover length is used) + // + // Note: Due to the use of ECB mode, no IV is defined for initializing + // the cipher. In addition, the data is not enciphered directly. Instead, + // the computed IV is encrypted. The result is subsequently XORed + // bitwise with the data to compute the cipher text. + final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", "BC"); + cipher.init(Cipher.ENCRYPT_MODE, symmetricKey); + final byte[] intermediateResult = cipher.doFinal(IV); + + final byte[] result = new byte[data.length]; + + // xor encryption result with data + for (int i = 0; i < data.length; i++) { + result[i] = (byte) ((data[i]) ^ (intermediateResult[i])); + } + + final byte[] encryptedTurnOverValue = new byte[turnOverCounterLengthInBytes]; + + // turnover length is used + System.arraycopy(result, 0, encryptedTurnOverValue, 0, turnOverCounterLengthInBytes); + + // encode result as BASE64 + return CashBoxUtils.base64Encode(encryptedTurnOverValue, false); + } + + /** + * method for AES decryption in ECB mode + * + * @param concatenatedHashValue + * @param base64EncryptedTurnOverValue + * @param symmetricKey + */ + public static long decryptECB(final byte[] concatenatedHashValue, final String base64EncryptedTurnOverValue, + final SecretKey symmetricKey) + throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + + // extract bytes 0-15 from hash value + final ByteBuffer byteBufferIV = ByteBuffer.allocate(16); + byteBufferIV.put(concatenatedHashValue); + final byte[] IV = byteBufferIV.array(); + + final byte[] encryptedTurnOverValue = CashBoxUtils.base64Decode(base64EncryptedTurnOverValue, false); + + // prepare AES cipher with ECB mode + // + // Note: Due to the use of ECB mode, no IV is defined for initializing + // the cipher. In addition, the data is not enciphered directly. Instead, + // the IV computed above is encrypted again. The result is subsequently + // XORed + // bitwise with the cipher text to retrieve the plain data. + final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", "BC"); + cipher.init(Cipher.ENCRYPT_MODE, symmetricKey); + final byte[] intermediateResult = cipher.doFinal(IV); + + final byte[] result = new byte[encryptedTurnOverValue.length]; + + // XOR decryption result with data + for (int i = 0; i < encryptedTurnOverValue.length; i++) { + result[i] = (byte) ((encryptedTurnOverValue[i]) ^ (intermediateResult[i])); + } + + return getLong(result); + + } + + /** + * method for AES encryption in CFB mode (for the first block CFB and CTR are + * exactly the same + * + * @param concatenatedHashValue + * @param turnoverCounter + * @param symmetricKey + */ + public static String encryptCFB(final byte[] concatenatedHashValue, final Long turnoverCounter, final SecretKey symmetricKey,int turnOverCounterLengthInBytes) + throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + + // extract bytes 0-15 from hash value + final ByteBuffer byteBufferIV = ByteBuffer.allocate(16); + byteBufferIV.put(concatenatedHashValue); + final byte[] IV = byteBufferIV.array(); + + // prepare data + // block size for AES is 128 bit (16 bytes) + // thus, the turnover counter needs to be inserted into an array of length 16 + + //initialisation of the data which should be encrypted + final ByteBuffer byteBufferData = ByteBuffer.allocate(16); + byteBufferData.putLong(turnoverCounter); + final byte[] data = byteBufferData.array(); + + //now the turnover counter is represented in two's-complement representation (negative values are possible) + //length is defined by the respective implementation (min. 5 bytes) + byte[] turnOverCounterByteRep = get2ComplementRepForLong(turnoverCounter,turnOverCounterLengthInBytes); + + //two's-complement representation is copied to the data array, and inserted at index 0 + System.arraycopy(turnOverCounterByteRep,0,data,0,turnOverCounterByteRep.length); + + // prepare AES cipher with CFB mode, NoPadding is essential for the + // decryption process. Padding could not be reconstructed due + // to storing only 8 bytes of the cipher text (not the full 16 bytes) + // (or 5 bytes if the mininum turnover length is used) + final IvParameterSpec ivSpec = new IvParameterSpec(IV); + + final Cipher cipher = Cipher.getInstance("AES/CFB/NoPadding", "BC"); + cipher.init(Cipher.ENCRYPT_MODE, symmetricKey, ivSpec); + + // encrypt the turnover value with the prepared cipher + final byte[] encryptedTurnOverValueComplete = cipher.doFinal(data); + + // extract bytes that will be stored in the receipt (only bytes 0-7) + final byte[] encryptedTurnOverValue = new byte[turnOverCounterLengthInBytes]; // or 5 bytes if min. + // turnover length is + // used + System.arraycopy(encryptedTurnOverValueComplete, 0, encryptedTurnOverValue, 0, + turnOverCounterLengthInBytes); + + // encode result as BASE64 + final String base64EncryptedTurnOverValue = CashBoxUtils.base64Encode(encryptedTurnOverValue, false); + + + + return base64EncryptedTurnOverValue; + + } + + /** + * method for AES decryption in CFB mode + * + * @param concatenatedHashValue + * @param base64EncryptedTurnOverValue + * @param symmetricKey + */ + public static long decryptCFB(final byte[] concatenatedHashValue, final String base64EncryptedTurnOverValue, + final SecretKey symmetricKey) + throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + + // extract bytes 0-15 from hash value + final ByteBuffer byteBufferIV = ByteBuffer.allocate(16); + byteBufferIV.put(concatenatedHashValue); + final byte[] IV = byteBufferIV.array(); + + final byte[] encryptedTurnOverValue = CashBoxUtils.base64Decode(base64EncryptedTurnOverValue, false); + + // prepare AES cipher with CFB mode + final IvParameterSpec ivSpec = new IvParameterSpec(IV); + + final Cipher cipher = Cipher.getInstance("AES/CFB/NoPadding", "BC"); + cipher.init(Cipher.DECRYPT_MODE, symmetricKey, ivSpec); + final byte[] testPlainTurnOverValueComplete = cipher.doFinal(encryptedTurnOverValue); + return getLong(testPlainTurnOverValueComplete); + + } + + //this helper-method converts the byte-array-representation of the turnover counter to a long value + //the constructor of the biginteger class correctly interprets the byte array as two's-complement rep and + //creates the appropriate biginteger, which is then converted to a long value + static long getLong(final byte[] bytes) { + + return new BigInteger(bytes).longValue(); + } + + /** + * method for AES encryption in CTR mode + * + * @param concatenatedHashValue + * @param turnoverCounter + * @param symmetricKey + */ + public static String encryptCTR(final byte[] concatenatedHashValue, Long turnoverCounter, final SecretKey symmetricKey, int turnOverCounterLengthInBytes) + throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + + // extract bytes 0-15 from hash value + final ByteBuffer byteBufferIV = ByteBuffer.allocate(16); + byteBufferIV.put(concatenatedHashValue); + final byte[] IV = byteBufferIV.array(); + + // prepare data + // block size for AES is 128 bit (16 bytes) + // thus, the turnover counter needs to be inserted into an array of length 16 + + //initialisation of the data which should be encrypted + final ByteBuffer byteBufferData = ByteBuffer.allocate(16); + byteBufferData.putLong(turnoverCounter); + final byte[] data = byteBufferData.array(); + + //now the turnover counter is represented in two's-complement representation (negative values are possible) + //length is defined by the respective implementation (min. 5 bytes) + byte[] turnOverCounterByteRep = get2ComplementRepForLong(turnoverCounter,turnOverCounterLengthInBytes); + + //two's-complement representation is copied to the data array, and inserted at index 0 + System.arraycopy(turnOverCounterByteRep,0,data,0,turnOverCounterByteRep.length); + + // prepare AES cipher with CTR/ICM mode, NoPadding is essential for the + // decryption process. Padding could not be reconstructed due + // to storing only 8 bytes of the cipher text (not the full 16 bytes) + // (or 5 bytes if the mininum turnover length is used) + final IvParameterSpec ivSpec = new IvParameterSpec(IV); + + final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC"); + cipher.init(Cipher.ENCRYPT_MODE, symmetricKey, ivSpec); + + // encrypt the turnover value with the prepared cipher + final byte[] encryptedTurnOverValueComplete = cipher.doFinal(data); + + // extract bytes that will be stored in the receipt (only bytes 0-7) + // cryptographic NOTE: this is only possible due to the use of the CTR + // mode, would not work for ECB/CBC etc. modes + final byte[] encryptedTurnOverValue = new byte[turnOverCounterLengthInBytes]; // or 5 bytes if min. + // turnover length is + // used + System.arraycopy(encryptedTurnOverValueComplete, 0, encryptedTurnOverValue, 0, + turnOverCounterLengthInBytes); + + // encode result as BASE64 + + return CashBoxUtils.base64Encode(encryptedTurnOverValue, false); + + } + + /** + * method for AES decryption in CTR mode + * + * @param concatenatedHashValue + * @param base64EncryptedTurnOverValue + * @param symmetricKey + */ + public static long decryptCTR(final byte[] concatenatedHashValue, final String base64EncryptedTurnOverValue, + final SecretKey symmetricKey) + throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + + // extract bytes 0-15 from hash value + final ByteBuffer byteBufferIV = ByteBuffer.allocate(16); + byteBufferIV.put(concatenatedHashValue); + final byte[] IV = byteBufferIV.array(); + + final byte[] encryptedTurnOverValue = CashBoxUtils.base64Decode(base64EncryptedTurnOverValue, false); + + // prepare AES cipher with CTR/ICM mode + final IvParameterSpec ivSpec = new IvParameterSpec(IV); + + final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC"); + cipher.init(Cipher.DECRYPT_MODE, symmetricKey, ivSpec); + final byte[] testPlainTurnOverValueComplete = cipher.doFinal(encryptedTurnOverValue); + return getLong(testPlainTurnOverValueComplete); + + } + + // see next method + public static long decryptTurnOverCounter(final String encryptedTurnOverCounterBase64, final String hashAlgorithm, + final String cashBoxIDUTF8String, final String receiptIdentifierUTF8String, final String aesKeyBase64) throws Exception { + final byte[] rawAesKey = CashBoxUtils.base64Decode(aesKeyBase64, false); + final SecretKey aesKey = new SecretKeySpec(rawAesKey, "AES"); + return decryptTurnOverCounter(encryptedTurnOverCounterBase64, hashAlgorithm, cashBoxIDUTF8String, + receiptIdentifierUTF8String, aesKey); + } + + /** + * decrypt the turnover counter with the given AES key, and parameters for IV + * creation + * Ref: Detailspezifikation Abs 8/Abs 9/Abs 10 + * + * @param encryptedTurnOverCounterBase64 + * encrypted turnover counter + * @param hashAlgorithm + * hash-algorithm used to generate IV + * @param cashBoxIDUTF8String + * cashbox-id, required for IV creation + * @param receiptIdentifierUTF8String + * receiptidentifier, required for IV creation + * @param aesKey + * aes key + * @return decrypted turnover value as long + * @throws Exception + */ + public static long decryptTurnOverCounter(final String encryptedTurnOverCounterBase64, final String hashAlgorithm, + final String cashBoxIDUTF8String, final String receiptIdentifierUTF8String, final SecretKey aesKey) throws Exception { + // calc IV value (cashbox if + receipt identifer, both as UTF-8 Strings) + final String IVUTF8StringRepresentation = cashBoxIDUTF8String + receiptIdentifierUTF8String; + + // calc hash + final MessageDigest messageDigest = MessageDigest.getInstance(hashAlgorithm); + final byte[] hashValue = messageDigest.digest(IVUTF8StringRepresentation.getBytes()); + final byte[] concatenatedHashValue = new byte[16]; + System.arraycopy(hashValue, 0, concatenatedHashValue, 0, 16); + + // extract bytes 0-15 from hash value + final ByteBuffer byteBufferIV = ByteBuffer.allocate(16); + byteBufferIV.put(concatenatedHashValue); + + // IV for AES algorithm + final byte[] IV = byteBufferIV.array(); + + // prepare AES cipher with CTR/ICM mode, NoPadding is essential for the + // decryption process. Padding could not be reconstructed due + // to storing only 8 bytes of the cipher text (not the full 16 bytes) (or 5 + // bytes if the minimum turnover length is used) + final IvParameterSpec ivSpec = new IvParameterSpec(IV); + + // start decryption process + final ByteBuffer encryptedTurnOverValueComplete = ByteBuffer.allocate(16); + + // decode turnover base64 value + final byte[] encryptedTurnOverValue = CashBoxUtils.base64Decode(encryptedTurnOverCounterBase64, false); + + // extract length (required to extract the correct number of bytes from + // decrypted value + final int lengthOfEncryptedTurnOverValue = encryptedTurnOverValue.length; + + // prepare for decryption (require 128 bit blocks...) + encryptedTurnOverValueComplete.put(encryptedTurnOverValue); + + // decryption setup, AES ciper in CTR mode, NO PADDING!) + final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding", "BC"); + cipher.init(Cipher.DECRYPT_MODE, aesKey, ivSpec); + + // decrypt value, now we have a 128 bit block, with trailing junk bytes + final byte[] plainTurnOverValueComplete = cipher.doFinal(encryptedTurnOverValue); + + // // remove junk bytes by extracting known length of plain text + byte[] plainTurnOverValueTruncated = new byte[lengthOfEncryptedTurnOverValue]; + System.arraycopy(plainTurnOverValueComplete, 0, plainTurnOverValueTruncated, 0, lengthOfEncryptedTurnOverValue); + + return new BigInteger(plainTurnOverValueTruncated).longValue(); + } + + /** + * computing chaining value for input receipt + * @param input input receipt for which the chaining value should be calculated + * @param rkSuite suite with information for chaining value calculation + * @return BASE64-encoded chain value + * @throws NoSuchAlgorithmException + */ + public static String computeChainingValue(final String input, final RKSuiteIdentifier rkSuite) + throws NoSuchAlgorithmException { + final MessageDigest md = MessageDigest.getInstance(rkSuite.getHashAlgorithmForPreviousSignatureValue()); + + // calculate hash value + md.update(input.getBytes()); + final byte[] digest = md.digest(); + + // extract number of bytes (N, defined in RKsuite) from hash value + final int bytesToExtract = rkSuite.getNumberOfBytesExtractedFromPrevSigHash(); + final byte[] conDigest = new byte[bytesToExtract]; + System.arraycopy(digest, 0, conDigest, 0, bytesToExtract); + + // encode value as BASE64 String ==> chainValue + return CashBoxUtils.base64Encode(conDigest, false); + } + + /** + * get hash value for given String + * @param data string to be hashed + * @return hash value as biginteger in hex representation + * @throws CryptoException + */ + public static String hashData(final String data) throws CryptoException { + try { + final MessageDigest md = MessageDigest.getInstance("SHA-256", "BC"); + md.update(data.getBytes("UTF-8")); + return new BigInteger(md.digest()).toString(16); + } catch (final Exception e) { + throw new CryptoException(e.getMessage(), e); + } + } +} diff --git a/regkassen-common/src/test/java/at/asitplus/regkassen/common/util/CryptoUtilTest.java b/regkassen-common/src/test/java/at/asitplus/regkassen/common/util/CryptoUtilTest.java new file mode 100644 index 0000000..21be2b3 --- /dev/null +++ b/regkassen-common/src/test/java/at/asitplus/regkassen/common/util/CryptoUtilTest.java @@ -0,0 +1,75 @@ +/* + * 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.common.util; + +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.math.BigInteger; +import java.security.SecureRandom; + +public class CryptoUtilTest { + + @Test(dataProvider = "byteProvider") + public void testByteArrayToLong(byte[] bytes, long expected) { + + Assert.assertEquals(CryptoUtil.getLong(bytes), expected); + } + + @DataProvider + public Object[][] byteProvider() { + + final int NUM_VALUES = 200; + Object[][] res = new Object[NUM_VALUES][2]; + SecureRandom secureRandom = new SecureRandom(); + + for (int i = 0; i < NUM_VALUES; ++i) { + + byte[] arr; + long number; + do { + number = Integer.MAX_VALUE; + number += secureRandom.nextInt(); + if (i % 2 == 0) + number += secureRandom.nextInt(); + if (i % 3 == 0) + number += secureRandom.nextInt(); + if (i % 5 == 0) + number += secureRandom.nextInt(); + if (i % 7 == 0) + number += secureRandom.nextInt(); + if (i % 11 == 0) + number += secureRandom.nextInt(); + if (i % 13 == 0) + number += secureRandom.nextInt(); + + if (i % 2 == 0) { + number = -number; + } + + arr = BigInteger.valueOf(number).toByteArray(); + } while (arr.length != 5); + + res[i][0] = arr; + res[i][1] = number; + } + return res; + + } +}