From 6f2d61fa4e243575c9818396278927ccff13d8a7 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Mon, 8 Aug 2022 16:55:41 +0200 Subject: [PATCH] [knx] Add support for KNX IP Secure (#12709) * [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 --- bundles/org.openhab.binding.knx/README.md | 35 ++++- .../knx/internal/KNXBindingConstants.java | 4 + .../internal/client/AbstractKNXClient.java | 127 ++++++++++++++---- .../client/CustomKNXNetworkLinkIP.java | 1 + .../internal/client/DeviceInfoClientImpl.java | 5 + .../binding/knx/internal/client/IPClient.java | 126 ++++++++++++++--- .../config/IPBridgeConfiguration.java | 20 +++ .../handler/AbstractKNXThingHandler.java | 6 +- .../handler/IPBridgeThingHandler.java | 120 +++++++++++++++-- .../handler/KNXBridgeBaseThingHandler.java | 108 +++++++++++++++ .../handler/SerialBridgeThingHandler.java | 2 +- .../main/resources/OH-INF/i18n/knx.properties | 19 ++- .../src/main/resources/OH-INF/thing/ip.xml | 34 ++++- .../KNXBridgeBaseThingHandlerTest.java | 75 +++++++++++ 14 files changed, 625 insertions(+), 57 deletions(-) create mode 100644 bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandlerTest.java diff --git a/bundles/org.openhab.binding.knx/README.md b/bundles/org.openhab.binding.knx/README.md index 2c4b8c564791c..7b448f1be71b6 100644 --- a/bundles/org.openhab.binding.knx/README.md +++ b/bundles/org.openhab.binding.knx/README.md @@ -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`: \, 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. diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java index 00dfb907fe099..1fa777fb63a2c 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/KNXBindingConstants.java @@ -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 iana EIBnet/IP diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java index ecc905fd68e05..397d777fe8870 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/AbstractKNXClient.java @@ -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.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.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 @@ */ @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 void initialize() { 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 @@ private boolean scheduleReconnectJob() { } private void cancelReconnectJob() { - ScheduledFuture currentReconnectJob = connectJob; + final ScheduledFuture currentReconnectJob = connectJob; if (currentReconnectJob != null) { currentReconnectJob.cancel(true); connectJob = null; @@ -171,55 +186,111 @@ private synchronized boolean connectIfNotAutomatic() { } 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 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 @@ private void disconnect(@Nullable Exception e) { } @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 @Nullable T nullify(T target, @Nullable Consumer lastWill) { @@ -276,6 +352,7 @@ private String toDPTValue(Type type, String dpt) { 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 @@ private void readNextQueuedDatapoint() { } public void dispose() { + state = ClientState.DISPOSE; + cancelReconnectJob(); disconnect(null); } @@ -420,7 +499,7 @@ public void writeToKNX(OutboundSpec commandSpec) throws KNXException { 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 void respondToKNX(OutboundSpec responseSpec) throws KNXException { 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 @@ private void sendToKNX(ProcessCommunication communicator, KNXNetworkLink link, G 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; } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java index 266b5231f2aa3..350cce8c1929e 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomKNXNetworkLinkIP.java @@ -30,6 +30,7 @@ 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) diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/DeviceInfoClientImpl.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/DeviceInfoClientImpl.java index 993c510aa307c..7a03af8700eb7 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/DeviceInfoClientImpl.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/DeviceInfoClientImpl.java @@ -61,6 +61,11 @@ private interface ReadFunction { 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; diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java index 045fefadb63fe..7689476c1dc81 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java @@ -17,6 +17,7 @@ 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.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 @@ @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 IPClient(int ipConnectionType, String ip, String localSource, int port, 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 @@ protected KNXNetworkLink establishConnection() throws KNXException, InterruptedE } 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 @@ private KNXnetIPConnection getConnection(int serviceMode, @Nullable InetSocketAd 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 @@ private KNXnetIPConnection getConnection(int serviceMode, @Nullable InetSocketAd } } 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(); + } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/IPBridgeConfiguration.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/IPBridgeConfiguration.java index 6c6b8816aab71..f6c77e038da10 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/IPBridgeConfiguration.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/IPBridgeConfiguration.java @@ -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 String getLocalIp() { 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; + } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java index 318f58f14945a..27dcef9e42db7 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/AbstractKNXThingHandler.java @@ -162,7 +162,8 @@ private void pollDeviceStatus() { } } } 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 @@ protected void attachToClient() { 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); diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java index ad935d9e161c9..83f0e0fc0f54c 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/IPBridgeThingHandler.java @@ -14,11 +14,11 @@ 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.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 @@ 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 IPBridgeThingHandler(Bridge bridge, NetworkAddressService networkAddressS @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 void initialize() { 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 void initialize() { 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 diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java index e9c087b4acf5a..373caac1e2f09 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandler.java @@ -29,27 +29,135 @@ 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 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 diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java index 4ed6ab511371e..102332033a53e 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/SerialBridgeThingHandler.java @@ -50,8 +50,8 @@ public void initialize() { @Override public void dispose() { - super.dispose(); client.dispose(); + super.dispose(); } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties index 66d7d1056739b..631a9bb7adb09 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/i18n/knx.properties @@ -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. diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml index 89e2c6472f22f..a97c739c18d6b 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/ip.xml @@ -9,12 +9,19 @@ This is a KNX IP interface or router + + + Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation. + - The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER + The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or + SECUREROUTER + + @@ -64,6 +71,31 @@ Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s 60 + + password + + Backbone key for secure router mode. 16 bytes + in hex notation. Can also be found + in ETS security report. + true + + + + Tunnel user id for secure tunnel mode. + true + + + password + + Tunnel user key for secure tunnel mode. + true + + + password + + Tunnel device authentication for secure tunnel mode. + true + 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 index 0000000000000..f204d487d7c58 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/handler/KNXBridgeBaseThingHandlerTest.java @@ -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")); + } +}