All Downloads are FREE. Search and download functionalities are using the official Maven repository.

gurux.dlms.secure.GXSecure Maven / Gradle / Ivy

There is a newer version: 4.0.72
Show newest version
//
// --------------------------------------------------------------------------
//  Gurux Ltd
// 
//
//
// Filename:        $HeadURL$
//
// Version:         $Revision$,
//                  $Date$
//                  $Author$
//
// Copyright (c) Gurux Ltd
//
//---------------------------------------------------------------------------
//
//  DESCRIPTION
//
// This file is a part of Gurux Device Framework.
//
// Gurux Device Framework is Open Source software; you can redistribute it
// and/or modify it under the terms of the GNU General Public License 
// as published by the Free Software Foundation; version 2 of the License.
// Gurux Device Framework is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of 
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
// See the GNU General Public License for more details.
//
// More information of Gurux products: https://www.gurux.org
//
// This code is licensed under the GNU General Public License v2. 
// Full text may be retrieved at http://www.gnu.org/licenses/gpl-2.0.txt
//---------------------------------------------------------------------------

package gurux.dlms.secure;

import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyAgreement;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import gurux.dlms.GXByteBuffer;
import gurux.dlms.GXCryptoKeyParameter;
import gurux.dlms.GXDLMSExceptionResponse;
import gurux.dlms.GXDLMSSettings;
import gurux.dlms.GXDLMSTranslator;
import gurux.dlms.GXICipher;
import gurux.dlms.asn.GXAsn1BitString;
import gurux.dlms.asn.GXAsn1Converter;
import gurux.dlms.asn.GXAsn1Integer;
import gurux.dlms.asn.GXAsn1Sequence;
import gurux.dlms.enums.Authentication;
import gurux.dlms.enums.Command;
import gurux.dlms.enums.CryptoKeyType;
import gurux.dlms.enums.ExceptionServiceError;
import gurux.dlms.enums.ExceptionStateError;
import gurux.dlms.enums.KeyAgreementScheme;
import gurux.dlms.enums.Security;
import gurux.dlms.enums.Signing;
import gurux.dlms.internal.GXCommon;
import gurux.dlms.objects.enums.CertificateType;
import gurux.dlms.objects.enums.SecurityPolicy;
import gurux.dlms.objects.enums.SecuritySuite;

public final class GXSecure {
    private static final byte[] IV =
            { (byte) 0xA6, (byte) 0xA6, (byte) 0xA6, (byte) 0xA6, (byte) 0xA6, (byte) 0xA6, (byte) 0xA6, (byte) 0xA6 };

    /**
     * Default Logger.
     */
    private final static Logger LOGGER = Logger.getLogger(GXSecure.class.getName());

    /*
     * Constructor.
     */
    private GXSecure() {
    }

    private static int getBlocklength(final byte[] data) {
        if (data.length % 16 == 0) {
            return data.length;
        }
        return data.length + (16 - data.length % 16);
    }

    /*
     * Encrypt data using AES.
     * @param data Encrypted data.
     * @param secret Secret.
     */
    static byte[] aes1Encrypt(final byte[] data, final byte[] secret)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException,
            BadPaddingException, InvalidAlgorithmParameterException {
        byte[] d = new byte[getBlocklength(data)];
        byte[] s = new byte[16];
        System.arraycopy(data, 0, d, 0, data.length);
        System.arraycopy(secret, 0, s, 0, secret.length);
        IvParameterSpec ivspec = new IvParameterSpec(new byte[16]);
        SecretKeySpec keyToBeWrapped2 = new SecretKeySpec(s, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, keyToBeWrapped2, ivspec);
        return cipher.doFinal(d);
    }

    /*
     * Decrypt data using AES.
     * @param data Encrypted data.
     * @param secret Secret.
     */
    static byte[] aes1Decrypt(final byte[] data, final byte[] secret)
            throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException,
            BadPaddingException, InvalidAlgorithmParameterException {
        byte[] d = new byte[getBlocklength(data)];
        byte[] s = new byte[16];
        System.arraycopy(data, 0, d, 0, data.length);
        System.arraycopy(secret, 0, s, 0, secret.length);

        IvParameterSpec ivspec = new IvParameterSpec(new byte[16]);
        SecretKeySpec keyToBeWrapped2 = new SecretKeySpec(s, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, keyToBeWrapped2, ivspec);
        return cipher.doFinal(d);
    }

    /*
     * Encrypt data using AES RFC3394 key-wrapping.
     * @param data Encrypted data.
     */
    static byte[] encryptAesKeyWrapping(final byte[] data, final byte[] kek) throws InvalidKeyException,
            NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
        if (kek == null || kek.length % 8 != 0) {
            throw new IllegalArgumentException("Key Encrypting Key");
        }
        int n = data.length / 8;

        if ((n * 8) != data.length) {
            throw new IllegalArgumentException("Invalid data.");
        }
        byte[] block = new byte[data.length + IV.length];
        byte[] buf = new byte[8 + IV.length];

        System.arraycopy(IV, 0, block, 0, IV.length);
        System.arraycopy(data, 0, block, IV.length, data.length);
        SecretKeySpec keyToBeWrapped2 = new SecretKeySpec(kek, "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, keyToBeWrapped2);
        for (int j = 0; j != 6; j++) {
            for (int i = 1; i <= n; i++) {
                System.arraycopy(block, 0, buf, 0, IV.length);
                System.arraycopy(block, 8 * i, buf, IV.length, 8);
                buf = cipher.doFinal(buf);
                int t = n * j + i;
                for (int k = 1; t != 0; k++) {
                    byte v = (byte) t;
                    buf[IV.length - k] ^= v;
                    t = t >> 8;
                }
                System.arraycopy(buf, 0, block, 0, 8);
                System.arraycopy(buf, 8, block, 8 * i, 8);
            }
        }
        return block;
    }

    /*
     * Decrypt data using AES RFC3394 key-wrapping.
     * @param input Decrypted data.
     * @param kek KEK.
     */
    static byte[] decryptAesKeyWrapping(final byte[] input, final byte[] kek) {
        if (kek == null || kek.length % 8 != 0) {
            throw new IllegalArgumentException("Key Encrypting Key");
        }
        int n = input.length / 8;
        if ((n * 8) != input.length) {
            throw new IllegalArgumentException("Invalid data.");
        }

        byte[] block;
        if (input.length > IV.length) {
            block = new byte[input.length - IV.length];
        } else {
            block = new byte[IV.length];
        }
        byte[] a = new byte[IV.length];
        byte[] buf = new byte[8 + IV.length];

        System.arraycopy(input, 0, a, 0, IV.length);
        System.arraycopy(input, 0 + IV.length, block, 0, input.length - IV.length);

        n = n - 1;
        if (n == 0) {
            n = 1;
        }
        try {
            SecretKeySpec keyToBeWrapped2 = new SecretKeySpec(kek, "AES");
            Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, keyToBeWrapped2);
            for (int j = 5; j >= 0; j--) {
                for (int i = n; i >= 1; i--) {
                    System.arraycopy(a, 0, buf, 0, IV.length);
                    System.arraycopy(block, 8 * (i - 1), buf, IV.length, 8);
                    int t = n * j + i;
                    for (int k = 1; t != 0; k++) {
                        byte v = (byte) t;
                        buf[IV.length - k] ^= v;
                        t = t >> 8;
                    }
                    buf = cipher.doFinal(buf);
                    System.arraycopy(buf, 0, a, 0, 8);
                    System.arraycopy(buf, 8, block, 8 * (i - 1), 8);
                }
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex.getMessage());
        }
        if (!GXCommon.compare(a, IV)) {
            throw new ArithmeticException("AES key wrapping failed.");
        }
        return block;
    }

    /*
     * Chipher text.
     * @param settings DLMS settings.
     * @param cipher Chipher settings.
     * @param ic IC
     * @param data Text to chipher.
     * @param secret Secret.
     * @return Chiphered text.
     */
    public static byte[] secure(final GXDLMSSettings settings, final GXICipher cipher, final long ic, final byte[] data,
            final byte[] secret) throws InvalidKeyException, NoSuchPaddingException, InvalidAlgorithmParameterException,
            IllegalBlockSizeException, BadPaddingException, SignatureException {
        try {

            byte[] d;
            if (settings.getAuthentication() == Authentication.HIGH) {
                return aes1Encrypt(data, secret);
            }
            // Get server Challenge.
            GXByteBuffer challenge = new GXByteBuffer();
            // Get shared secret
            if (settings.getAuthentication() == Authentication.HIGH_GMAC) {
                challenge.set(data);
            } else if (settings.getAuthentication() == Authentication.HIGH_SHA256
                    || settings.getAuthentication() == Authentication.HIGH_ECDSA) {
                challenge.set(secret);
            } else {
                challenge.set(data);
                challenge.set(secret);
            }
            d = challenge.array();
            MessageDigest md;
            switch (settings.getAuthentication()) {
            case HIGH_MD5:
                md = MessageDigest.getInstance("MD5");
                d = md.digest(d);
                break;
            case HIGH_SHA1:
                md = MessageDigest.getInstance("SHA-1");
                d = md.digest(d);
                break;
            case HIGH_SHA256:
                md = MessageDigest.getInstance("SHA-256");
                d = md.digest(d);
                break;
            case HIGH_GMAC:
                // SC is always Security.Authentication.
                AesGcmParameter p = new AesGcmParameter(settings, 0, Security.AUTHENTICATION, cipher.getSecuritySuite(),
                        ic, secret, cipher.getBlockCipherKey(), cipher.getAuthenticationKey());
                p.setType(CountType.TAG);
                challenge.clear();
                challenge.setUInt8(
                        Security.AUTHENTICATION.getValue() | settings.getCipher().getSecuritySuite().getValue());
                challenge.setUInt32(p.getInvocationCounter());
                challenge.set(encryptAesGcm(true, p, d));
                d = challenge.array();
                break;
            case HIGH_ECDSA:
                Signature sig;
                if (settings.getCipher().getSecuritySuite() == SecuritySuite.SUITE_1) {
                    sig = Signature.getInstance("SHA256withECDSA");
                } else if (settings.getCipher().getSecuritySuite() == SecuritySuite.SUITE_2) {
                    sig = Signature.getInstance("SHA384withECDSA");
                } else {
                    throw new IllegalArgumentException("Invalid security suite.");
                }
                if (cipher.getSigningKeyPair() == null) {
                    throw new IllegalArgumentException("SigningKeyPair is empty.");
                }
                sig.initSign(cipher.getSigningKeyPair().getPrivate());
                sig.update(secret);
                d = sig.sign();
                GXAsn1Sequence seq = (GXAsn1Sequence) GXAsn1Converter.fromByteArray(d);
                GXByteBuffer bb = new GXByteBuffer();
                bb.set(((GXAsn1Integer) seq.get(0)).getByteArray());
                if (settings.getCipher().getSecuritySuite() == SecuritySuite.SUITE_1) {
                    if (bb.size() != 32) {
                        bb.move(1, 0, 32);
                    }
                    bb.set(((GXAsn1Integer) seq.get(1)).getByteArray());
                    if (bb.size() != 64) {
                        bb.move(33, 32, 32);
                    }
                } else {
                    // Security Suite 2.
                    if (bb.size() != 48) {
                        bb.move(1, 0, 48);
                    }
                    bb.set(((GXAsn1Integer) seq.get(1)).getByteArray());
                    if (bb.size() != 96) {
                        bb.move(49, 48, 48);
                    }
                }
                d = bb.array();
                break;
            default:
            }
            return d;
        } catch (NoSuchAlgorithmException ex) {
            throw new RuntimeException(ex.getMessage());
        }
    }

    /*
     * Generates challenge.
     * @param authentication Used authentication.
     * @return Generated challenge.
     */
    public static byte[] generateChallenge(final Authentication authentication, final byte size) {
        // Random challenge is 8 to 64 bytes.
        Random r = new Random();
        int len = size;
        if (size == 0 || (size == 16 && authentication == Authentication.HIGH_ECDSA)) {
            if (authentication == Authentication.HIGH_ECDSA) {
                len = r.nextInt(32) + 32;
            } else {
                len = r.nextInt(57) + 8;
            }
        }
        byte[] result = new byte[len];
        for (int pos = 0; pos != len; ++pos) {
            result[pos] = (byte) r.nextInt(0x7A);
        }
        return result;
    }

    /*
     * Generate KDF.
     * @param hashAlg Hash Algorithm. (SHA-256 or SHA-384 )
     * @param z Shared Secret.
     * @param keyDataLen Key data length in bits.
     * @param algorithmID Algorithm ID.
     * @param partyUInfo Sender system title.
     * @param partyVInfo Receiver system title.
     * @param suppPubInfo Not used in DLMS.
     * @param suppPrivInfo Not used in DLMS.
     * @return Generated KDF.
     */
    public static byte[] generateKDF(final String hashAlg, final byte[] z, final int keyDataLen,
            final byte[] algorithmID, final byte[] partyUInfo, final byte[] partyVInfo, final byte[] suppPubInfo,
            final byte[] suppPrivInfo) {
        GXByteBuffer bb = new GXByteBuffer();
        bb.set(algorithmID);
        bb.set(partyUInfo);
        bb.set(partyVInfo);
        if (suppPubInfo != null) {
            bb.set(suppPubInfo);
        }
        if (suppPrivInfo != null) {
            bb.set(suppPrivInfo);
        }
        return generateKDF(hashAlg, z, keyDataLen, bb.array());
    }

    /*
     * Generate KDF.
     * @param hashAlg Hash Algorithm. (SHA-256 or SHA-384 )
     * @param z Shared Secret.
     * @param keyDataLen Key data length in bits.
     * @param otherInfo OtherInfo
     * @return Generated KDF.
     */
    private static byte[] generateKDF(final String hashAlg, final byte[] z, final int keyDataLen,
            final byte[] otherInfo) {
        byte[] key = new byte[keyDataLen / 8];
        try {
            MessageDigest md = MessageDigest.getInstance(hashAlg);
            int hashLen = md.getDigestLength();
            int cnt = key.length / hashLen;
            byte[] v = new byte[4];
            for (int pos = 1; pos <= cnt; pos++) {
                md.reset();
                getBytes(v, pos);
                md.update(v);
                md.update(z);
                md.update(otherInfo);
                byte[] hash = md.digest();
                if (pos < cnt) {
                    System.arraycopy(hash, 0, key, hashLen * (pos - 1), hashLen);
                } else {
                    if (key.length % hashLen == 0) {
                        System.arraycopy(hash, 0, key, hashLen * (pos - 1), hashLen);
                    } else {
                        System.arraycopy(hash, 0, key, hashLen * (pos - 1), key.length % hashLen);
                    }
                }
            }
            return key;
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    /*
     * Convert int to byte array.
     * @param buff Buffer where data is saved.
     * @param value Integer value.
     */
    private static void getBytes(final byte[] buff, final int value) {
        buff[0] = (byte) (value >>> 24);
        buff[1] = (byte) ((value >>> 16) & 0xFF);
        buff[2] = (byte) ((value >>> 8) & 0xFF);
        buff[3] = (byte) (value & 0xFF);
    }

    /**
     * Get Ephemeral Public Key Signature.
     * 
     * @param keyId
     *            Key ID.
     * @param ephemeralKey
     *            Ephemeral key.
     * @return Ephemeral Public Key Signature.
     */
    public static byte[] getEphemeralPublicKeyData(final int keyId, final PublicKey ephemeralKey) {
        GXAsn1BitString tmp =
                (GXAsn1BitString) ((GXAsn1Sequence) GXAsn1Converter.fromByteArray(ephemeralKey.getEncoded())).get(1);

        // Ephemeral public key client
        GXByteBuffer epk = new GXByteBuffer(tmp.getValue());
        // First byte is 4 in Java and that is not used. We can override it.
        epk.getData()[0] = (byte) keyId;
        return epk.array();
    }

    /*
     * Get Ephemeral Public Key Signature.
     * @param keyId Key ID.
     * @param ephemeralKey Ephemeral key.
     * @param signKey Private Key.
     * @return Ephemeral Public Key Signature.
     */
    public static byte[] getEphemeralPublicKeySignature(final int keyId, final PublicKey ephemeralKey,
            final PrivateKey signKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
        byte[] epk = getEphemeralPublicKeyData(keyId, ephemeralKey);
        // Add ephemeral public key signature.
        Signature instance = Signature.getInstance("SHA256withECDSA");
        instance.initSign(signKey);
        instance.update(epk);
        byte[] sign = instance.sign();
        GXAsn1Sequence tmp2 = (GXAsn1Sequence) GXAsn1Converter.fromByteArray(sign);
        LOGGER.log(Level.FINEST, "{0}", GXCommon.toHex(sign));
        GXByteBuffer data = new GXByteBuffer(64);
        // Truncate to 64 bytes. Remove zeros from the begin.
        byte[] arr = ((GXAsn1Integer) tmp2.get(0)).getByteArray();
        if (arr.length < 32) {
            GXByteBuffer bb = new GXByteBuffer();
            bb.size(32 - arr.length);
            bb.set(arr);
            arr = bb.array();
        }
        data.set(arr, arr.length - 32, 32);
        arr = ((GXAsn1Integer) tmp2.get(1)).getByteArray();
        data.set(arr, arr.length - 32, 32);
        return data.array();
    }

    /*
     * Validate ephemeral public key signature.
     * @param data Data to validate.
     * @param sign Sign.
     * @param publicSigningKey Public Signing key from other party.
     * @return Is verify succeeded.
     */
    public static boolean validateEphemeralPublicKeySignature(final byte[] data, final byte[] sign,
            final PublicKey publicSigningKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {

        GXAsn1Integer a = new GXAsn1Integer(sign, 0, 32);
        GXAsn1Integer b = new GXAsn1Integer(sign, 32, 32);
        GXAsn1Sequence s = new GXAsn1Sequence();
        s.add(a);
        s.add(b);
        byte[] tmp = GXAsn1Converter.toByteArray(s);
        Signature instance = Signature.getInstance("SHA256withECDSA");
        instance.initVerify(publicSigningKey);
        instance.update(data);
        boolean v = instance.verify(tmp);
        if (!v) {
            LOGGER.log(Level.FINEST, "Data: {0}", GXCommon.toHex(data));
            LOGGER.log(Level.FINEST, "Sign: {0}", GXCommon.toHex(sign));
        }
        return v;
    }

    private static Cipher getCipher(final AesGcmParameter p, final boolean encrypt) throws NoSuchAlgorithmException,
            NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
        GXByteBuffer iv = new GXByteBuffer();
        iv.set(p.getSystemTitle());
        iv.setUInt32(p.getInvocationCounter());
        SecretKeySpec eks = new SecretKeySpec(p.getBlockCipherKey(), "AES");
        Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
        int mode;
        if (encrypt) {
            mode = Cipher.ENCRYPT_MODE;
        } else {
            mode = Cipher.DECRYPT_MODE;
        }
        c.init(mode, eks, new GCMParameterSpec(12 * 8, iv.array()));
        return c;
    }

    private static byte[] countTag(final Cipher c, final AesGcmParameter p, final byte[] data)
            throws IllegalBlockSizeException, BadPaddingException {
        GXByteBuffer data2 = new GXByteBuffer();
        data2.setUInt8(p.getSecurity().getValue() | p.getSecuritySuite().getValue());
        data2.set(p.getAuthenticationKey());
        if (data != null) {
            data2.set(data);
        }
        c.updateAAD(data2.array());
        byte[] tmp = c.doFinal();
        // DLMS Tag is only 12 bytes.
        byte[] tag = new byte[12];
        System.arraycopy(tmp, 0, tag, 0, 12);
        return tag;
    }

    private static byte[] encrypt(final Cipher c, final byte[] data)
            throws IllegalBlockSizeException, BadPaddingException {
        byte[] es = c.doFinal(data);
        // Remove tag.
        byte[] tmp = new byte[es.length - 12];
        System.arraycopy(es, 0, tmp, 0, tmp.length);
        return tmp;
    }

    public static byte[] encryptAesGcm(final boolean encrypt, final AesGcmParameter p, final byte[] data)
            throws IllegalBlockSizeException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
            NoSuchPaddingException, InvalidAlgorithmParameterException {
        Cipher c = getCipher(p, encrypt || p.getSecurity() != Security.AUTHENTICATION_ENCRYPTION);
        if (LOGGER.isLoggable(Level.FINEST)) {
            LOGGER.log(Level.FINEST, "Authentication key: {0}", GXCommon.toHex(p.getAuthenticationKey()));
            LOGGER.log(Level.FINEST, "Block cipher key key: {0}", GXCommon.toHex(p.getBlockCipherKey()));
        }
        // If external Hardware Security Module is used.
        CryptoKeyType keyType;
        switch (p.getSecurity()) {
        case AUTHENTICATION:
            keyType = CryptoKeyType.AUTHENTICATION;
            break;
        case ENCRYPTION:
            keyType = CryptoKeyType.BLOCK_CIPHER;
            break;
        case AUTHENTICATION_ENCRYPTION:
            keyType = CryptoKeyType
                    .forValue(CryptoKeyType.AUTHENTICATION.getValue() | CryptoKeyType.BLOCK_CIPHER.getValue());
            break;
        default:
            throw new IllegalArgumentException("Security");
        }

        GXByteBuffer bb = new GXByteBuffer(10 + data.length);
        if (encrypt) {
            if (p.getType() == CountType.PACKET) {
                bb.setUInt8(p.getSecurity().getValue() | p.getSecuritySuite().getValue());
                bb.setUInt32(p.getInvocationCounter());
            }
            if (p.getSecurity() == Security.AUTHENTICATION) {
                byte[] tmp = countTag(c, p, data);
                if (p.getType() == CountType.PACKET) {
                    bb.set(data);
                }
                bb.set(tmp);
            } else if (p.getSecurity() == Security.ENCRYPTION) {
                bb.set(encrypt(c, data));
            } else if (p.getSecurity() == Security.AUTHENTICATION_ENCRYPTION) {
                byte[] crypted = null;
                if (p.getSettings() != null) {
                    crypted = GXCommon.crypt(p.getSettings(), CertificateType.DIGITAL_SIGNATURE, data, true, keyType,
                            Command.GLO_INITIATE_REQUEST, p.getSecurity(), p.getSecuritySuite(),
                            p.getInvocationCounter());
                }
                if (crypted != null) {
                    bb.set(crypted);
                } else {
                    GXByteBuffer data2 = new GXByteBuffer();
                    data2.setUInt8(p.getSecurity().getValue() | p.getSecuritySuite().getValue());
                    data2.set(p.getAuthenticationKey());
                    c.updateAAD(data2.array());
                    bb.set(c.doFinal(data));
                }
            }
            if (p.getType() == CountType.PACKET) {
                GXByteBuffer bb2 = new GXByteBuffer(5 + bb.size());
                bb2.setUInt8(p.getTag());
                if (p.getTag() == Command.GENERAL_GLO_CIPHERING || p.getTag() == Command.GENERAL_DED_CIPHERING
                        || p.getTag() == Command.DATA_NOTIFICATION) {
                    GXCommon.setObjectCount(p.getSystemTitle().length, bb2);
                    bb2.set(p.getSystemTitle());
                }
                GXCommon.setObjectCount(bb.size(), bb2);
                bb2.set(bb);
                return bb2.array();
            }
            return bb.array();
        }
        if (p.getSecurity() == Security.ENCRYPTION) {
            byte[] tmp2 = c.doFinal(data);
            // Remove tag.
            byte[] tmp = new byte[tmp2.length - 12];
            System.arraycopy(tmp2, 0, tmp, 0, tmp.length);
            return tmp;
        }
        if (p.getSecurity() == Security.AUTHENTICATION) {
            // Get tag.
            byte[] received = new byte[12];
            System.arraycopy(data, data.length - 12, received, 0, 12);
            GXByteBuffer data2 = new GXByteBuffer();
            data2.set(data);
            data2.size(data2.size() - 12);
            byte[] counted = countTag(c, p, data2.array());
            if (!GXCommon.compare(counted, received)) {
                throw new javax.crypto.AEADBadTagException();
            }
            return data2.array();
        }
        try {
            byte[] tmp = null;
            if (p.getSettings() != null) {
                tmp = GXCommon.crypt(p.getSettings(), CertificateType.DIGITAL_SIGNATURE, data, false, keyType,
                        Command.GLO_INITIATE_RESPONSE, p.getSecurity(), p.getSecuritySuite(), p.getInvocationCounter());
            }
            if (tmp != null) {
                return tmp;
            }
            GXByteBuffer data2 = new GXByteBuffer();
            int tag = p.getSecurity().getValue() | p.getSecuritySuite().getValue();
            if (p.isCompressed()) {
                tag |= 0x80;
            }
            if (p.isBroadcasted()) {
                tag |= 0x40;
            }
            data2.setUInt8(tag);
            data2.set(p.getAuthenticationKey());
            c.updateAAD(data2.array());
            return c.doFinal(data);
        } catch (Exception ex) {
            if (p.getXml() == null) {
                LOGGER.log(Level.SEVERE, "Decrypt failed. Invalid authentication tag.");
                throw ex;
            }
            if (ex instanceof javax.crypto.AEADBadTagException) {
                p.getXml().appendComment("Authentication tag is invalid.");
                c = getCipher(p, true);
                // Remove tag and encrypt the data.
                bb.set(data, 0, data.length - 12);
                byte[] tmp2 = c.doFinal(bb.array());
                // Remove tag.
                byte[] tmp = new byte[tmp2.length - 12];
                System.arraycopy(tmp2, 0, tmp, 0, tmp.length);
                return tmp;
            } else {
                throw ex;
            }
        }
    }

    /*
     * Decrypts data using Aes Gcm.
     * @param c Cipher settings.
     * @param p GMAC Parameter.
     * @return Decrypted data.
     */
    static byte[] decryptAesGcm(final GXICipher c, final AesGcmParameter p, final GXByteBuffer data)
            throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException,
            InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
        try {
            if (data == null || data.size() - data.position() < 2) {
                throw new IllegalArgumentException("cryptedData");
            }
            byte[] tmp;
            int len, cmd = data.getUInt8();
            switch (cmd) {
            case Command.GENERAL_GLO_CIPHERING:
            case Command.GENERAL_DED_CIPHERING:
                len = GXCommon.getObjectCount(data);
                if (len != 0) {
                    byte[] title = new byte[len];
                    data.get(title);
                    p.setSystemTitle(title);
                }
                if (p.getSystemTitle() == null || p.getSystemTitle().length != 8) {
                    if (p.getXml() == null) {
                        throw new IllegalArgumentException("Invalid sender system title.");
                    } else {
                        p.getXml().appendComment("Invalid sender system title.");
                    }
                }
                break;
            case Command.GENERAL_CIPHERING:
            case Command.GLO_INITIATE_REQUEST:
            case Command.GLO_INITIATE_RESPONSE:
            case Command.GLO_READ_REQUEST:
            case Command.GLO_READ_RESPONSE:
            case Command.GLO_WRITE_REQUEST:
            case Command.GLO_WRITE_RESPONSE:
            case Command.GLO_GET_REQUEST:
            case Command.GLO_GET_RESPONSE:
            case Command.GLO_SET_REQUEST:
            case Command.GLO_SET_RESPONSE:
            case Command.GLO_METHOD_REQUEST:
            case Command.GLO_METHOD_RESPONSE:
            case Command.GLO_EVENT_NOTIFICATION_REQUEST:
            case Command.DED_INITIATE_REQUEST:
            case Command.DED_INITIATE_RESPONSE:
            case Command.DED_GET_REQUEST:
            case Command.DED_GET_RESPONSE:
            case Command.DED_SET_REQUEST:
            case Command.DED_SET_RESPONSE:
            case Command.DED_METHOD_REQUEST:
            case Command.DED_METHOD_RESPONSE:
            case Command.DED_EVENT_NOTIFICATION:
            case Command.DED_READ_REQUEST:
            case Command.DED_READ_RESPONSE:
            case Command.DED_WRITE_REQUEST:
            case Command.DED_WRITE_RESPONSE:
            case Command.GLO_CONFIRMED_SERVICE_ERROR:
            case Command.DED_CONFIRMED_SERVICE_ERROR:
            case Command.GENERAL_SIGNING:
                break;
            default:
                throw new IllegalArgumentException("cryptedData");
            }
            int value = 0;
            KeyPair kp = null;
            PrivateKey key = null;
            PublicKey pub = null;
            GXByteBuffer transactionId = null;
            if (cmd == Command.GENERAL_CIPHERING || cmd == Command.GENERAL_SIGNING) {
                transactionId = new GXByteBuffer();
                len = GXCommon.getObjectCount(data);
                if (len != 0) {
                    GXCommon.setObjectCount(len, transactionId);
                    transactionId.set(data, len);
                    p.setTransactionId(transactionId.getUInt64());
                } else {
                    p.setTransactionId(BigInteger.ZERO);
                }
                len = GXCommon.getObjectCount(data);
                tmp = new byte[len];
                if (len != 0) {
                    data.get(tmp);
                    p.setSystemTitle(tmp);
                }
                if (p.getSystemTitle() == null || p.getSystemTitle().length != 8) {
                    if (p.getXml() == null) {
                        throw new IllegalArgumentException("Invalid sender system title.");
                    } else {
                        p.getXml().appendComment("Invalid sender system title.");
                    }
                }
                len = GXCommon.getObjectCount(data);
                tmp = new byte[len];
                data.get(tmp);
                p.setRecipientSystemTitle(tmp);
                // Get date time.
                len = GXCommon.getObjectCount(data);
                if (len != 0) {
                    tmp = new byte[len];
                    data.get(tmp);
                    p.setDateTime(tmp);
                }
                // other-information
                len = data.getUInt8();
                if (len != 0) {
                    tmp = new byte[len];
                    data.get(tmp);
                    p.setOtherInformation(tmp);
                }
                if (cmd == Command.GENERAL_CIPHERING) {
                    // KeyInfo OPTIONAL
                    // len =
                    data.getUInt8();
                    // AgreedKey CHOICE tag.
                    data.getUInt8();
                    // key-parameters
                    // len =
                    data.getUInt8();
                    value = data.getUInt8();
                    p.setKeyParameters(value);
                    if (value == KeyAgreementScheme.ONE_PASS_DIFFIE_HELLMAN.ordinal()) {
                        p.getSettings().getCipher().setSigning(Signing.ONE_PASS_DIFFIE_HELLMAN);
                        // key-ciphered-data
                        len = GXCommon.getObjectCount(data);
                        GXByteBuffer bb = new GXByteBuffer();
                        bb.set(data, len);
                        if (p.getXml() != null) {
                            p.setKeyCipheredData(bb.array());
                        }
                        kp = p.getSettings().getCipher().getKeyAgreementKeyPair();
                        if (kp == null || kp.getPublic() == null) {
                            pub = (PublicKey) p.getSettings().getKey(CertificateType.KEY_AGREEMENT, p.getSystemTitle(),
                                    false);
                            if (pub != null) {
                                p.getSettings().getCipher().setKeyAgreementKeyPair(new KeyPair(pub,
                                        p.getSettings().getCipher().getKeyAgreementKeyPair().getPrivate()));
                            }
                        }
                        if (kp.getPrivate() == null) {
                            key = (PrivateKey) p.getSettings().getKey(CertificateType.KEY_AGREEMENT, p.getSystemTitle(),
                                    true);
                            if (key != null) {
                                p.getSettings().getCipher().setKeyAgreementKeyPair(new KeyPair(
                                        p.getSettings().getCipher().getKeyAgreementKeyPair().getPublic(), key));
                            }
                        }
                        if (kp.getPublic() != null) {
                            // Get Ephemeral public key.
                            int keySize = len / 2;
                            pub = GXAsn1Converter.getPublicKey(bb.subArray(0, keySize));
                        }
                    } else if (value == KeyAgreementScheme.STATIC_UNIFIED_MODEL.ordinal()) {
                        p.getSettings().getCipher().setSigning(Signing.STATIC_UNIFIED_MODEL);
                        len = GXCommon.getObjectCount(data);
                        if (len != 0) {
                            throw new IllegalArgumentException("Invalid key parameters");
                        }
                        kp = p.getSettings().getCipher().getKeyAgreementKeyPair();
                        if (kp == null || kp.getPublic() == null) {
                            pub = (PublicKey) p.getSettings().getKey(CertificateType.KEY_AGREEMENT, p.getSystemTitle(),
                                    false);
                        } else {
                            pub = p.getSettings().getCipher().getKeyAgreementKeyPair().getPublic();
                        }
                        if (kp.getPrivate() == null) {
                            key = (PrivateKey) p.getSettings().getKey(CertificateType.KEY_AGREEMENT,
                                    p.getRecipientSystemTitle(), true);
                        } else {
                            key = p.getSettings().getCipher().getKeyAgreementKeyPair().getPrivate();
                        }
                        if (kp.getPublic() == null || kp.getPrivate() == null) {
                            kp = new KeyPair(pub, key);
                            p.getSettings().getCipher().setKeyAgreementKeyPair(kp);
                        }
                    }
                } else {
                    // Update security because server needs it and client when
                    // push message is received.
                    p.getSettings().getCipher().setSigning(Signing.GENERAL_SIGNING);
                    kp = p.getSettings().getCipher().getSigningKeyPair();
                    if (kp.getPublic() == null || kp.getPrivate() == null) {
                        if (kp.getPublic() == null) {
                            key = p.getSettings().getCipher().getSigningKeyPair().getPrivate();
                            pub = (PublicKey) p.getSettings().getKey(CertificateType.DIGITAL_SIGNATURE,
                                    p.getSystemTitle(), false);
                            kp = new KeyPair(pub, key);
                            p.getSettings().getCipher().setSigningKeyPair(kp);
                        }
                        if (kp.getPrivate() == null) {
                            pub = p.getSettings().getCipher().getSigningKeyPair().getPublic();
                            key = (PrivateKey) p.getSettings().getKey(CertificateType.DIGITAL_SIGNATURE,
                                    p.getRecipientSystemTitle(), true);
                            kp = new KeyPair(pub, key);
                            p.getSettings().getCipher().setSigningKeyPair(kp);
                        }
                    }
                }
            }
            int contentStart = data.position();
            // Content length is not add for the signed data.
            len = GXCommon.getObjectCount(data);
            if (len > data.available()) {
                throw new IllegalArgumentException("Not enought data.");
            }
            p.setCipheredContent(data.subArray(data.position(), len));
            if (cmd == Command.GENERAL_SIGNING) {
                if (!GXCommon.isCiphered(data.getUInt8())) {
                    data.position(data.position() - 1);
                    if (p.getSettings().isServer()) {
                        List list = new ArrayList();
                        list.add(SecurityPolicy.ENCRYPTED_REQUEST);
                        list.add(SecurityPolicy.AUTHENTICATED_REQUEST);
                        if ((p.getSettings().getCipher().getSecurityPolicy().containsAll(list))) {
                            throw new GXDLMSExceptionResponse(ExceptionStateError.SERVICE_NOT_ALLOWED,
                                    ExceptionServiceError.DECIPHERING_ERROR, 0);
                        }
                    }
                    return data.remaining();
                }
                len = GXCommon.getObjectCount(data);
                if (len > data.available()) {
                    throw new Exception("Not enought data.");
                }
            }

            short sc = data.getUInt8();
            p.setSecuritySuite(SecuritySuite.forValue(sc & 0x3));
            p.setSecurity(Security.forValue(sc & 0x30));
            if ((sc & 0x80) != 0) {
                LOGGER.log(Level.FINEST, "Compression is used.");
            }
            if ((sc & 0x40) != 0) {
                LOGGER.log(Level.FINEST, "Key_Set is used.");
                if (p.getXml() == null) {
                    throw new GXDLMSExceptionResponse(ExceptionStateError.SERVICE_NOT_ALLOWED,
                            ExceptionServiceError.DECIPHERING_ERROR, null);
                }
            }
            if ((sc & 0x20) != 0) {
                LOGGER.log(Level.FINEST, "Encryption is applied.");
            }
            if ((sc & 0x10) != 0) {
                LOGGER.log(Level.FINEST, "Authentication is applied.");
            }
            if (value != 0 && p.getXml() != null && kp.getPublic() == null) {
                return p.getCipheredContent();
            }
            byte[] algID = GXCommon.hexToBytes("60857405080300"); // AES-GCM-128
            if (cmd == Command.GENERAL_SIGNING && p.getSecurity() != Security.NONE) {
                if (!(p.getXml() != null && (kp.getPrivate() == null || kp.getPublic() == null))) {
                    LOGGER.log(Level.FINEST, "Private signing key: ", kp.getPrivate());
                    LOGGER.log(Level.FINEST, "Public signing key: ", kp.getPublic());
                }
            } else if (value == KeyAgreementScheme.ONE_PASS_DIFFIE_HELLMAN.ordinal()) {
                // Client generates shared secret.
                KeyAgreement ka = KeyAgreement.getInstance("ECDH");
                ka.init(kp.getPrivate());
                ka.doPhase(kp.getPublic(), true);
                byte[] z = ka.generateSecret();
                LOGGER.log(Level.FINEST, "Private agreement key {0}",
                        GXCommon.toHex(GXAsn1Converter.rawValue(key), true));
                LOGGER.log(Level.FINEST, "Public ephemeral key: {0}",
                        GXCommon.toHex(GXAsn1Converter.rawValue(pub), true));
                LOGGER.log(Level.FINEST, "Shared secret: {0}", GXCommon.toHex(z, true));
                GXByteBuffer kdf = new GXByteBuffer();
                kdf.set(GXSecure.generateKDF("SHA-256", z, 256, algID, p.getSystemTitle(), p.getRecipientSystemTitle(),
                        null, null));
                LOGGER.log(Level.FINEST, "KDF {0}", kdf);
                p.setBlockCipherKey(kdf.subArray(0, 16));
            } else if (value == KeyAgreementScheme.STATIC_UNIFIED_MODEL.ordinal()) {
                // Client generates shared secret.
                KeyAgreement ka = KeyAgreement.getInstance("ECDH");
                ka.init(p.getSettings().getCipher().getKeyAgreementKeyPair().getPrivate());
                ka.doPhase(kp.getPublic(), true);
                byte[] z = ka.generateSecret();
                LOGGER.log(Level.FINEST, "Private Agreement key: {0}", GXDLMSTranslator.toHex(
                        GXAsn1Converter.rawValue(p.getSettings().getCipher().getKeyAgreementKeyPair().getPrivate())));
                LOGGER.log(Level.FINEST, "Public Agreement key: {0}",
                        GXDLMSTranslator.toHex(GXAsn1Converter.rawValue(pub)));
                LOGGER.log(Level.FINEST, "Shared secret {0}", GXCommon.toHex(z, true));
                GXByteBuffer kdf = new GXByteBuffer();
                kdf.set(GXSecure.generateKDF("SHA-256", z, 256, algID, p.getSystemTitle(), transactionId.array(),
                        p.getRecipientSystemTitle(), null));

                LOGGER.log(Level.FINEST, "KDF {0}", kdf);
                LOGGER.log(Level.FINEST, "Authentication key {0}", GXDLMSTranslator.toHex(p.getAuthenticationKey()));
                // Get Ephemeral signing key.
                p.setBlockCipherKey(kdf.subArray(0, 16));
            }
            if (p.getXml() == null && value != 0 && kp.getPublic() == null) {
                throw new IllegalArgumentException("Invalid Key-id value.");
            }
            long invocationCounter = 0;
            if (p.getSecurity() != Security.NONE) {
                invocationCounter = data.getUInt32();
            }
            if (p.getSettings().getInvocationCounter() != null
                    && p.getSettings().getInvocationCounter().getValue() instanceof Number) {
                {
                    if (invocationCounter < ((Number) p.getSettings().getInvocationCounter().getValue()).longValue()) {
                        throw new GXDLMSExceptionResponse(ExceptionStateError.SERVICE_NOT_ALLOWED,
                                ExceptionServiceError.INVOCATION_COUNTER_ERROR,
                                p.getSettings().getInvocationCounter().getValue());
                    }
                    // Update IC value.
                    p.getSettings().getInvocationCounter().setValue(invocationCounter);
                }
            }
            p.setInvocationCounter(invocationCounter);
            if (p.getAuthenticationKey() == null || p.getBlockCipherKey() == null) {
                if (p.getSettings().getCryptoNotifier() == null) {
                    throw new Exception("Failed to get the block cipher key.");
                }
                GXCryptoKeyParameter args = new GXCryptoKeyParameter();
                args.setInvocationCounter(invocationCounter);
                args.setSystemTitle(p.getSystemTitle());
                if (p.getBlockCipherKey() == null) {
                    args.setKeyType(CryptoKeyType
                            .forValue(args.getKeyType().getValue() | CryptoKeyType.BLOCK_CIPHER.getValue()));
                }
                if (p.getAuthenticationKey() == null) {
                    args.setKeyType(CryptoKeyType
                            .forValue(args.getKeyType().getValue() | CryptoKeyType.AUTHENTICATION.getValue()));
                }
                p.getSettings().getCryptoNotifier().onKey(p.getSettings().getCryptoNotifier(), args);
                if (p.getBlockCipherKey() == null) {
                    if (args.getBlockCipherKey() == null
                            || (args.getBlockCipherKey().length != 16 && args.getBlockCipherKey().length != 32)) {
                        throw new Exception("Invalid Block cipher key.");
                    }
                    p.setBlockCipherKey(args.getBlockCipherKey());
                }
                if (p.getAuthenticationKey() == null) {
                    if (args.getAuthenticationKey() == null
                            || (args.getAuthenticationKey().length != 16 && args.getAuthenticationKey().length != 32)) {
                        throw new Exception("Invalid authentication key.");
                    }
                    p.setAuthenticationKey(args.getAuthenticationKey());
                }
            }
            LOGGER.log(Level.FINEST, "Decrypt settings {0}", p.toString());
            LOGGER.log(Level.FINEST, "Encrypted {0}",
                    GXCommon.toHex(data.getData(), false, data.position(), data.available()));
            byte[] tag = null;
            if (p.getSecurity() != Security.ENCRYPTION) {
                tag = new byte[12];
            }
            byte[] encryptedData;
            int length;
            if (p.getSecurity() == Security.AUTHENTICATION) {
                try {
                    encryptedData = encryptAesGcm(false, p, data.remaining());
                } catch (AEADBadTagException e) {
                    LOGGER.log(Level.SEVERE, "Decrypt failed. Invalid authentication tag.");
                    encryptedData = null;
                    if (p.getXml() == null) {
                        throw e;
                    } else {
                        p.getXml().appendComment("Decrypt failed. Invalid authentication tag.");
                    }
                }
                return encryptedData;
            }
            byte[] ciphertext = null;
            if (p.getSecurity() == Security.ENCRYPTION) {
                length = len - 5;
                ciphertext = new byte[length];
                data.get(ciphertext);
            } else if (p.getSecurity() == Security.AUTHENTICATION_ENCRYPTION) {
                length = len - 12 - 5;
                ciphertext = new byte[length];
                data.get(ciphertext);
                data.get(tag);
            }
            byte[] decrypted;
            // Data might be without ciphering in GeneralSigning.
            if (p.getSecurity() != Security.NONE) {
                GXByteBuffer bb = new GXByteBuffer();
                bb.set(ciphertext);
                bb.set(tag);
                decrypted = encryptAesGcm(false, p, bb.array());
                LOGGER.log(Level.FINEST, "Decrypted: {0}", GXCommon.toHex(decrypted, true));
            } else {
                length = len;
                decrypted = new byte[length];
                data.get(decrypted);
            }
            if (cmd == Command.GENERAL_SIGNING) {
                // Content length is not add for the signed data.
                GXByteBuffer signedData = new GXByteBuffer();
                signedData.set(data.getData(), 1, contentStart - 1);
                signedData.set(p.getCipheredContent());
                len = GXCommon.getObjectCount(data);
                byte[] s = new byte[len];
                data.get(s);
                p.setSignature(s);
                LOGGER.log(Level.FINEST, "Verifying signature for sender: {0}",
                        GXCommon.toHex(p.getSystemTitle(), true));
                if (p.getXml() == null) {
                    if (kp.getPublic() == null) {
                        throw new IllegalArgumentException("Public key is not set.");
                    }
                    if (!validateEphemeralPublicKeySignature(signedData.array(), p.getSignature(), kp.getPublic())) {
                        throw new Exception("Invalid signature.");
                    }
                } else {
                    if (kp.getPublic() == null && kp.getPrivate() == null) {
                        p.getXml().appendComment("Failed to verify signed data. Public key is not set.");
                    } else {
                        if (!validateEphemeralPublicKeySignature(signedData.array(), p.getSignature(),
                                kp.getPublic())) {
                            p.getXml().appendComment("Failed to verify signed data. Invalid signature.");
                        }
                    }
                }
            }
            return decrypted;
        } catch (AEADBadTagException ex) {
            throw ex;
        } catch (Exception ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage());
            throw new GXDLMSExceptionResponse(ExceptionStateError.SERVICE_NOT_ALLOWED,
                    ExceptionServiceError.DECIPHERING_ERROR, null);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy