org.opensmartgridplatform.adapter.protocol.dlms.application.services.SecurityKeyService.java Source code

Java tutorial

Introduction

Here is the source code for org.opensmartgridplatform.adapter.protocol.dlms.application.services.SecurityKeyService.java

Source

/**
 * Copyright 2017 Smart Society Services B.V.
 *
 * 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
 */
package org.opensmartgridplatform.adapter.protocol.dlms.application.services;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.opensmartgridplatform.adapter.protocol.dlms.domain.entities.DlmsDevice;
import org.opensmartgridplatform.adapter.protocol.dlms.domain.entities.SecurityKey;
import org.opensmartgridplatform.adapter.protocol.dlms.domain.entities.SecurityKeyType;
import org.opensmartgridplatform.adapter.protocol.dlms.domain.repositories.DlmsDeviceRepository;
import org.opensmartgridplatform.adapter.protocol.dlms.exceptions.ProtocolAdapterException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import org.opensmartgridplatform.shared.exceptionhandling.ComponentType;
import org.opensmartgridplatform.shared.exceptionhandling.EncrypterException;
import org.opensmartgridplatform.shared.exceptionhandling.FunctionalException;
import org.opensmartgridplatform.shared.exceptionhandling.FunctionalExceptionType;
import org.opensmartgridplatform.shared.security.EncryptionService;
import org.opensmartgridplatform.shared.security.RsaEncryptionService;

/**
 * SecurityKeyService offers a single point of entry for all code that needs to
 * deal with any of the DLMS security keys.
 * <p>
 * All code using or updating DLMS security keys for devices should use this
 * service to delegate all key handling to.
 */
@Service(value = "dlmsSecurityKeyService")
@Transactional(value = "transactionManager")
public class SecurityKeyService {

    private static final Logger LOGGER = LoggerFactory.getLogger(SecurityKeyService.class);

    public static final int AES_GMC_128_KEY_SIZE = 128;

    @Autowired
    private DlmsDeviceRepository dlmsDeviceRepository;

    @Autowired
    private EncryptionService encryptionService;

    @Autowired
    private RsaEncryptionService rsaEncryptionService;

    /**
     * Re-encrypts the given key with a secret known only inside this protocol
     * adapter.
     * <p>
     * New keys can be provided to OSGP from outside in a form encrypted with
     * the public key from an asymmetrical key pair for the platform, which is
     * available to external organizations.<br>
     * Inside the DLMS protocol adapter keys are encrypted with a faster
     * symmetrical encryption using a secret key that is not supposed to be
     * known outside this protocol adapter.
     *
     * @param externallyEncryptedKey
     *            key encrypted with the externally known public key for OSGP
     * @param keyType
     *            type of the key, for logging purposes
     * @return the key encrypted with the symmetrical secret key used only
     *         inside the DLMS protocol adapter, or an empty byte array if
     *         {@code externallyEncryptedKey == null}
     * @throws FunctionalException
     *             in case of a encryption/decryption errors while handling the
     *             key
     */
    public byte[] reEncryptKey(final byte[] externallyEncryptedKey, final SecurityKeyType keyType)
            throws FunctionalException {

        if (externallyEncryptedKey == null) {
            return new byte[0];
        }

        final byte[] key = this.rsaDecrypt(externallyEncryptedKey, keyType);
        return this.aesEncrypt(key, keyType);
    }

    private final byte[] rsaDecrypt(final byte[] externallyEncryptedKey, final SecurityKeyType keyType)
            throws FunctionalException {
        try {
            return this.rsaEncryptionService.decrypt(externallyEncryptedKey);
        } catch (final Exception e) {
            LOGGER.error("Unexpected exception during decryption", e);

            throw new FunctionalException(FunctionalExceptionType.DECRYPTION_EXCEPTION, ComponentType.PROTOCOL_DLMS,
                    new EncrypterException(
                            String.format("Unexpected exception during decryption of %s key.", keyType)));
        }
    }

    private final byte[] aesEncrypt(final byte[] key, final SecurityKeyType keyType) throws FunctionalException {
        try {
            return this.encryptionService.encrypt(key);
        } catch (final Exception e) {
            LOGGER.error("Unexpected exception during encryption", e);

            throw new FunctionalException(FunctionalExceptionType.ENCRYPTION_EXCEPTION, ComponentType.PROTOCOL_DLMS,
                    new EncrypterException(
                            String.format("Unexpected exception during encryption of %s key.", keyType)));
        }
    }

    /**
     * Decrypts the given symmetrically encrypted key.
     * <p>
     * <strong>NB:</strong> Only decrypt keys like this at the moment they are
     * required as part of the communication with a device.
     *
     * @param encryptedKey
     *            key encrypted with the symmetrical key internal to the DLMS
     *            protocol adapter.
     * @param keyType
     *            type of the key, for logging purposes
     * @return the plain key, or an empty byte array if
     *         {@code encryptedKey == null}
     */
    public byte[] decryptKey(final byte[] encryptedKey, final SecurityKeyType keyType)
            throws ProtocolAdapterException {
        if (encryptedKey == null) {
            return new byte[0];
        }
        try {
            return this.encryptionService.decrypt(encryptedKey);
        } catch (final Exception e) {
            throw new ProtocolAdapterException("Error decrypting " + keyType + " key", e);
        }
    }

    /**
     * Encrypts the given {@code plainKey} with the symmetrical secret key that
     * is internal to the DLMS protocol adapter.
     *
     * @param plainKey
     *            plain key without encryption
     * @param keyType
     *            type of the key, for logging purposes
     * @return the given key encrypted with the symmetrical key internal to the
     *         DLMS protocol adapter.
     */
    public byte[] encryptKey(final byte[] plainKey, final SecurityKeyType keyType) throws ProtocolAdapterException {
        if (plainKey == null) {
            return new byte[0];
        }
        try {
            return this.encryptionService.encrypt(plainKey);
        } catch (final Exception e) {
            throw new ProtocolAdapterException("Error encrypting " + keyType + " key", e);
        }
    }

    /**
     * Retrieves the DLMS master key (KEK) for the device with the given
     * {@code deviceIdentification}.
     * <p>
     * <strong>NB:</strong> Only retrieve keys like this at the moment they are
     * required as part of the communication with a device.
     *
     * @param deviceIdentification
     *            the identification of a DLMS device.
     * @return the key, possibly {@code null} if either the device is not found
     *         or it does not have a valid master key.
     * @throws EncrypterException
     *             if there is an error decoding the key.
     */
    public byte[] getDlmsMasterKey(final String deviceIdentification) {
        LOGGER.info("Retrieving DLMS master key for device {}", deviceIdentification);
        return this.getKey(deviceIdentification, SecurityKeyType.E_METER_MASTER);
    }

    /**
     * Retrieves the DLMS authentication key for the device with the given
     * {@code deviceIdentification}.
     * <p>
     * <strong>NB:</strong> Only retrieve keys like this at the moment they are
     * required as part of the communication with a device.
     *
     * @param deviceIdentification
     *            the identification of a DLMS device.
     * @return the key, possibly {@code null} if either the device is not found
     *         or it does not have a valid authentication key.
     * @throws EncrypterException
     *             if there is an error decoding the key.
     */
    public byte[] getDlmsAuthenticationKey(final String deviceIdentification) {
        LOGGER.info("Retrieving DLMS authentication key for device {}", deviceIdentification);
        return this.getKey(deviceIdentification, SecurityKeyType.E_METER_AUTHENTICATION);
    }

    /**
     * Retrieves the DLMS global unicast encryption key for the device with the
     * given {@code deviceIdentification}.
     * <p>
     * <strong>NB:</strong> Only retrieve keys like this at the moment they are
     * required as part of the communication with a device.
     *
     * @param deviceIdentification
     *            the identification of a DLMS device.
     * @return the key, possibly an empty byte array if either the device is not
     *         found or it does not have a valid global unicast encryption key.
     * @throws EncrypterException
     *             if there is an error decoding the key.
     */
    public byte[] getDlmsGlobalUnicastEncryptionKey(final String deviceIdentification) {
        LOGGER.info("Retrieving DLMS global unicast encryption key for device {}", deviceIdentification);
        return this.getKey(deviceIdentification, SecurityKeyType.E_METER_ENCRYPTION);
    }

    /**
     * Retrieves the M-Bus Default key for the M-Bus device with the given
     * {@code mbusDeviceIdentification}.
     * <p>
     * <strong>NB:</strong> Only retrieve keys like this at the moment they are
     * required as part of the communication with a DLMS gateway device.
     *
     * @param mbusDeviceIdentification
     *            the identification of an M-Bus device.
     * @return the key, possibly an empty byte array if either the device is not
     *         found or it does not have a valid M-Bus Default key.
     * @throws EncrypterException
     *             if there is an error decoding the key.
     */
    public byte[] getMbusDefaultKey(final String mbusDeviceIdentification) {
        LOGGER.info("Retrieving M-Bus Default key for device {}", mbusDeviceIdentification);
        return this.getKey(mbusDeviceIdentification, SecurityKeyType.G_METER_MASTER);
    }

    /**
     * Retrieves the M-Bus User key for the M-Bus device with the given
     * {@code mbusDeviceIdentification}.
     * <p>
     * <strong>NB:</strong> Only retrieve keys like this at the moment they are
     * required as part of the communication with a DLMS gateway device.
     *
     * @param mbusDeviceIdentification
     *            the identification of an M-Bus device.
     * @return the key, possibly an empty byte array if either the device is not
     *         found or it does not have a valid M-Bus User key.
     * @throws EncrypterException
     *             if the key is found, but there is an error decoding or
     *             decrypting the key.
     */
    public byte[] getMbusUserKey(final String mbusDeviceIdentification) {
        LOGGER.info("Retrieving M-Bus User key for device {}", mbusDeviceIdentification);
        return this.getKey(mbusDeviceIdentification, SecurityKeyType.G_METER_ENCRYPTION);
    }

    /**
     * Retrieves the DLMS low level security password for the device with the
     * given {@code deviceIdentification}.
     * <p>
     * <strong>NB:</strong> Only retrieve keys like this at the moment they are
     * required as part of the communication with a device.
     *
     * @param deviceIdentification
     *            the identification of a DLMS device.
     * @return the key, possibly an empty byte array if either the device is not
     *         found or it does not have a valid password.
     * @throws EncrypterException
     *             if there is an error decoding the key.
     */
    public byte[] getDlmsPassword(final String deviceIdentification) {
        LOGGER.info("Retrieving DLMS LLS Password for device {}", deviceIdentification);
        return this.getKey(deviceIdentification, SecurityKeyType.PASSWORD);
    }

    private byte[] getKey(final String deviceIdentification, final SecurityKeyType securityKeyType) {

        final DlmsDevice dlmsDevice = this.dlmsDeviceRepository.findByDeviceIdentification(deviceIdentification);
        if (dlmsDevice == null) {
            LOGGER.warn("No DlmsDevice found for identification {} - returning null as {} key.",
                    deviceIdentification, securityKeyType);
            return new byte[0];
        }

        final SecurityKey securityKey = dlmsDevice.getValidSecurityKey(securityKeyType);
        if (securityKey == null) {
            LOGGER.warn("No valid {} key found with device {} - returning null.", securityKeyType,
                    deviceIdentification);
            return new byte[0];
        }

        try {
            final byte[] encryptedKey = Hex.decodeHex(securityKey.getKey().toCharArray());
            return this.encryptionService.decrypt(encryptedKey);
        } catch (final DecoderException | FunctionalException e) {
            throw new EncrypterException("Error decoding " + securityKey + " for device " + deviceIdentification,
                    e);
        }
    }

    /**
     * Store new key
     * <p>
     * A new key is a security key with a device that does not have a valid from
     * date. This situation occurs in the process of updating a key, when the
     * new key is known, but not yet set on the device.
     * <p>
     * <strong>CAUTION:</strong> Only call this method when a successful
     * connection with the device has been set up (that is: a valid
     * communication key that works is known), and you are sure any existing new
     * key data is NOT VALID (for instance a new key stored earlier in an
     * attempt to replace the communication key that got aborted).<br>
     * <strong>This method will throw away any earlier stored new key and
     * replace it.</strong>
     * <p>
     * The moment the new key is known to be transferred to the device, make
     * sure to update its status from a new key to a valid key (and invalidating
     * any previous key) by calling
     * {@link #validateNewKey(DlmsDevice, SecurityKeyType)}.
     *
     * @see #validateNewKey(DlmsDevice, SecurityKeyType)
     * @param device
     *            DLMS device
     * @param encryptedKey
     *            key encrypted with the symmetrical key internal to the DLMS
     *            protocol adapter.
     * @param keyType
     *            type of key
     * @return saved device, with a new key of the given type
     */
    public DlmsDevice storeNewKey(final DlmsDevice device, final byte[] encryptedKey,
            final SecurityKeyType keyType) {
        this.removeEarlierStoredNewKeyIfFound(device, keyType);
        this.addNewKeyToDevice(device, encryptedKey, keyType);
        return this.dlmsDeviceRepository.save(device);
    }

    private void removeEarlierStoredNewKeyIfFound(final DlmsDevice device, final SecurityKeyType keyType) {
        final SecurityKey existingKey = device.getNewSecurityKey(keyType);
        if (existingKey != null) {
            LOGGER.warn("Removing earlier stored key in the NEW state: {}", existingKey);
            device.getSecurityKeys().remove(existingKey);
        }
    }

    private void addNewKeyToDevice(final DlmsDevice device, final byte[] encryptedKey,
            final SecurityKeyType keyType) {
        final SecurityKey newKey = new SecurityKey(device, keyType, Hex.encodeHexString(encryptedKey), null, null);
        device.addSecurityKey(newKey);
    }

    /**
     * Updates the state of a new key (having valid from date {@code null}) to
     * be considered valid (setting valid from to now).<br>
     * This invalidates any previous valid key (setting valid of the previous
     * key to now).
     * <p>
     * This method should be called to validate a new key stored with
     * {@link #storeNewKey(DlmsDevice, byte[], SecurityKeyType)} after it has
     * been confirmed to be set on the device.
     *
     * @see #storeNewKey(DlmsDevice, byte[], SecurityKeyType)
     * @param device
     *            DLMS device
     * @param keyType
     *            type of key
     * @return saved device, with a new security key that has become valid, and
     *         any previously valid security key marked as no longer valid
     * @throws ProtocolAdapterException
     *             if no new key is stored with the given device
     */
    public DlmsDevice validateNewKey(final DlmsDevice device, final SecurityKeyType keyType)
            throws ProtocolAdapterException {

        final SecurityKey newKey = this.findNewKey(device, keyType);
        final SecurityKey previousValidKey = device.getValidSecurityKey(keyType);
        return this.updateDeviceWithNewValidKey(device, previousValidKey, newKey);
    }

    private SecurityKey findNewKey(final DlmsDevice device, final SecurityKeyType keyType)
            throws ProtocolAdapterException {
        final SecurityKey newKey = device.getNewSecurityKey(keyType);
        if (newKey == null) {
            throw new ProtocolAdapterException(
                    "No new " + keyType + " key found with device " + device.getDeviceIdentification());
        }
        return newKey;
    }

    private DlmsDevice updateDeviceWithNewValidKey(final DlmsDevice device, final SecurityKey previousValidKey,
            final SecurityKey newKey) {

        final Date now = new Date();
        if (previousValidKey != null) {
            previousValidKey.setValidTo(now);
        }
        newKey.setValidFrom(now);
        return this.dlmsDeviceRepository.save(device);
    }

    /**
     * Generates a new key that can be used as DLMS master key, authentication
     * key, global unicast encryption key, M-Bus Default key or M-Bus User key.
     * <p>
     * The master keys (DLMS master or M-Bus Default) cannot be changed on a
     * device, but can be generated for use in tests or with simulated devices.
     *
     * @return a new 16-byte AES key.
     */
    public byte[] generateKey() {
        try {
            final KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(AES_GMC_128_KEY_SIZE);
            return keyGenerator.generateKey().getEncoded();
        } catch (final NoSuchAlgorithmException e) {
            throw new AssertionError("Expected AES algorithm to be available for key generation.", e);
        }
    }

    /**
     * Convenience method to generate a new key that does not need to be used
     * immediately, and return it appropriately encrypted with the secret key
     * for the DLMS protocol adapter.
     *
     * @see #generateKey()
     * @return a new encrypted key.
     */
    public byte[] generateAndEncryptKey() {
        try {
            return this.encryptionService.encrypt(this.generateKey());
        } catch (final FunctionalException e) {
            throw new EncrypterException("Error encrypting freshly generated key", e);
        }
    }

    /**
     * Encrypts a new M-Bus User key with the M-Bus Default key for use as M-Bus
     * Client Setup transfer_key parameter.
     * <p>
     * Note that the specifics of the encryption of the M-Bus User key depend on
     * the M-Bus version the devices support. This method should be appropriate
     * for use with DSMR 4 M-Bus devices.
     * <p>
     * The encryption is performed by applying an AES/CBC/NoPadding cipher
     * initialized for encryption with the given mbusDefaultKey and an
     * initialization vector of 16 zero-bytes to the given mbusUserKey.
     *
     * @return the properly wrapped User key for a DSMR 4 M-Bus User key change.
     */
    public byte[] encryptMbusUserKey(final byte[] mbusDefaultKey, final byte[] mbusUserKey)
            throws ProtocolAdapterException {

        final Key secretkeySpec = new SecretKeySpec(mbusDefaultKey, "AES");

        try {

            final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");

            final IvParameterSpec params = new IvParameterSpec(new byte[16]);
            cipher.init(Cipher.ENCRYPT_MODE, secretkeySpec, params);

            return cipher.doFinal(mbusUserKey);

        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
                | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
            final String message = "Error encrypting M-Bus User key with M-Bus Default key for transfer.";
            LOGGER.error(message, e);
            throw new ProtocolAdapterException(message);
        }
    }

    /**
     * Increments the invocation counter for the {@link SecurityKey} of the
     * given {@code keyType} with the device with the given
     * {@code deviceIdentification} based on a number of sent messages with a
     * DLMS client.
     */
    public void incrementInvocationCounter(final String deviceIdentification, final SecurityKeyType keyType,
            final int numberOfSentMessages) {

        final DlmsDevice dlmsDevice = this.dlmsDeviceRepository.findByDeviceIdentification(deviceIdentification);
        if (dlmsDevice == null) {
            LOGGER.error(
                    "No DlmsDevice found for identification {} - unable to update invocation counter for {} key.",
                    deviceIdentification, keyType);
            return;
        }

        final SecurityKey securityKey = dlmsDevice.getValidSecurityKey(keyType);
        if (securityKey == null) {
            LOGGER.error("No valid {} key found with device {} - unable to update invocation counter.", keyType,
                    deviceIdentification);
            return;
        }

        final int newInvocationCounter = securityKey.getInvocationCounter() + numberOfSentMessages;
        securityKey.setInvocationCounter(newInvocationCounter);

        this.dlmsDeviceRepository.save(dlmsDevice);
    }
}