at.asitplus.regkassen.common.util.CryptoUtil.java Source code

Java tutorial

Introduction

Here is the source code for at.asitplus.regkassen.common.util.CryptoUtil.java

Source

/*
 * 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);
        }
    }
}