]> git.basschouten.com Git - openhab-addons.git/commitdiff
[knx] Add support for KNX IP Secure (#12709)
authorHolger Friedrich <holgerfriedrich@users.noreply.github.com>
Mon, 8 Aug 2022 14:55:41 +0000 (16:55 +0200)
committerGitHub <noreply@github.com>
Mon, 8 Aug 2022 14:55:41 +0000 (16:55 +0200)
* [knx] Add support for KNX IP Secure

* add support for KNX IP Secure, new options SECURETUNNEL and
  SECUREROUTER, refers to #8872
* add config options for credentials for secure connections
* update user documentation
* add test cases

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
14 files changed:
bundles/org.openhab.binding.knx/README.md
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/DeviceInfoClientImpl.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/IPBridgeConfiguration.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java
bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java
bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties
bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml
bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandlerTest.java [new file with mode: 0644]

index 2c4b8c564791c23924bbe9f593a1ea4e876f6656..7b448f1be71b629990091b202ad808c6f978aff5 100644 (file)
@@ -29,7 +29,7 @@ The IP Gateway is the most commonly used way to connect to the KNX bus. At its b
 
 | Name                | Required     | Description                                                                                                  | Default value                                        |
 |---------------------|--------------|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------|
-| type                | Yes          | The IP connection type for connecting to the KNX bus (`TUNNEL` or `ROUTER`)                                  | -                                                    |
+| type                | Yes          | The IP connection type for connecting to the KNX bus (`TUNNEL`, `ROUTER`, `SECURETUNNEL` or `SECUREROUTER`)  | -                                                    |
 | ipAddress           | for `TUNNEL` | Network address of the KNX/IP gateway. If type `ROUTER` is set, the IPv4 Multicast Address can be set.       | for `TUNNEL`: \<nothing\>, for `ROUTER`: 224.0.23.12 |
 | portNumber          | for `TUNNEL` | Port number of the KNX/IP gateway                                                                            | 3671                                                 |
 | localIp             | No           | Network address of the local host to be used to set up the connection to the KNX/IP gateway                  | the system-wide configured primary interface address |
@@ -39,6 +39,10 @@ The IP Gateway is the most commonly used way to connect to the KNX bus. At its b
 | responseTimeout     | No           | Timeout in seconds to wait for a response from the KNX bus                                                   | 10                                                   |
 | readRetriesLimit    | No           | Limits the read retries while initialization from the KNX bus                                                | 3                                                    |
 | autoReconnectPeriod | No           | Seconds between connect retries when KNX link has been lost (0 means never).                                 | 0                                                    |
+| routerBackboneKey   | No           | KNX secure: Backbone key for secure router mode                                                              | -                                                    |
+| tunnelUserId        | No           | KNX secure: Tunnel user id for secure tunnel mode (if specified, it must be a number >0)                     | -                                                    |
+| tunnelUserPassword  | No           | KNX secure: Tunnel user key for secure tunnel mode                                                           | -                                                    |
+| tunnelDeviceAuthentication  | No   | KNX secure: Tunnel device authentication for secure tunnel mode                                              | -                                                    |
 
 
 ### Serial Gateway
@@ -208,6 +212,35 @@ Each configuration parameter has a `mainGA` where commands are written to and op
 The `dpt` element is optional. If omitted, the corresponding default value will be used (see the channel descriptions above).
 
 
+## KNX Secure
+
+> NOTE: Support for KNX Secure is partly implemented for openHAB and should be considered as experimental.
+
+### KNX IP Secure
+
+KNX IP Secure protects the traffic between openHAB and your KNX installation.
+It **requires a KNX Secure Router or a Secure IP Interface** and a KNX installation **with security features enabled in ETS tool**.
+
+For *Secure routing* mode, the so called `backbone key` needs to be configured in openHAB.
+It is created by the ETS tool and cannot be changed via the ETS user interface.
+
+- The backbone key can be extracted from Security report (ETS, Reports, Security, look for a 32-digit key) and specified in parameter `routerBackboneKey`.
+
+For *Secure tunneling* with a Secure IP Interface (or a router in tunneling mode), more parameters are required.
+A unique device authentication key, and a specific tunnel identifier and password need to be available.
+
+- All information can be looked up in ETS and provided separately: `tunnelDeviceAuthentication`, `tunnelUserPassword`.
+`tunnelUserId` is a number which is not directly visible in ETS, but can be looked up in keyring export or deduced (typically 2 for the first tunnel of a device, 3 for the second one, ...).
+`tunnelUserPasswort` is set in ETS in the properties of the tunnel (below the IP interface you will see the different tunnels listed) denoted as "Password". `tunnelDeviceAuthentication` is set in the properties of the IP interface itself, check for a tab "IP" and a description "Authentication Code".
+
+### KNX Data Secure
+
+KNX Data Secure protects the content of messages on the KNX bus. In a KNX installation, both classic and secure group addresses can coexist.
+Data Secure does _not_ necessarily require a KNX Secure Router or a Secure IP Interface, but a KNX installation with newer KNX devices which support Data Secure and with **security features enabled in ETS tool**.
+
+> NOTE: **openHAB currently ignores messages with secure group addresses.**
+
+
 ## Examples
 
 The following two templates are sufficient for almost all purposes.
index 00dfb907fe099b136843121846cbf328d6e4f3bf..1fa777fb63a2c4ed1089a148548a5d918c2c5a06 100644 (file)
@@ -54,6 +54,10 @@ public class KNXBindingConstants {
     public static final String PORT_NUMBER = "portNumber";
     public static final String SERIAL_PORT = "serialPort";
     public static final String USE_CEMI = "useCemi";
+    public static final String ROUTER_BACKBONE_GROUP_KEY = "routerBackboneGroupKey";
+    public static final String TUNNEL_USER_ID = "tunnelUserId";
+    public static final String TUNNEL_USER_PASSWORD = "tunnelUserPassword";
+    public static final String TUNNEL_DEVICE_AUTHENTICATION = "tunnelDeviceAuthentication";
 
     // The default multicast ip address (see <a
     // href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP
index ecc905fd68e0561de3442e6b8e019617cfb24217..397d777fe8870d6e09740ffe03cdd6b953b202ee 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.knx.internal.client;
 
 import java.time.Duration;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.CopyOnWriteArraySet;
@@ -40,6 +41,7 @@ import tuwien.auto.calimero.FrameEvent;
 import tuwien.auto.calimero.GroupAddress;
 import tuwien.auto.calimero.IndividualAddress;
 import tuwien.auto.calimero.KNXException;
+import tuwien.auto.calimero.KNXIllegalArgumentException;
 import tuwien.auto.calimero.datapoint.CommandDP;
 import tuwien.auto.calimero.datapoint.Datapoint;
 import tuwien.auto.calimero.device.ProcessCommunicationResponder;
@@ -55,6 +57,7 @@ import tuwien.auto.calimero.process.ProcessCommunicator;
 import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
 import tuwien.auto.calimero.process.ProcessEvent;
 import tuwien.auto.calimero.process.ProcessListener;
+import tuwien.auto.calimero.secure.KnxSecureException;
 import tuwien.auto.calimero.secure.SecureApplicationLayer;
 import tuwien.auto.calimero.secure.Security;
 
@@ -66,6 +69,14 @@ import tuwien.auto.calimero.secure.Security;
  */
 @NonNullByDefault
 public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClient {
+    public enum ClientState {
+        INIT,
+        RUNNING,
+        INTERRUPTED,
+        DISPOSE
+    }
+
+    private ClientState state = ClientState.INIT;
 
     private static final int MAX_SEND_ATTEMPTS = 2;
 
@@ -146,7 +157,11 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
 
     private boolean scheduleReconnectJob() {
         if (autoReconnectPeriod > 0) {
-            connectJob = knxScheduler.schedule(this::connect, autoReconnectPeriod, TimeUnit.SECONDS);
+            // schedule connect job, for the first connection ignore autoReconnectPeriod and use 1 sec
+            final long reconnectDelayS = (state == ClientState.INIT) ? 1 : autoReconnectPeriod;
+            final String prefix = (state == ClientState.INIT) ? "re" : "";
+            logger.debug("Bridge {} scheduling {}connect in {}s", thingUID, prefix, reconnectDelayS);
+            connectJob = knxScheduler.schedule(this::connect, reconnectDelayS, TimeUnit.SECONDS);
             return true;
         } else {
             return false;
@@ -154,7 +169,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
     }
 
     private void cancelReconnectJob() {
-        ScheduledFuture<?> currentReconnectJob = connectJob;
+        final ScheduledFuture<?> currentReconnectJob = connectJob;
         if (currentReconnectJob != null) {
             currentReconnectJob.cancel(true);
             connectJob = null;
@@ -171,55 +186,111 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
     }
 
     private synchronized boolean connect() {
+        if (state == ClientState.INIT) {
+            state = ClientState.RUNNING;
+        } else if (state == ClientState.DISPOSE) {
+            logger.trace("connect() ignored, closing down");
+            return false;
+        }
+
         if (isConnected()) {
             return true;
         }
         try {
+            // We have a valid "connection" object, this is ensured by IPClient.java.
+            // "releaseConnection" is actually removing all registered users of this connection and stopping
+            // all threads.
+            // Note that this will also kill this function in the following call to sleep in case of a
+            // connection loss -> restart is via triggered via scheduledReconnect in handler for InterruptedException.
             releaseConnection();
+            Thread.sleep(1000);
+            logger.debug("Bridge {} is connecting to KNX bus", thingUID);
 
-            logger.debug("Bridge {} is connecting to the KNX bus", thingUID);
-
+            // now establish (possibly encrypted) connection, according to settings (tunnel, routing, secure...)
             KNXNetworkLink link = establishConnection();
             this.link = link;
 
+            // ManagementProcedures provided by Calimero: allow managing other KNX devices, e.g. check if an address is
+            // reachable.
+            // Note for KNX Secure: ManagmentProcedueresImpl currently does not provide a ctor with external SAL,
+            // it internally creates an instance of ManagementClientImpl, which uses
+            // Security.defaultInstallation().deviceToolKeys()
+            // Protected ctor using given ManagementClientImpl is avalable (custom class to be inherited)
             managementProcedures = new ManagementProceduresImpl(link);
 
+            // ManagementClient provided by Calimero: allow reading device info, etc.
+            // Note for KNX Secure: ManagementClientImpl does not provide a ctor with external SAL in Calimero 2.5,
+            // is uses global Security.defaultInstalltion().deviceToolKeys()
+            // Current main branch includes a protected ctor (custom class to be inherited)
+            // TODO Calimero>2.5: check if there is a new way to provide security info, there is a new protected ctor
+            // TODO check if we can avoid creating another ManagementClient and re-use this from ManagemntProcedures
             ManagementClient managementClient = new ManagementClientImpl(link);
             managementClient.responseTimeout(Duration.ofSeconds(responseTimeout));
             this.managementClient = managementClient;
 
+            // OH helper for reading device info, based on managementClient above
             deviceInfoClient = new DeviceInfoClientImpl(managementClient);
 
+            // ProcessCommunicator provides main KNX communication (Calimero).
+            // Note for KNX Secure: SAL to be provided
             ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link);
             processCommunicator.responseTimeout(Duration.ofSeconds(responseTimeout));
             processCommunicator.addProcessListener(processListener);
             this.processCommunicator = processCommunicator;
 
+            // ProcessCommunicationResponder provides responses to requests from KNX bus (Calimero).
+            // Note for KNX Secure: SAL to be provided
             ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link,
                     new SecureApplicationLayer(link, Security.defaultInstallation()));
             this.responseCommunicator = responseCommunicator;
 
+            // register this class, callbacks will be triggered
             link.addLinkListener(this);
 
+            // create a job carrying out read requests
             busJob = knxScheduler.scheduleWithFixedDelay(() -> readNextQueuedDatapoint(), 0, readingPause,
                     TimeUnit.MILLISECONDS);
 
             statusUpdateCallback.updateStatus(ThingStatus.ONLINE);
             connectJob = null;
+
+            logger.info("Bridge {} connected to KNX bus", thingUID);
+
+            state = ClientState.RUNNING;
             return true;
-        } catch (KNXException | InterruptedException e) {
-            logger.debug("Error connecting to the bus: {}", e.getMessage(), e);
+        } catch (InterruptedException e) {
+            final var lastState = state;
+            state = ClientState.INTERRUPTED;
+
+            logger.trace("Bridge {}, connection interrupted", thingUID);
+
+            disconnect(e);
+            if (lastState != ClientState.DISPOSE) {
+                scheduleReconnectJob();
+            }
+
+            return false;
+        } catch (KNXException | KnxSecureException e) {
+            logger.debug("Bridge {} cannot connect: {}", thingUID, e.getMessage());
             disconnect(e);
             scheduleReconnectJob();
             return false;
+        } catch (KNXIllegalArgumentException e) {
+            logger.debug("Bridge {} cannot connect: {}", thingUID, e.getMessage());
+            disconnect(e, Optional.of(ThingStatusDetail.CONFIGURATION_ERROR));
+            return false;
         }
     }
 
     private void disconnect(@Nullable Exception e) {
+        disconnect(e, Optional.empty());
+    }
+
+    private synchronized void disconnect(@Nullable Exception e, Optional<ThingStatusDetail> detail) {
         releaseConnection();
         if (e != null) {
-            String message = e.getLocalizedMessage();
-            statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+            final String message = e.getLocalizedMessage();
+            statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, detail.orElse(ThingStatusDetail.COMMUNICATION_ERROR),
                     message != null ? message : "");
         } else {
             statusUpdateCallback.updateStatus(ThingStatus.OFFLINE);
@@ -227,22 +298,27 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
     }
 
     @SuppressWarnings("null")
-    private void releaseConnection() {
-        logger.debug("Bridge {} is disconnecting from the KNX bus", thingUID);
-        readDatapoints.clear();
+    protected void releaseConnection() {
+        logger.debug("Bridge {} is disconnecting from KNX bus", thingUID);
+        var tmplink = link;
+        if (tmplink != null) {
+            link.removeLinkListener(this);
+        }
         busJob = nullify(busJob, j -> j.cancel(true));
-        deviceInfoClient = null;
-        managementProcedures = nullify(managementProcedures, mp -> mp.detach());
-        managementClient = nullify(managementClient, mc -> mc.detach());
-        link = nullify(link, l -> l.close());
-        processCommunicator = nullify(processCommunicator, pc -> {
-            pc.removeProcessListener(processListener);
-            pc.detach();
-        });
+        readDatapoints.clear();
         responseCommunicator = nullify(responseCommunicator, rc -> {
             rc.removeProcessListener(processListener);
             rc.detach();
         });
+        processCommunicator = nullify(processCommunicator, pc -> {
+            pc.removeProcessListener(processListener);
+            pc.detach();
+        });
+        deviceInfoClient = null;
+        managementClient = nullify(managementClient, mc -> mc.detach());
+        managementProcedures = nullify(managementProcedures, mp -> mp.detach());
+        link = nullify(link, l -> l.close());
+        logger.trace("Bridge {} disconnected from KNX bus", thingUID);
     }
 
     private <T> @Nullable T nullify(T target, @Nullable Consumer<T> lastWill) {
@@ -276,6 +352,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
         return typeHelper.toDPTValue(type, dpt);
     }
 
+    // datapoint is null at end of the list, warning is misleading
     @SuppressWarnings("null")
     private void readNextQueuedDatapoint() {
         if (!connectIfNotAutomatic()) {
@@ -316,6 +393,8 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
     }
 
     public void dispose() {
+        state = ClientState.DISPOSE;
+
         cancelReconnectJob();
         disconnect(null);
     }
@@ -420,7 +499,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
         ProcessCommunicator processCommunicator = this.processCommunicator;
         KNXNetworkLink link = this.link;
         if (processCommunicator == null || link == null) {
-            logger.debug("Cannot write to the KNX bus (processCommuicator: {}, link: {})",
+            logger.debug("Cannot write to KNX bus (processCommuicator: {}, link: {})",
                     processCommunicator == null ? "Not OK" : "OK",
                     link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
             return;
@@ -439,7 +518,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
         ProcessCommunicationResponder responseCommunicator = this.responseCommunicator;
         KNXNetworkLink link = this.link;
         if (responseCommunicator == null || link == null) {
-            logger.debug("Cannot write to the KNX bus (responseCommunicator: {}, link: {})",
+            logger.debug("Cannot write to KNX bus (responseCommunicator: {}, link: {})",
                     responseCommunicator == null ? "Not OK" : "OK",
                     link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
             return;
@@ -475,10 +554,10 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
                 break;
             } catch (KNXException e) {
                 if (i < MAX_SEND_ATTEMPTS - 1) {
-                    logger.debug("Value '{}' could not be sent to the KNX bus using datapoint '{}': {}. Will retry.",
-                            type, datapoint, e.getLocalizedMessage());
+                    logger.debug("Value '{}' could not be sent to KNX bus using datapoint '{}': {}. Will retry.", type,
+                            datapoint, e.getLocalizedMessage());
                 } else {
-                    logger.warn("Value '{}' could not be sent to the KNX bus using datapoint '{}': {}. Giving up now.",
+                    logger.warn("Value '{}' could not be sent to KNX bus using datapoint '{}': {}. Giving up now.",
                             type, datapoint, e.getLocalizedMessage());
                     throw e;
                 }
index 266b5231f2aa377bfc658e76595cba9e890dbcad..350cce8c1929efbe756c8b1cefd3c266ec05096a 100644 (file)
@@ -30,6 +30,7 @@ import tuwien.auto.calimero.link.medium.KNXMediumSettings;
 public class CustomKNXNetworkLinkIP extends KNXNetworkLinkIP {
 
     public static final int TUNNELING = KNXNetworkLinkIP.TUNNELING;
+    public static final int TUNNELINGV2 = KNXNetworkLinkIP.TunnelingV2;
     public static final int ROUTING = KNXNetworkLinkIP.ROUTING;
 
     CustomKNXNetworkLinkIP(final int serviceMode, KNXnetIPConnection conn, KNXMediumSettings settings)
index 993c510aa307c27a2aa839e9f0ecd65afb52fd19..7a03af8700eb767491a1f6d8055afc83c82239c7 100644 (file)
@@ -61,6 +61,11 @@ public class DeviceInfoClientImpl implements DeviceInfoClient {
                 return result;
             } catch (KNXException e) {
                 logger.debug("Could not {} of {}: {}", task, address, e.getMessage());
+                try {
+                    // avoid trashing the log on connection loss
+                    Thread.sleep(1000);
+                } catch (InterruptedException ignored) {
+                }
             } catch (InterruptedException e) {
                 logger.trace("Interrupted to {}", task);
                 return null;
index 045fefadb63fe8d8a337729fadbbfb069b7d5cc9..7689476c1dc810687cfec7457621aacfa4ab3e6f 100644 (file)
@@ -17,6 +17,7 @@ import java.net.InetSocketAddress;
 import java.net.NetworkInterface;
 import java.net.SocketException;
 import java.net.UnknownHostException;
+import java.time.Duration;
 import java.util.concurrent.ScheduledExecutorService;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -32,6 +33,9 @@ import tuwien.auto.calimero.knxnetip.KNXnetIPConnection;
 import tuwien.auto.calimero.knxnetip.KNXnetIPRouting;
 import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel;
 import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel.TunnelingLayer;
+import tuwien.auto.calimero.knxnetip.SecureConnection;
+import tuwien.auto.calimero.knxnetip.TcpConnection;
+import tuwien.auto.calimero.knxnetip.TcpConnection.SecureSession;
 import tuwien.auto.calimero.link.KNXNetworkLink;
 import tuwien.auto.calimero.link.KNXNetworkLinkIP;
 import tuwien.auto.calimero.link.medium.KNXMediumSettings;
@@ -46,23 +50,43 @@ import tuwien.auto.calimero.link.medium.TPSettings;
 @NonNullByDefault
 public class IPClient extends AbstractKNXClient {
 
+    public enum IpConnectionType {
+        TUNNEL,
+        ROUTER,
+        SECURE_TUNNEL,
+        SECURE_ROUTER
+    };
+
     private final Logger logger = LoggerFactory.getLogger(IPClient.class);
 
     private static final String MODE_ROUTER = "ROUTER";
     private static final String MODE_TUNNEL = "TUNNEL";
+    private static final String MODE_SECURE_ROUTER = "SECURE ROUTER";
+    private static final String MODE_SECURE_TUNNEL = "SECURE TUNNEL";
+    private static final long PAUSE_ON_TCP_SESSION_CLOSE_MS = 1000;
 
-    private final int ipConnectionType;
+    private final IpConnectionType ipConnectionType;
     private final String ip;
     private final String localSource;
     private final int port;
     @Nullable
     private final InetSocketAddress localEndPoint;
     private final boolean useNAT;
+    private final byte[] secureRoutingBackboneGroupKey;
+    private final long secureRoutingLatencyToleranceMs;
+    private final byte[] secureTunnelDevKey;
+    private final int secureTunnelUser;
+    private final byte[] secureTunnelUserKey;
+    private final ThingUID thingUID;
+
+    @Nullable
+    SecureSession tcpSession;
 
-    public IPClient(int ipConnectionType, String ip, String localSource, int port,
-            @Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod, ThingUID thingUID,
-            int responseTimeout, int readingPause, int readRetriesLimit, ScheduledExecutorService knxScheduler,
-            StatusUpdateCallback statusUpdateCallback) {
+    public IPClient(IpConnectionType ipConnectionType, String ip, String localSource, int port,
+            @Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod,
+            byte[] secureRoutingBackboneGroupKey, long secureRoutingLatencyToleranceMs, byte[] secureTunnelDevKey,
+            int secureTunnelUser, byte[] secureTunnelUserKey, ThingUID thingUID, int responseTimeout, int readingPause,
+            int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback) {
         super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler,
                 statusUpdateCallback);
         this.ipConnectionType = ipConnectionType;
@@ -71,6 +95,13 @@ public class IPClient extends AbstractKNXClient {
         this.port = port;
         this.localEndPoint = localEndPoint;
         this.useNAT = useNAT;
+        this.secureRoutingBackboneGroupKey = secureRoutingBackboneGroupKey;
+        this.secureRoutingLatencyToleranceMs = secureRoutingLatencyToleranceMs;
+        this.secureTunnelDevKey = secureTunnelDevKey;
+        this.secureTunnelUser = secureTunnelUser;
+        this.secureTunnelUserKey = secureTunnelUserKey;
+        this.thingUID = thingUID;
+        tcpSession = null;
     }
 
     @Override
@@ -82,23 +113,44 @@ public class IPClient extends AbstractKNXClient {
     }
 
     private String connectionTypeToString() {
-        return ipConnectionType == CustomKNXNetworkLinkIP.ROUTING ? MODE_ROUTER : MODE_TUNNEL;
+        if (ipConnectionType == IpConnectionType.ROUTER) {
+            return MODE_ROUTER;
+        }
+        if (ipConnectionType == IpConnectionType.TUNNEL) {
+            return MODE_TUNNEL;
+        }
+        if (ipConnectionType == IpConnectionType.SECURE_ROUTER) {
+            return MODE_SECURE_ROUTER;
+        }
+        if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
+            return MODE_SECURE_TUNNEL;
+        }
+        return "unknown connection type";
     }
 
-    private KNXNetworkLinkIP createKNXNetworkLinkIP(int serviceMode, @Nullable InetSocketAddress localEP,
-            @Nullable InetSocketAddress remoteEP, boolean useNAT, KNXMediumSettings settings)
-            throws KNXException, InterruptedException {
+    private KNXNetworkLinkIP createKNXNetworkLinkIP(IpConnectionType ipConnectionType,
+            @Nullable InetSocketAddress localEP, @Nullable InetSocketAddress remoteEP, boolean useNAT,
+            KNXMediumSettings settings) throws KNXException, InterruptedException {
+        // Calimero service mode, ROUTING for both classic and secure routing
+        int serviceMode = CustomKNXNetworkLinkIP.ROUTING;
+        if (ipConnectionType == IpConnectionType.TUNNEL) {
+            serviceMode = CustomKNXNetworkLinkIP.TUNNELING;
+        } else if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
+            serviceMode = CustomKNXNetworkLinkIP.TUNNELINGV2;
+        }
+
         // creating the connection here as a workaround for
         // https://github.com/calimero-project/calimero-core/issues/57
-        KNXnetIPConnection conn = getConnection(serviceMode, localEP, remoteEP, useNAT);
+        KNXnetIPConnection conn = getConnection(ipConnectionType, localEP, remoteEP, useNAT);
         return new CustomKNXNetworkLinkIP(serviceMode, conn, settings);
     }
 
-    private KNXnetIPConnection getConnection(int serviceMode, @Nullable InetSocketAddress localEP,
+    private KNXnetIPConnection getConnection(IpConnectionType ipConnectionType, @Nullable InetSocketAddress localEP,
             @Nullable InetSocketAddress remoteEP, boolean useNAT) throws KNXException, InterruptedException {
         KNXnetIPConnection conn;
-        switch (serviceMode) {
-            case CustomKNXNetworkLinkIP.TUNNELING:
+        switch (ipConnectionType) {
+            case TUNNEL:
+            case SECURE_TUNNEL:
                 InetSocketAddress local = localEP;
                 if (local == null) {
                     try {
@@ -107,9 +159,23 @@ public class IPClient extends AbstractKNXClient {
                         throw new KNXException("no local host available");
                     }
                 }
-                conn = new KNXnetIPTunnel(TunnelingLayer.LinkLayer, local, remoteEP, useNAT);
+                if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
+                    logger.trace("creating new TCP connection");
+                    if (tcpSession != null) {
+                        logger.debug("tcpSession might still be open");
+                    }
+                    // using .clone for the keys is essential - otherwise Calimero clears the array and a reconnect will
+                    // fail
+                    tcpSession = TcpConnection.newTcpConnection(localEP, remoteEP).newSecureSession(secureTunnelUser,
+                            secureTunnelUserKey.clone(), secureTunnelDevKey.clone());
+                    conn = SecureConnection.newTunneling(TunnelingLayer.LinkLayer, tcpSession,
+                            new IndividualAddress(localSource));
+                } else {
+                    conn = new KNXnetIPTunnel(TunnelingLayer.LinkLayer, local, remoteEP, useNAT);
+                }
                 break;
-            case CustomKNXNetworkLinkIP.ROUTING:
+            case ROUTER:
+            case SECURE_ROUTER:
                 NetworkInterface netIf = null;
                 if (localEP != null && !localEP.isUnresolved()) {
                     try {
@@ -119,11 +185,39 @@ public class IPClient extends AbstractKNXClient {
                     }
                 }
                 final InetAddress mcast = remoteEP != null ? remoteEP.getAddress() : null;
-                conn = new KNXnetIPRouting(netIf, mcast);
+                if (ipConnectionType == IpConnectionType.SECURE_ROUTER) {
+                    conn = SecureConnection.newRouting(netIf, mcast, secureRoutingBackboneGroupKey,
+                            Duration.ofMillis(secureRoutingLatencyToleranceMs));
+                } else {
+                    conn = new KNXnetIPRouting(netIf, mcast);
+                }
                 break;
             default:
                 throw new KNXIllegalArgumentException("unknown service mode");
         }
         return conn;
     }
+
+    private void closeTcpConnection() {
+        final SecureSession toBeClosed = tcpSession;
+        if (toBeClosed != null) {
+            tcpSession = null;
+            logger.debug("Bridge {} closing TCP connection", thingUID);
+            try {
+                toBeClosed.close();
+                try {
+                    Thread.sleep(PAUSE_ON_TCP_SESSION_CLOSE_MS);
+                } catch (InterruptedException e) {
+                }
+            } catch (Exception e) {
+                logger.debug("closing TCP connection failed: {}", e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    protected void releaseConnection() {
+        closeTcpConnection();
+        super.releaseConnection();
+    }
 }
index 6c6b8816aab715c9f86900536e1a3a9e330b3a50..f6c77e038da10e47c6132517f3cfae87802a1819 100644 (file)
@@ -31,6 +31,10 @@ public class IPBridgeConfiguration extends BridgeConfiguration {
     private BigDecimal portNumber = BigDecimal.valueOf(0);
     private String localIp = "";
     private String localSourceAddr = "";
+    private String routerBackboneKey = "";
+    private String tunnelUserId = "";
+    private String tunnelUserPassword = "";
+    private String tunnelDeviceAuthentication = "";
 
     public Boolean getUseNAT() {
         return useNAT;
@@ -55,4 +59,20 @@ public class IPBridgeConfiguration extends BridgeConfiguration {
     public String getLocalSourceAddr() {
         return localSourceAddr;
     }
+
+    public String getRouterBackboneKey() {
+        return routerBackboneKey;
+    }
+
+    public String getTunnelUserId() {
+        return tunnelUserId;
+    }
+
+    public String getTunnelUserPassword() {
+        return tunnelUserPassword;
+    }
+
+    public String getTunnelDeviceAuthentication() {
+        return tunnelDeviceAuthentication;
+    }
 }
index 318f58f14945ae400498496f6508bb3e31884c71..27dcef9e42db7e03eade98c3fa812de2b2f95a4a 100644 (file)
@@ -162,7 +162,8 @@ public abstract class AbstractKNXThingHandler extends BaseThingHandler implement
                 }
             }
         } catch (KNXException e) {
-            logger.debug("An error occurred while testing the reachability of a thing '{}'", getThing().getUID(), e);
+            logger.debug("An error occurred while testing the reachability of a thing '{}': {}", getThing().getUID(),
+                    e.getMessage());
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
         }
     }
@@ -191,7 +192,8 @@ public abstract class AbstractKNXThingHandler extends BaseThingHandler implement
                 updateStatus(ThingStatus.ONLINE);
             }
         } catch (KNXFormatException e) {
-            logger.debug("An exception occurred while setting the individual address '{}'", config.getAddress(), e);
+            logger.debug("An exception occurred while setting the individual address '{}': {}", config.getAddress(),
+                    e.getMessage());
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
         }
         getClient().registerGroupAddressListener(this);
index ad935d9e161c9eec4e9dfd1376f982c72b55ebcd..83f0e0fc0f54c4f0253131b834e360be5949909f 100644 (file)
@@ -14,11 +14,11 @@ package org.openhab.binding.knx.internal.handler;
 
 import java.net.InetSocketAddress;
 import java.text.MessageFormat;
+import java.util.concurrent.Future;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.knx.internal.KNXBindingConstants;
-import org.openhab.binding.knx.internal.client.CustomKNXNetworkLinkIP;
 import org.openhab.binding.knx.internal.client.IPClient;
 import org.openhab.binding.knx.internal.client.KNXClient;
 import org.openhab.binding.knx.internal.client.NoOpClient;
@@ -30,6 +30,8 @@ import org.openhab.core.thing.ThingStatusDetail;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import tuwien.auto.calimero.secure.KnxSecureException;
+
 /**
  * The {@link IPBridgeThingHandler} is responsible for handling commands, which are
  * sent to one of the channels. It implements a KNX/IP Gateway, that either acts a a
@@ -43,10 +45,13 @@ import org.slf4j.LoggerFactory;
 public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
     private static final String MODE_ROUTER = "ROUTER";
     private static final String MODE_TUNNEL = "TUNNEL";
+    private static final String MODE_SECURE_ROUTER = "SECUREROUTER";
+    private static final String MODE_SECURE_TUNNEL = "SECURETUNNEL";
+    private @Nullable Future<?> initJob = null;
 
     private final Logger logger = LoggerFactory.getLogger(IPBridgeThingHandler.class);
 
-    private @Nullable IPClient client;
+    private @Nullable IPClient client = null;
     private final NetworkAddressService networkAddressService;
 
     public IPBridgeThingHandler(Bridge bridge, NetworkAddressService networkAddressService) {
@@ -56,7 +61,40 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
 
     @Override
     public void initialize() {
+        // initialisation would take too long and show a warning during binding startup
+        // KNX secure is adding serious delay
+        updateStatus(ThingStatus.UNKNOWN);
+        initJob = scheduler.submit(() -> {
+            initializeLater();
+        });
+    }
+
+    public void initializeLater() {
         IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class);
+        boolean securityAvailable = false;
+        try {
+            securityAvailable = initializeSecurity(config.getRouterBackboneKey(),
+                    config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), config.getTunnelUserPassword());
+            if (securityAvailable) {
+                logger.debug("KNX secure: router backboneGroupKey is {} set",
+                        ((secureRouting.backboneGroupKey.length == 16) ? "properly" : "not"));
+                boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16)
+                        && (secureTunnel.userKey.length == 16));
+                logger.debug("KNX secure: tunnel keys are {} set", (tunnelOk ? "properly" : "not"));
+            } else {
+                logger.debug("KNX security not configured");
+            }
+        } catch (KnxSecureException e) {
+            logger.debug("{}, {}", thing.getUID(), e.toString());
+
+            String message = e.getLocalizedMessage();
+            if (message == null) {
+                message = e.getClass().getSimpleName();
+            }
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "KNX security: " + message);
+            return;
+        }
+
         int autoReconnectPeriod = config.getAutoReconnectPeriod();
         if (autoReconnectPeriod != 0 && autoReconnectPeriod < 30) {
             logger.info("autoReconnectPeriod for {} set to {}s, allowed range is 0 (never) or >30", thing.getUID(),
@@ -70,20 +108,64 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
         String ip = config.getIpAddress();
         InetSocketAddress localEndPoint = null;
         boolean useNAT = false;
-        int ipConnectionType;
+
+        IPClient.IpConnectionType ipConnectionType;
         if (MODE_TUNNEL.equalsIgnoreCase(connectionTypeString)) {
             useNAT = config.getUseNAT();
-            ipConnectionType = CustomKNXNetworkLinkIP.TUNNELING;
+            ipConnectionType = IPClient.IpConnectionType.TUNNEL;
+        } else if (MODE_SECURE_TUNNEL.equalsIgnoreCase(connectionTypeString)) {
+            useNAT = config.getUseNAT();
+            ipConnectionType = IPClient.IpConnectionType.SECURE_TUNNEL;
+
+            if (!securityAvailable) {
+                logger.warn("Bridge {} missing security configuration for secure tunnel", thing.getUID());
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                        "Security configuration missing for secure tunnel");
+                return;
+            }
+            boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16)
+                    && (secureTunnel.userKey.length == 16));
+            if (!tunnelOk) {
+                logger.warn("Bridge {} incomplete security configuration for secure tunnel", thing.getUID());
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                        "Security configuration for secure tunnel is incomplete");
+                return;
+            }
+
+            logger.debug("KNX secure tunneling needs a few seconds to establish connection");
+            // user id, key, devAuth are already stored
         } else if (MODE_ROUTER.equalsIgnoreCase(connectionTypeString)) {
             useNAT = false;
             if (ip.isEmpty()) {
                 ip = KNXBindingConstants.DEFAULT_MULTICAST_IP;
             }
-            ipConnectionType = CustomKNXNetworkLinkIP.ROUTING;
+            ipConnectionType = IPClient.IpConnectionType.ROUTER;
+        } else if (MODE_SECURE_ROUTER.equalsIgnoreCase(connectionTypeString)) {
+            useNAT = false;
+            if (ip.isEmpty()) {
+                ip = KNXBindingConstants.DEFAULT_MULTICAST_IP;
+            }
+            ipConnectionType = IPClient.IpConnectionType.SECURE_ROUTER;
+
+            if (!securityAvailable) {
+                logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                        "Security configuration missing for secure routing");
+                return;
+            }
+            if (secureRouting.backboneGroupKey.length != 16) {
+                // failed to read shared backbone group key from config
+                logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                        "backboneGroupKey required for secure routing; please configure");
+                return;
+            }
+            logger.debug("KNX secure routing needs a few seconds to establish connection");
         } else {
-            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
-                    MessageFormat.format("Unknown IP connection type {0}. Known types are either 'TUNNEL' or 'ROUTER'",
-                            connectionTypeString));
+            logger.debug("Bridge {} unknown connection type", thing.getUID());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, MessageFormat.format(
+                    "Unknown IP connection type {0}. Known types are either 'TUNNEL', 'ROUTER', 'SECURETUNNEL', or 'SECUREROUTER'",
+                    connectionTypeString));
             return;
         }
 
@@ -95,23 +177,39 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
 
         updateStatus(ThingStatus.UNKNOWN);
         client = new IPClient(ipConnectionType, ip, localSource, port, localEndPoint, useNAT, autoReconnectPeriod,
-                thing.getUID(), config.getResponseTimeout().intValue(), config.getReadingPause().intValue(),
-                config.getReadRetriesLimit().intValue(), getScheduler(), this);
+                secureRouting.backboneGroupKey, secureRouting.latencyToleranceMs, secureTunnel.devKey,
+                secureTunnel.user, secureTunnel.userKey, thing.getUID(), config.getResponseTimeout().intValue(),
+                config.getReadingPause().intValue(), config.getReadRetriesLimit().intValue(), getScheduler(), this);
 
         final var tmpClient = client;
         if (tmpClient != null) {
             tmpClient.initialize();
         }
+
+        logger.trace("Bridge {} completed KNX scheduled initialization", thing.getUID());
     }
 
     @Override
     public void dispose() {
-        super.dispose();
+        final var tmpInitJob = initJob;
+        if (tmpInitJob != null) {
+            while (!tmpInitJob.isDone()) {
+                logger.trace("Bridge {}, shutdown during init, trying to cancel", thing.getUID());
+                tmpInitJob.cancel(true);
+                try {
+                    Thread.sleep(1000);
+                } catch (InterruptedException e) {
+                    logger.trace("Bridge {}, cancellation interrupted", thing.getUID());
+                }
+            }
+            initJob = null;
+        }
         final var tmpClient = client;
         if (tmpClient != null) {
             tmpClient.dispose();
             client = null;
         }
+        super.dispose();
     }
 
     @Override
index e9c087b4acf5a648a193159639358c84114f0e30..373caac1e2f09023c4e5afe2beef00a8208ca80b 100644 (file)
@@ -29,27 +29,135 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
 import org.openhab.core.types.Command;
 
 import tuwien.auto.calimero.IndividualAddress;
+import tuwien.auto.calimero.knxnetip.SecureConnection;
 import tuwien.auto.calimero.mgmt.Destination;
+import tuwien.auto.calimero.secure.KnxSecureException;
 
 /**
  * The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are
  * sent to one of the channels.
  *
  * @author Simon Kaufmann - Initial contribution and API
+ * @author Holger Friedrich - KNX Secure configuration
  */
 @NonNullByDefault
 public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implements StatusUpdateCallback {
 
+    public static class SecureTunnelConfig {
+        public SecureTunnelConfig() {
+            devKey = new byte[0];
+            userKey = new byte[0];
+            user = 0;
+        }
+
+        public byte[] devKey;
+        public byte[] userKey;
+        public int user = 0;
+    }
+
+    public static class SecureRoutingConfig {
+        public SecureRoutingConfig() {
+            backboneGroupKey = new byte[0];
+            latencyToleranceMs = 0;
+        }
+
+        public byte[] backboneGroupKey;
+        public long latencyToleranceMs = 0;
+    }
+
     protected ConcurrentHashMap<IndividualAddress, Destination> destinations = new ConcurrentHashMap<>();
     private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx");
     private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor();
+    protected SecureRoutingConfig secureRouting;
+    protected SecureTunnelConfig secureTunnel;
 
     public KNXBridgeBaseThingHandler(Bridge bridge) {
         super(bridge);
+        secureRouting = new SecureRoutingConfig();
+        secureTunnel = new SecureTunnelConfig();
     }
 
     protected abstract KNXClient getClient();
 
+    /***
+     * Initialize KNX secure if configured (full interface)
+     *
+     * @param cRouterBackboneGroupKey shared key for secure router mode.
+     * @param cTunnelDevAuth device password for IP interface in tunnel mode.
+     * @param cTunnelUser user id for tunnel mode. Must be an integer >0.
+     * @param cTunnelPassword user password for tunnel mode.
+     * @return
+     */
+    protected boolean initializeSecurity(String cRouterBackboneGroupKey, String cTunnelDevAuth, String cTunnelUser,
+            String cTunnelPassword) throws KnxSecureException {
+        secureRouting = new SecureRoutingConfig();
+        secureTunnel = new SecureTunnelConfig();
+
+        boolean securityInitialized = false;
+
+        // step 1: secure routing, backbone group key manually specified in OH config
+        if (!cRouterBackboneGroupKey.isBlank()) {
+            // provided in config
+            String key = cRouterBackboneGroupKey.trim().replaceFirst("^0x", "").trim().replace(" ", "");
+            if (!key.isEmpty()) {
+                // helper may throw KnxSecureException
+                secureRouting.backboneGroupKey = secHelperParseBackboneKey(key);
+                securityInitialized = true;
+            }
+        }
+
+        // step 2: check if valid tunnel parameters are specified in config
+        if (!cTunnelDevAuth.isBlank()) {
+            secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(cTunnelDevAuth.toCharArray());
+            securityInitialized = true;
+        }
+        if (!cTunnelPassword.isBlank()) {
+            secureTunnel.userKey = SecureConnection.hashUserPassword(cTunnelPassword.toCharArray());
+            securityInitialized = true;
+        }
+        if (!cTunnelUser.isBlank()) {
+            String user = cTunnelUser.trim();
+            try {
+                secureTunnel.user = Integer.decode(user);
+            } catch (NumberFormatException e) {
+                throw new KnxSecureException("tunnelUser must be a number >0");
+            }
+            if (secureTunnel.user <= 0) {
+                throw new KnxSecureException("tunnelUser must be a number >0");
+            }
+            securityInitialized = true;
+        }
+
+        // step 5: router: load latencyTolerance
+        // default to 2000ms
+        // this parameter is currently not exposed in config, it may later be set by using the keyring
+        secureRouting.latencyToleranceMs = 2000;
+
+        return securityInitialized;
+    }
+
+    /***
+     * converts hex string (32 characters) to byte[16]
+     *
+     * @param hexstring 32 characters hex
+     * @return key in byte array format
+     */
+    public static byte[] secHelperParseBackboneKey(String hexstring) throws KnxSecureException {
+        if (hexstring.length() != 32) {
+            throw new KnxSecureException("backbone key must be 32 characters (16 byte hex notation)");
+        }
+
+        byte[] parsed = new byte[16];
+        try {
+            for (byte i = 0; i < 16; i++) {
+                parsed[i] = (byte) Integer.parseInt(hexstring.substring(2 * i, 2 * i + 2), 16);
+            }
+        } catch (NumberFormatException e) {
+            throw new KnxSecureException("backbone key configured, cannot parse hex string, illegal character", e);
+        }
+        return parsed;
+    }
+
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
         // Nothing to do here
index 4ed6ab511371edc00c294b54bcd87be73d923e9f..102332033a53e274dac6dae58ff6d40b582e78c8 100644 (file)
@@ -50,8 +50,8 @@ public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler {
 
     @Override
     public void dispose() {
-        super.dispose();
         client.dispose();
+        super.dispose();
     }
 
     @Override
index 66d7d1056739bb3e13dc85ac53c1618f8e90b86e..631a9bb7adb09640c609b605b33a3a7490928aba 100644 (file)
@@ -24,6 +24,8 @@ thing-type.config.knx.device.readInterval.label = Read Interval
 thing-type.config.knx.device.readInterval.description = Interval (in seconds) between attempts to read the status group addresses on the bus
 thing-type.config.knx.ip.autoReconnectPeriod.label = Auto Reconnect Period
 thing-type.config.knx.ip.autoReconnectPeriod.description = Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s
+thing-type.config.knx.ip.group.knxsecure.label = KNX secure
+thing-type.config.knx.ip.group.knxsecure.description = Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation.
 thing-type.config.knx.ip.ipAddress.label = Network Address
 thing-type.config.knx.ip.ipAddress.description = Network address of the KNX/IP gateway
 thing-type.config.knx.ip.localIp.label = Local Network Address
@@ -38,10 +40,20 @@ thing-type.config.knx.ip.readingPause.label = Reading Pause
 thing-type.config.knx.ip.readingPause.description = Time in milliseconds of how long should be paused between two read requests to the bus during initialization
 thing-type.config.knx.ip.responseTimeout.label = Response Timeout
 thing-type.config.knx.ip.responseTimeout.description = Seconds to wait for a response from the KNX bus
+thing-type.config.knx.ip.routerBackboneKey.label = Router backbone key
+thing-type.config.knx.ip.routerBackboneKey.description = Backbone key for secure router mode. 16 bytes in hex notation. Can also be found in ETS security report.
+thing-type.config.knx.ip.tunnelDeviceAuthentication.label = Tunnel device authentication
+thing-type.config.knx.ip.tunnelDeviceAuthentication.description = Tunnel device authentication for secure tunnel mode.
+thing-type.config.knx.ip.tunnelUserId.label = Tunnel user id
+thing-type.config.knx.ip.tunnelUserId.description = Tunnel user id for secure tunnel mode.
+thing-type.config.knx.ip.tunnelUserPassword.label = Tunnel user password
+thing-type.config.knx.ip.tunnelUserPassword.description = Tunnel user key for secure tunnel mode.
 thing-type.config.knx.ip.type.label = IP Connection Type
-thing-type.config.knx.ip.type.description = The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER
+thing-type.config.knx.ip.type.description = The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or SECUREROUTER
 thing-type.config.knx.ip.type.option.TUNNEL = Tunnel
 thing-type.config.knx.ip.type.option.ROUTER = Router
+thing-type.config.knx.ip.type.option.SECURETUNNEL = Secure tunnel (experimental, use advanced options to configure)
+thing-type.config.knx.ip.type.option.SECUREROUTER = Secure router (experimental, use advanced options to configure)
 thing-type.config.knx.ip.useNAT.label = Use NAT
 thing-type.config.knx.ip.useNAT.description = Set to "true" when having network address translation between this server and the gateway
 thing-type.config.knx.serial.autoReconnectPeriod.label = Auto Reconnect Period
@@ -134,3 +146,8 @@ channel-type.config.knx.rollershutter.upDown.label = Address
 channel-type.config.knx.rollershutter.upDown.description = The group address(es) in Group Address Notation to move the shutter in the DOWN or UP direction
 channel-type.config.knx.single.ga.label = Address
 channel-type.config.knx.single.ga.description = The group address(es) in Group Address Notation
+
+# thing types config
+
+thing-type.config.knx.serial.group.knxsecure.label = KNX secure
+thing-type.config.knx.serial.group.knxsecure.description = Settings for KNX secure. Requires KNX secure features to be active in KNX installation.
index 89e2c6472f22f64a4c6216ba678224f128fce46c..a97c739c18d6bfc5684c7930c0b760d79a2d1fcf 100644 (file)
@@ -9,12 +9,19 @@
                <description>This is a KNX IP interface or router</description>
 
                <config-description>
+                       <parameter-group name="knxsecure">
+                               <label>KNX secure</label>
+                               <description>Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation.</description>
+                       </parameter-group>
                        <parameter name="type" type="text" required="true">
                                <label>IP Connection Type</label>
-                               <description>The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER</description>
+                               <description>The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or
+                                       SECUREROUTER</description>
                                <options>
                                        <option value="TUNNEL">Tunnel</option>
                                        <option value="ROUTER">Router</option>
+                                       <option value="SECURETUNNEL">Secure tunnel (experimental, use advanced options to configure)</option>
+                                       <option value="SECUREROUTER">Secure router (experimental, use advanced options to configure)</option>
                                </options>
                        </parameter>
                        <parameter name="ipAddress" type="text">
                                <description>Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s</description>
                                <default>60</default>
                        </parameter>
+                       <parameter name="routerBackboneKey" type="text" groupName="knxsecure">
+                               <context>password</context>
+                               <label>Router backbone key</label>
+                               <description>Backbone key for secure router mode. 16 bytes
+                                       in hex notation. Can also be found
+                                       in ETS security report.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="tunnelUserId" type="text" groupName="knxsecure">
+                               <label>Tunnel user id</label>
+                               <description>Tunnel user id for secure tunnel mode.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="tunnelUserPassword" type="text" groupName="knxsecure">
+                               <context>password</context>
+                               <label>Tunnel user password</label>
+                               <description>Tunnel user key for secure tunnel mode.</description>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="tunnelDeviceAuthentication" type="text" groupName="knxsecure">
+                               <context>password</context>
+                               <label>Tunnel device authentication</label>
+                               <description>Tunnel device authentication for secure tunnel mode.</description>
+                               <advanced>true</advanced>
+                       </parameter>
                </config-description>
        </bridge-type>
 
diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandlerTest.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandlerTest.java
new file mode 100644 (file)
index 0000000..f204d48
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.knx.internal.handler;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.net.NetworkAddressService;
+import org.openhab.core.thing.Bridge;
+
+import tuwien.auto.calimero.secure.KnxSecureException;
+
+/**
+ *
+ * @author Holger Friedrich - initial contribution
+ *
+ */
+@NonNullByDefault
+public class KNXBridgeBaseThingHandlerTest {
+
+    @Test
+    public void testSecurityHelpers() {
+        // now check router settings:
+        String bbKeyHex = "D947B12DDECAD528B1D5A88FD347F284";
+        byte[] bbKeyParsedLower = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex.toLowerCase());
+        byte[] bbKeyParsedUpper = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex);
+        assertEquals(16, bbKeyParsedUpper.length);
+        assertArrayEquals(bbKeyParsedUpper, bbKeyParsedLower);
+    }
+
+    @Test
+    @SuppressWarnings("null")
+    public void testInitializeSecurity() {
+        Bridge bridge = mock(Bridge.class);
+        NetworkAddressService nas = mock(NetworkAddressService.class);
+        IPBridgeThingHandler handler = new IPBridgeThingHandler(bridge, nas);
+
+        // no config given
+        assertFalse(handler.initializeSecurity("", "", "", ""));
+
+        // router password configured, length must be 16 bytes in hex notation
+        assertTrue(handler.initializeSecurity("D947B12DDECAD528B1D5A88FD347F284", "", "", ""));
+        assertTrue(handler.initializeSecurity("0xD947B12DDECAD528B1D5A88FD347F284", "", "", ""));
+        assertThrows(KnxSecureException.class, () -> {
+            handler.initializeSecurity("wrongLength", "", "", "");
+        });
+
+        // tunnel configuration
+        assertTrue(handler.initializeSecurity("", "da", "1", "pw"));
+        // cTunnelUser is restricted to a number >0
+        assertThrows(KnxSecureException.class, () -> {
+            handler.initializeSecurity("", "da", "0", "pw");
+        });
+        assertThrows(KnxSecureException.class, () -> {
+            handler.initializeSecurity("", "da", "eins", "pw");
+        });
+        // at least one setting for tunnel is given, count as try to configure secure tunnel
+        // plausibility is checked during initialize()
+        assertTrue(handler.initializeSecurity("", "da", "", ""));
+        assertTrue(handler.initializeSecurity("", "", "1", ""));
+        assertTrue(handler.initializeSecurity("", "", "", "pw"));
+    }
+}