From 334d4172400179de778baa9bea9f216647580027 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Tue, 8 Dec 2020 01:50:09 +0100 Subject: [PATCH 1/4] [knx] Allow decoding of KNX Data Secure frames * add passive (listening only) access for KNX Data Secure frames, #8872 * add config options for KNX keyring file and password * ease setup if IP Secure, as required parameters can be read from keyring * add tests for security functions * update user documentation Signed-off-by: Holger Friedrich --- bundles/org.openhab.binding.knx/README.md | 20 +- .../knx/internal/KNXBindingConstants.java | 3 + .../internal/client/AbstractKNXClient.java | 94 ++++--- .../client/CustomManagementClientImpl.java | 36 +++ .../CustomManagementProceduresImpl.java | 36 +++ .../client/CustomSecureManagement.java | 48 ++++ .../binding/knx/internal/client/IPClient.java | 5 +- .../knx/internal/client/SerialClient.java | 5 +- .../internal/config/BridgeConfiguration.java | 10 + .../config/IPBridgeConfiguration.java | 5 + .../handler/IPBridgeThingHandler.java | 21 +- .../handler/KNXBridgeBaseThingHandler.java | 251 +++++++++++++++++- .../handler/SerialBridgeThingHandler.java | 31 ++- .../main/resources/OH-INF/i18n/knx.properties | 25 +- .../src/main/resources/OH-INF/thing/ip.xml | 39 ++- .../main/resources/OH-INF/thing/serial.xml | 17 ++ .../knx/internal/client/DummyClient.java | 2 +- .../KNXBridgeBaseThingHandlerTest.java | 50 +++- .../internal/security/KNXSecurityTest.java | 250 +++++++++++++++++ .../misc/openhab6-minimal-ipif.knxkeys | 7 + .../misc/openhab6-minimal-sipif.knxkeys | 13 + .../misc/openhab6-minimal-sipr.knxkeys | 13 + .../src/test/resources/misc/openhab6.knxkeys | 35 +++ 23 files changed, 932 insertions(+), 84 deletions(-) create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomManagementClientImpl.java create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomManagementProceduresImpl.java create mode 100644 bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomSecureManagement.java create mode 100644 bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/security/KNXSecurityTest.java create mode 100644 bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-ipif.knxkeys create mode 100644 bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-sipif.knxkeys create mode 100644 bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-sipr.knxkeys create mode 100644 bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6.knxkeys diff --git a/bundles/org.openhab.binding.knx/README.md b/bundles/org.openhab.binding.knx/README.md index f366e1625fd87..21450c3657df8 100644 --- a/bundles/org.openhab.binding.knx/README.md +++ b/bundles/org.openhab.binding.knx/README.md @@ -66,6 +66,9 @@ At its base, the _ip_ bridge accepts the following configuration parameters: | 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 | - | +| keyringFile | No | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure GAs. | - | +| keyringPassword | No | KNX secure: Keyring file password (set during export from ETS) | - | +| tunnelSourceAddress | No | KNX secure: Physical KNX address of tunnel in secure mode to identify tunnel. If given, openHAB will read tunnelUserId, tunnelUserPassword, tunnelDeviceAuthentication from keyring | - | ### Serial Gateway @@ -79,6 +82,8 @@ The _serial_ bridge accepts the following configuration parameters: | readRetriesLimit | N | Limits the read retries while initialization from the KNX bus | 3 | | autoReconnectPeriod | N | Seconds between connect retries when KNX link has been lost, 0 means never retry | 0 | | useCemi | N | Use newer CEMI message format, useful for newer devices like KNX RF sticks, kBerry, etc. | false | +| keyringFile | N | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure GAs. | - | +| keyringPassword | N | KNX secure: Keyring file password (set during export from ETS) | - | ## Things @@ -452,16 +457,22 @@ It **requires a KNX Secure Router or a Secure IP Interface** and a KNX installat 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. +There are two possible ways to provide the key to openHAB: - The backbone key can be extracted from Security report (ETS, Reports, Security, look for a 32-digit key) and specified in parameter `routerBackboneKey`. +- The backbone key is included in ETS keyring export (ETS, project settings, export keyring). Keyring file is configured using `keyringFile` (put it in `config\misc` folder of the openHAB installation) and also requires `keyringPassword`. 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. +It can be provided to openHAB in two different ways: - All information can be looked up in ETS and provided separately: `tunnelDeviceAuthentication`, `tunnelUserPassword`. `tunnelUserId` is a number that 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) and denoted as "Password". `tunnelDeviceAuthentication` is set in the properties of the IP interface itself; check for the tab "IP" and the description "Authentication Code". +- All necessary information is included in ETS keyring export (ETS, project settings, export keyring). + Keyring file is configured using `keyringFile` (put it in `config\misc` folder of the openHAB installation) and `keyringPassword`. + In addition, `tunnelSourceAddress` needs to be set to uniquely identify the tunnel in use. ### KNX Data Secure @@ -469,7 +480,14 @@ 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 that support Data Secure and with **security features enabled in the ETS tool**. -> NOTE: **openHAB currently ignores messages with secure group addresses.** +**openHAB ignores messages with secure group addresses, unless data secure is configured.** + +> NOTE: openHAB currently does fully support passive (listening) access to secure group addresses. +Write access to secure group addresses is currently disabled in openHAB. +Initial/periodic read will fail, avoid automatic read (< in thing definition). + +All necessary information to decode secure group addresses is included in ETS keyring export (ETS, project settings, export keyring). +Keyring file is configured using `keyringFile` (put it in `config\misc` folder of the openHAB installation) and also requires `keyringPassword`. ## Examples 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 1c88448ad3765..2905aeb75eddb 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 @@ -63,10 +63,13 @@ 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 KEYRING_FILE = "keyringFile"; + public static final String KEYRING_PASSWORD = "keyringPassword"; 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"; + public static final String TUNNEL_SOURCE_ADDRESS = "tunnelSourceAddress"; // 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 87ef9245163e5..c3d1b4701554f 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 @@ -28,6 +28,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.dpt.ValueEncoder; import org.openhab.binding.knx.internal.handler.GroupAddressListener; +import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler; import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler.CommandExtensionData; import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider; import org.openhab.core.thing.ThingStatus; @@ -51,20 +52,18 @@ import tuwien.auto.calimero.link.NetworkLinkListener; import tuwien.auto.calimero.mgmt.Destination; import tuwien.auto.calimero.mgmt.ManagementClient; -import tuwien.auto.calimero.mgmt.ManagementClientImpl; import tuwien.auto.calimero.mgmt.ManagementProcedures; -import tuwien.auto.calimero.mgmt.ManagementProceduresImpl; +import tuwien.auto.calimero.mgmt.TransportLayerImpl; import tuwien.auto.calimero.process.ProcessCommunication; 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; /** - * KNX Client which encapsulates the communication with the KNX bus via the calimero libary. + * KNX Client which encapsulates the communication with the KNX bus via the calimero library. * * @author Simon Kaufmann - initial contribution and API. * @@ -92,6 +91,7 @@ public enum ClientState { private final StatusUpdateCallback statusUpdateCallback; private final ScheduledExecutorService knxScheduler; private final CommandExtensionData commandExtensionData; + protected final Security openhabSecurity; private @Nullable ProcessCommunicator processCommunicator; private @Nullable ProcessCommunicationResponder responseCommunicator; @@ -139,7 +139,7 @@ public void groupReadResponse(ProcessEvent e) { public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause, int readRetriesLimit, ScheduledExecutorService knxScheduler, CommandExtensionData commandExtensionData, - StatusUpdateCallback statusUpdateCallback) { + Security openhabSecurity, StatusUpdateCallback statusUpdateCallback) { this.autoReconnectPeriod = autoReconnectPeriod; this.thingUID = thingUID; this.responseTimeout = responseTimeout; @@ -148,6 +148,7 @@ public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int respons this.knxScheduler = knxScheduler; this.statusUpdateCallback = statusUpdateCallback; this.commandExtensionData = commandExtensionData; + this.openhabSecurity = openhabSecurity; } public void initialize() { @@ -206,38 +207,46 @@ private synchronized boolean connect() { 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); + // one transport layer implementation, to be shared by all following classes + TransportLayerImpl tl = new TransportLayerImpl(link); + + // new SecureManagement / SecureApplicationLayer, based on the keyring (if any) + // SecureManagement does not offer a public ctor which can use a given TL. + // Protected ctor using given TransportLayerImpl is available (custom class to be inherited) + // which also copies the relevant content of the supplied SAL to a new SAL instance created + // by SecureManagement ctor. + CustomSecureManagement sal = new CustomSecureManagement(tl, openhabSecurity); + + logger.debug("GAs: {} Send: {}, S={}", sal.security().groupKeys().size(), + sal.security().groupSenders().size(), + KNXBridgeBaseThingHandler.secHelperGetSecureGroupAddresses(sal.security())); // 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); + // Note for KNX Secure: ManagementClientImpl does not provide a ctor with external SAL in Calimero 2.5. + // Protected ctor using given ManagementClientImpl is available in >2.5 (custom class to be inherited) + ManagementClient managementClient = new CustomManagementClientImpl(link, sal); managementClient.responseTimeout(Duration.ofSeconds(responseTimeout)); this.managementClient = managementClient; + // ManagementProcedures provided by Calimero: allow managing other KNX devices, e.g. check if an address is + // reachable. + // Note for KNX Secure: ManagementProceduresImpl currently does not provide a public ctor with external SAL. + // Protected ctor using given ManagementClientImpl is available (custom class to be inherited) + managementProcedures = new CustomManagementProceduresImpl(managementClient, tl); + // 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); + final var useGoDiagnostics = true; + ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link, sal, useGoDiagnostics); 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 - this.responseCommunicator = new ProcessCommunicationResponder(link, - new SecureApplicationLayer(link, Security.defaultInstallation())); + ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link, sal); + this.responseCommunicator = responseCommunicator; // register this class, callbacks will be triggered link.addLinkListener(this); @@ -277,7 +286,7 @@ private synchronized boolean connect() { } } - private void disconnect(@Nullable Exception e) { + private synchronized void disconnect(@Nullable Exception e) { disconnect(e, null); } @@ -294,23 +303,23 @@ private synchronized void disconnect(@Nullable Exception e, @Nullable ThingStatu protected void releaseConnection() { logger.debug("Bridge {} is disconnecting from KNX bus", thingUID); - var tmplink = link; - if (tmplink != null) { - tmplink.removeLinkListener(this); + var tmpLink = link; + if (tmpLink != null) { + tmpLink.removeLinkListener(this); } - busJob = nullify(busJob, j -> j.cancel(true)); readDatapoints.clear(); - responseCommunicator = nullify(responseCommunicator, rc -> { - rc.removeProcessListener(processListener); - rc.detach(); - }); + busJob = nullify(busJob, j -> j.cancel(true)); + deviceInfoClient = null; + managementProcedures = nullify(managementProcedures, ManagementProcedures::detach); + managementClient = nullify(managementClient, ManagementClient::detach); processCommunicator = nullify(processCommunicator, pc -> { pc.removeProcessListener(processListener); pc.detach(); }); - deviceInfoClient = null; - managementClient = nullify(managementClient, ManagementClient::detach); - managementProcedures = nullify(managementProcedures, ManagementProcedures::detach); + responseCommunicator = nullify(responseCommunicator, rc -> { + rc.removeProcessListener(processListener); + rc.detach(); + }); link = nullify(link, KNXNetworkLink::close); logger.trace("Bridge {} disconnected from KNX bus", thingUID); } @@ -361,13 +370,20 @@ private void readNextQueuedDatapoint() { } ReadDatapoint datapoint = readDatapoints.poll(); if (datapoint != null) { + // TODO #8872: allow write access, currently only listening mode + if (openhabSecurity.groupKeys().containsKey(datapoint.getDatapoint().getMainAddress())) { + logger.debug("outgoing secure communication not implemented, explicit read from GA '{}' skipped", + datapoint.getDatapoint().getMainAddress()); + return; + } + datapoint.incrementRetries(); try { logger.trace("Sending a Group Read Request telegram for {}", datapoint.getDatapoint().getMainAddress()); processCommunicator.read(datapoint.getDatapoint()); } catch (KNXException e) { // Note: KnxException does not cover KnxRuntimeException and subclasses KnxSecureException, - // KnxIllegArgumentException + // KnxIllegalArgumentException if (datapoint.getRetries() < datapoint.getLimit()) { readDatapoints.add(datapoint); logger.debug("Could not read value for datapoint {}: {}. Going to retry.", @@ -532,6 +548,12 @@ private void sendToKNX(ProcessCommunication communicator, GroupAddress groupAddr return; } + // TODO #8872: allow write access, currently only listening mode + if (openhabSecurity.groupKeys().containsKey(groupAddress)) { + logger.debug("outgoing secure communication not implemented, write to GA '{}' skipped", groupAddress); + return; + } + Datapoint datapoint = new CommandDP(groupAddress, thingUID.toString(), 0, NORMALIZED_DPT.getOrDefault(dpt, dpt)); String mappedValue = ValueEncoder.encode(type, dpt); diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomManagementClientImpl.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomManagementClientImpl.java new file mode 100644 index 0000000000000..0df7bb1f963df --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomManagementClientImpl.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 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.client; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import tuwien.auto.calimero.link.KNXLinkClosedException; +import tuwien.auto.calimero.link.KNXNetworkLink; +import tuwien.auto.calimero.mgmt.ManagementClientImpl; +import tuwien.auto.calimero.mgmt.SecureManagement; + +/** + * This class is to provide access to protected constructors in the Calimero library. + * Reason is to provide custom KNX keyring data. + * + * @author Holger Friedrich - initial contribution + * + */ +@NonNullByDefault +public class CustomManagementClientImpl extends ManagementClientImpl { + public CustomManagementClientImpl(final KNXNetworkLink link, final SecureManagement secureManagement) + throws KNXLinkClosedException { + // super(link, secureManagement) is available since Calimero 2.5.1 + super(link, secureManagement); + } +} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomManagementProceduresImpl.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomManagementProceduresImpl.java new file mode 100644 index 0000000000000..3f8489ed23df1 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomManagementProceduresImpl.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2024 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.client; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import tuwien.auto.calimero.link.KNXLinkClosedException; +import tuwien.auto.calimero.mgmt.ManagementClient; +import tuwien.auto.calimero.mgmt.ManagementProceduresImpl; +import tuwien.auto.calimero.mgmt.TransportLayer; + +/** + * This class is to provide access to protected constructors in the Calimero library. + * Reason is to provide custom KNX keyring data. + * + * @author Holger Friedrich - initial contribution + * + */ +@NonNullByDefault +public class CustomManagementProceduresImpl extends ManagementProceduresImpl { + public CustomManagementProceduresImpl(final ManagementClient mgmtClient, final TransportLayer transportLayer) + throws KNXLinkClosedException { + // super(mgmtClient, transportLayer) is protected + super(mgmtClient, transportLayer); + } +} diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomSecureManagement.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomSecureManagement.java new file mode 100644 index 0000000000000..7f4556d39b046 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/CustomSecureManagement.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2024 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.client; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import tuwien.auto.calimero.SerialNumber; +import tuwien.auto.calimero.link.KNXLinkClosedException; +import tuwien.auto.calimero.mgmt.SecureManagement; +import tuwien.auto.calimero.mgmt.TransportLayer; +import tuwien.auto.calimero.mgmt.TransportLayerImpl; +import tuwien.auto.calimero.secure.Security; + +/** + * This class is to provide access to protected constructors in the Calimero library. + * Reason is to provide custom KNX keyring data. + * + * @author Holger Friedrich - initial contribution + * + */ +@NonNullByDefault +public class CustomSecureManagement extends SecureManagement { + public CustomSecureManagement(final TransportLayer transportLayer, final Security security) + throws KNXLinkClosedException { + // super(link, secureManagement) is not yet available in Calimero 2.5.1 + super((TransportLayerImpl) transportLayer, SerialNumber.Zero, 0, security.deviceToolKeys()); + + // instance of Security has been created in ctor of SecureManagement, but only using deviceToolKeys + // no need to clear, just copy (otherwise SAL would lack the group keys) + final Security sal = security(); + sal.groupKeys().putAll(security.groupKeys()); + sal.groupSenders().putAll(security.groupSenders()); + } + + public final Security security() { + return super.security(); + } +} 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 ba9261471efba..1c209ffc22b76 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 @@ -41,6 +41,7 @@ import tuwien.auto.calimero.link.KNXNetworkLinkIP; import tuwien.auto.calimero.link.medium.KNXMediumSettings; import tuwien.auto.calimero.link.medium.TPSettings; +import tuwien.auto.calimero.secure.Security; /** * IP specific {@link AbstractKNXClient} implementation. @@ -88,9 +89,9 @@ public IPClient(IpConnectionType ipConnectionType, String ip, String localSource byte[] secureRoutingBackboneGroupKey, long secureRoutingLatencyToleranceMs, byte[] secureTunnelDevKey, int secureTunnelUser, byte[] secureTunnelUserKey, ThingUID thingUID, int responseTimeout, int readingPause, int readRetriesLimit, ScheduledExecutorService knxScheduler, CommandExtensionData commandExtensionData, - StatusUpdateCallback statusUpdateCallback) { + Security openhabSecurity, StatusUpdateCallback statusUpdateCallback) { super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler, - commandExtensionData, statusUpdateCallback); + commandExtensionData, openhabSecurity, statusUpdateCallback); this.ipConnectionType = ipConnectionType; this.ip = ip; this.localSource = localSource; diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/SerialClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/SerialClient.java index 7d0af16f9b3ac..49d3e3306ed31 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/SerialClient.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/SerialClient.java @@ -32,6 +32,7 @@ import tuwien.auto.calimero.link.KNXNetworkLink; import tuwien.auto.calimero.link.KNXNetworkLinkFT12; import tuwien.auto.calimero.link.medium.TPSettings; +import tuwien.auto.calimero.secure.Security; import tuwien.auto.calimero.serial.FT12Connection; /** @@ -53,10 +54,10 @@ public class SerialClient extends AbstractKNXClient { public SerialClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause, int readRetriesLimit, ScheduledExecutorService knxScheduler, String serialPort, boolean useCemi, - SerialPortManager serialPortManager, CommandExtensionData commandExtensionData, + SerialPortManager serialPortManager, CommandExtensionData commandExtensionData, Security openhabSecurity, StatusUpdateCallback statusUpdateCallback) { super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler, - commandExtensionData, statusUpdateCallback); + commandExtensionData, openhabSecurity, statusUpdateCallback); this.serialPortManager = serialPortManager; this.serialPort = serialPort; this.useCemi = useCemi; diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/BridgeConfiguration.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/BridgeConfiguration.java index c750280511011..89af885de3556 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/BridgeConfiguration.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/BridgeConfiguration.java @@ -26,6 +26,8 @@ public class BridgeConfiguration { private int readingPause = 0; private int readRetriesLimit = 0; private int responseTimeout = 0; + private String keyringFile = ""; + private String keyringPassword = ""; public int getAutoReconnectPeriod() { return autoReconnectPeriod; @@ -46,4 +48,12 @@ public int getResponseTimeout() { public void setAutoReconnectPeriod(int period) { autoReconnectPeriod = period; } + + public String getKeyringFile() { + return keyringFile; + } + + public String getKeyringPassword() { + return keyringPassword; + } } 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 bbae73c127705..49749a3ca2705 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 @@ -33,6 +33,7 @@ public class IPBridgeConfiguration extends BridgeConfiguration { private String tunnelUserId = ""; private String tunnelUserPassword = ""; private String tunnelDeviceAuthentication = ""; + private String tunnelSourceAddress = ""; public Boolean getUseNAT() { return useNAT; @@ -73,4 +74,8 @@ public String getTunnelUserPassword() { public String getTunnelDeviceAuthentication() { return tunnelDeviceAuthentication; } + + public String getTunnelSourceAddress() { + return tunnelSourceAddress; + } } 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 f440eea031485..470ca48364001 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 @@ -72,14 +72,25 @@ public void initializeLater() { IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class); boolean securityAvailable = false; try { - securityAvailable = initializeSecurity(config.getRouterBackboneKey(), - config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), config.getTunnelUserPassword()); + securityAvailable = initializeSecurity(config.getKeyringFile(), config.getKeyringPassword(), + config.getRouterBackboneKey(), config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), + config.getTunnelUserPassword(), config.getTunnelSourceAddress()); 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")); + + if (keyring.isPresent()) { + logger.debug("KNX secure available for {} devices, {} group addresses", + openhabSecurity.deviceToolKeys().size(), openhabSecurity.groupKeys().size()); + + logger.debug("Secure group addresses and associated devices: {}", + secHelperGetSecureGroupAddresses(openhabSecurity)); + } else { + logger.debug("KNX secure: keyring is not available"); + } } else { logger.debug("KNX security not configured"); } @@ -154,7 +165,7 @@ public void initializeLater() { return; } if (secureRouting.backboneGroupKey.length != 16) { - // failed to read shared backbone group key from config + // failed to read shared backbone group key from config or keyring logger.warn("Bridge {} invalid security configuration for secure routing", thing.getUID()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/error.knx-secure-routing-backbonegroupkey-invalid"); @@ -186,7 +197,7 @@ public void initializeLater() { secureRouting.backboneGroupKey, secureRouting.latencyToleranceMs, secureTunnel.devKey, secureTunnel.user, secureTunnel.userKey, thing.getUID(), config.getResponseTimeout(), config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), getCommandExtensionData(), - this); + openhabSecurity, this); IPClient tmpClient = client; if (tmpClient != null) { @@ -200,7 +211,7 @@ public void initializeLater() { public void dispose() { Future tmpInitJob = initJob; if (tmpInitJob != null) { - while (!tmpInitJob.isDone()) { + if (!tmpInitJob.isDone()) { logger.trace("Bridge {}, shutdown during init, trying to cancel", thing.getUID()); tmpInitJob.cancel(true); try { 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 dc07731bdfce8..ed2e8111ac33f 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 @@ -12,7 +12,15 @@ */ package org.openhab.binding.knx.internal.handler; +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; import java.util.TreeMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -21,6 +29,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.client.KNXClient; import org.openhab.binding.knx.internal.client.StatusUpdateCallback; +import org.openhab.core.OpenHAB; import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; @@ -29,8 +38,16 @@ import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.types.Command; +import tuwien.auto.calimero.GroupAddress; +import tuwien.auto.calimero.IndividualAddress; +import tuwien.auto.calimero.KNXFormatException; import tuwien.auto.calimero.knxnetip.SecureConnection; +import tuwien.auto.calimero.secure.Keyring; +import tuwien.auto.calimero.secure.Keyring.Backbone; +import tuwien.auto.calimero.secure.Keyring.Interface; import tuwien.auto.calimero.secure.KnxSecureException; +import tuwien.auto.calimero.secure.Security; +import tuwien.auto.calimero.xml.KNXMLException; /** * The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are @@ -73,12 +90,20 @@ public record CommandExtensionData(Map unknownGA) { private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx"); private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor(); + protected Optional keyring; + // password used to protect content of the keyring + private String keyringPassword = ""; + // backbone key (shared password used for secure router mode) + + protected Security openhabSecurity; protected SecureRoutingConfig secureRouting; protected SecureTunnelConfig secureTunnel; private CommandExtensionData commandExtensionData; public KNXBridgeBaseThingHandler(Bridge bridge) { super(bridge); + keyring = Optional.empty(); + openhabSecurity = Security.newSecurity(); secureRouting = new SecureRoutingConfig(); secureTunnel = new SecureTunnelConfig(); commandExtensionData = new CommandExtensionData(new TreeMap<>()); @@ -90,25 +115,48 @@ public CommandExtensionData getCommandExtensionData() { return commandExtensionData; } + /*** + * Initialize KNX secure if configured (simple interface) + * + * @param cKeyringFile keyring file, exported from ETS tool + * @param cKeyringPassword keyring password, set during export from ETS tool + * @return + */ + protected boolean initializeSecurity(String cKeyringFile, String cKeyringPassword) throws KnxSecureException { + return initializeSecurity(cKeyringFile, cKeyringPassword, "", "", "", "", ""); + } + /*** * 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. + * @param cKeyringFile keyring file, exported from ETS tool + * @param cKeyringPassword keyring password, set during export from ETS tool + * @param cRouterBackboneGroupKey shared key for secure router mode. If not given, it will be read from keyring. + * @param cTunnelDevAuth device password for IP interface in tunnel mode. If not given it will be read from keyring + * if cTunnelSourceAddr is configured. + * @param cTunnelUser user id for tunnel mode. Must be an integer >0. If not given it will be read from keyring if + * cTunnelSourceAddr is configured. + * @param cTunnelPassword user password for tunnel mode. If not given it will be read from keyring if + * cTunnelSourceAddr is configured. + * @param cTunnelSourceAddr specify the KNX address to uniquely identify a tunnel connection in secure tunneling + * mode. Not required if cTunnelDevAuth, cTunnelUser, and cTunnelPassword are given. * @return */ - protected boolean initializeSecurity(String cRouterBackboneGroupKey, String cTunnelDevAuth, String cTunnelUser, - String cTunnelPassword) throws KnxSecureException { + protected boolean initializeSecurity(String cKeyringFile, String cKeyringPassword, String cRouterBackboneGroupKey, + String cTunnelDevAuth, String cTunnelUser, String cTunnelPassword, String cTunnelSourceAddr) + throws KnxSecureException { + keyring = Optional.empty(); + keyringPassword = ""; + IndividualAddress secureTunnelSourceAddr = null; secureRouting = new SecureRoutingConfig(); secureTunnel = new SecureTunnelConfig(); boolean securityInitialized = false; - // step 1: secure routing, backbone group key manually specified in OH config + // step 1: secure routing, backbone group key manually specified in OH config (typically it is read from + // keyring) if (!cRouterBackboneGroupKey.isBlank()) { - // provided in config + // provided in config, this will override whatever is read from keyring String key = cRouterBackboneGroupKey.trim().replaceFirst("^0x", "").trim().replace(" ", ""); if (!key.isEmpty()) { // helper may throw KnxSecureException @@ -118,6 +166,14 @@ protected boolean initializeSecurity(String cRouterBackboneGroupKey, String cTun } // step 2: check if valid tunnel parameters are specified in config + if (!cTunnelSourceAddr.isBlank()) { + try { + secureTunnelSourceAddr = new IndividualAddress(cTunnelSourceAddr.trim()); + securityInitialized = true; + } catch (KNXFormatException e) { + throw new KnxSecureException("tunnel source address cannot be parsed, valid format is x.y.z"); + } + } if (!cTunnelDevAuth.isBlank()) { secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(cTunnelDevAuth.toCharArray()); securityInitialized = true; @@ -139,29 +195,107 @@ protected boolean initializeSecurity(String cRouterBackboneGroupKey, String cTun securityInitialized = true; } + // step 3: keyring + if (!cKeyringFile.isBlank()) { + // filename defined in config, start parsing + try { + // load keyring file from config dir, folder misc + String keyringUri = OpenHAB.getConfigFolder() + File.separator + "misc" + File.separator + cKeyringFile; + try { + keyring = Optional.ofNullable(Keyring.load(keyringUri)); + } catch (KNXMLException e) { + throw new KnxSecureException("keyring file configured, but loading failed: ", e); + } + if (!keyring.isPresent()) { + throw new KnxSecureException("keyring file configured, but loading failed: " + keyringUri); + } + + // loading was successful, check signatures + // -> disabled, as Calimero v2.5 does this within the load() function + // if (!keyring.verifySignature(cKeyringPassword.toCharArray())) + // throw new KnxSecureException( + // "signature verification failed, please check keyring file: " + keyringUri); + keyringPassword = cKeyringPassword; + + // We use a specific Security instance instead of default Calimero static instance + // Security.defaultInstallation(). + // This necessary as it seems there is no possibility to clear the global instance on config changes. + openhabSecurity.useKeyring(keyring.get(), keyringPassword.toCharArray()); + + securityInitialized = true; + } catch (KnxSecureException e) { + keyring = Optional.empty(); + keyringPassword = ""; + throw e; + } catch (Exception e) { + // load() may throw KnxSecureException or other undeclared exceptions, e.g. UncheckedIOException when + // file is not found + keyring = Optional.empty(); + keyringPassword = ""; + throw new KnxSecureException("keyring file configured, but loading failed", e); + } + } + + // step 4: router: load backboneGroupKey from keyring if not manually specified + if ((secureRouting.backboneGroupKey.length == 0) && (keyring.isPresent())) { + // backbone group key is only available if secure routers are present + final Optional key = secHelperReadBackboneKey(keyring, keyringPassword); + if (key.isPresent()) { + secureRouting.backboneGroupKey = key.get(); + 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 + // this parameter is currently not exposed in config, in case it must be set by using the keyring secureRouting.latencyToleranceMs = 2000; + if (keyring.isPresent()) { + // backbone latency is only relevant if secure routers are present + final Optional bb = keyring.get().backbone(); + if (bb.isPresent()) { + final long toleranceMs = bb.get().latencyTolerance().toMillis(); + secureRouting.latencyToleranceMs = toleranceMs; + } + } + + // step 6: tunnel: load data from keyring + if (secureTunnelSourceAddr != null) { + // requires a valid keyring + if (!keyring.isPresent()) { + throw new KnxSecureException("valid keyring specification required for secure tunnel mode"); + } + // other parameters will not be accepted, since all is read from keyring in this case + if ((secureTunnel.userKey.length > 0) || secureTunnel.user != 0 || (secureTunnel.devKey.length > 0)) { + throw new KnxSecureException( + "tunnelSourceAddr is configured, please do not specify other parameters of secure tunnel"); + } + Optional config = secHelperReadTunnelConfig(keyring, keyringPassword, + secureTunnelSourceAddr); + if (config.isEmpty()) { + throw new KnxSecureException("tunnel definition cannot be read from keyring"); + } + secureTunnel = config.get(); + } return securityInitialized; } /*** * converts hex string (32 characters) to byte[16] * - * @param hexstring 32 characters hex + * @param hexString 32 characters hex * @return key in byte array format */ - public static byte[] secHelperParseBackboneKey(String hexstring) throws KnxSecureException { - if (hexstring.length() != 32) { + 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); + 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); @@ -169,6 +303,97 @@ public static byte[] secHelperParseBackboneKey(String hexstring) throws KnxSecur return parsed; } + public static Optional secHelperReadBackboneKey(Optional keyring, String keyringPassword) { + if (keyring.isEmpty()) { + throw new KnxSecureException("keyring not available, cannot read backbone key"); + } + final Optional bb = keyring.get().backbone(); + if (bb.isPresent()) { + final Optional gk = bb.get().groupKey(); + if (gk.isPresent()) { + byte[] secureRoutingBackboneGroupKey = keyring.get().decryptKey(gk.get(), + keyringPassword.toCharArray()); + if (secureRoutingBackboneGroupKey.length != 16) { + throw new KnxSecureException("backbone key found, unexpected length != 16"); + } + return Optional.of(secureRoutingBackboneGroupKey); + } + } + return Optional.empty(); + } + + public static Optional secHelperReadTunnelConfig(Optional keyring, + String keyringPassword, IndividualAddress secureTunnelSourceAddr) { + if (keyring.isEmpty()) { + throw new KnxSecureException("keyring not available, cannot read tunnel config"); + } + // iterate all interfaces to find matching secureTunnelSourceAddr + SecureTunnelConfig secureTunnel = new SecureTunnelConfig(); + Iterator> itInterface = keyring.get().interfaces().values().iterator(); + boolean complete = false; + while (!complete && itInterface.hasNext()) { + List eInterface = itInterface.next(); + // tunnels are nested + Iterator itTunnel = eInterface.iterator(); + while (!complete && itTunnel.hasNext()) { + Interface eTunnel = itTunnel.next(); + + if (secureTunnelSourceAddr.equals(eTunnel.address())) { + String pw = ""; + final Optional pwBytes = eTunnel.password(); + if (pwBytes.isPresent()) { + pw = new String(keyring.get().decryptPassword(pwBytes.get(), keyringPassword.toCharArray())); + secureTunnel.userKey = SecureConnection.hashUserPassword(pw.toCharArray()); + } + + String au = ""; + final Optional auBytes = eTunnel.authentication(); + if (auBytes.isPresent()) { + au = new String(keyring.get().decryptPassword(auBytes.get(), keyringPassword.toCharArray())); + secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(au.toCharArray()) + .clone(); + } + + // set user, 0=fail + secureTunnel.user = eTunnel.user(); + + return Optional.of(secureTunnel); + } + } + } + return Optional.empty(); + } + + /*** + * Show all secure group addresses and surrogates. A surrogate is the device which is asked to carry out an indirect + * read/write request. + * Simpler approach w/o surrogates: Security.defaultInstallation().groupSenders().toString()); + */ + public static String secHelperGetSecureGroupAddresses(final Security openhabSecurity) { + Map> groupSendersWithSurrogate = new HashMap>(); + final Map> senders = openhabSecurity.groupSenders(); + for (var entry : senders.entrySet()) { + final GroupAddress ga = entry.getKey(); + // the following approach is uses by Calimero to deduce the surrogate for GA diagnostics + // see calimero-core security/SecureApplicationLayer.java, surrogate(...) + IndividualAddress surrogate = null; + try { + surrogate = senders.getOrDefault(ga, Set.of()).stream().findAny().get(); + } catch (NoSuchElementException e) { + } + Set devices = new HashSet(); + for (var device : entry.getValue()) { + if (device.equals(surrogate)) { + devices.add(device.toString() + " (S)"); + } else { + devices.add(device.toString()); + } + } + groupSendersWithSurrogate.put(ga, devices); + } + return groupSendersWithSurrogate.toString(); + } + @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 5cb4692de9655..4a43bb1753671 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 @@ -23,9 +23,12 @@ import org.openhab.core.io.transport.serial.SerialPortManager; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingStatus; +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 Serial/USB Gateway, that either acts as a @@ -57,7 +60,7 @@ public void initialize() { SerialBridgeConfiguration config = getConfigAs(SerialBridgeConfiguration.class); client = new SerialClient(config.getAutoReconnectPeriod(), thing.getUID(), config.getResponseTimeout(), config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), config.getSerialPort(), - config.useCemi(), serialPortManager, getCommandExtensionData(), this); + config.useCemi(), serialPortManager, getCommandExtensionData(), openhabSecurity, this); updateStatus(ThingStatus.UNKNOWN); // delay actual initialization, allow for longer runtime of actual initialization @@ -65,6 +68,32 @@ public void initialize() { } public void initializeLater() { + SerialBridgeConfiguration config = getConfigAs(SerialBridgeConfiguration.class); + try { + if (initializeSecurity(config.getKeyringFile(), config.getKeyringPassword())) { + if (keyring.isPresent()) { + logger.info("KNX secure available for {} devices, {} group addresses", + openhabSecurity.deviceToolKeys().size(), openhabSecurity.groupKeys().size()); + + logger.debug("Secure group addresses and associated devices: {}", + secHelperGetSecureGroupAddresses(openhabSecurity)); + } else { + logger.debug("KNX secure: keyring is not available"); + } + } 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; + } + SerialClient tmpClient = client; if (tmpClient != null) { tmpClient.initialize(); 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 4914cdc865aea..ccfbd898fe80e 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 @@ -33,6 +33,10 @@ 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.keyringFile.label = Keyring file +thing-type.config.knx.ip.keyringFile.description = Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. knx.knxkeys. This file is mandatory to decode secure GAs. It can provide settings and credentials for IP Secure if not configured separately. +thing-type.config.knx.ip.keyringPassword.label = Keyring password +thing-type.config.knx.ip.keyringPassword.description = Keyring file password (set during export from ETS). thing-type.config.knx.ip.localIp.label = Local Network Address thing-type.config.knx.ip.localIp.description = Network address of the local host to be used to set up the connection to the KNX/IP gateway thing-type.config.knx.ip.localSourceAddr.label = Local Device Address @@ -46,13 +50,15 @@ thing-type.config.knx.ip.readingPause.description = Time in milliseconds of how 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.routerBackboneKey.description = Backbone key for secure router mode. 16 bytes in hex notation. Can also be found in ETS security report. Optional, can be read from keyring file if it is configured. 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.tunnelDeviceAuthentication.description = Tunnel device authentication for secure tunnel mode. Optional, can be read from keyring file if tunnelSourceAddr is configured. +thing-type.config.knx.ip.tunnelSourceAddress.label = Tunnel source address +thing-type.config.knx.ip.tunnelSourceAddress.description = Physical KNX address of tunnel in secure mode. Optional, only used in combination with keyring file to uniquely identify a tunnel. If given, openHAB will try to read user id, user password and device authentication from keyring. 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.tunnelUserId.description = Tunnel user id for secure tunnel mode. Optional, can be read from keyring file if tunnelSourceAddr is configured. 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.tunnelUserPassword.description = Tunnel user key for secure tunnel mode. Optional, can be read from keyring file if tunnelSourceAddr is configured. 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, ROUTER, SECURETUNNEL, or SECUREROUTER thing-type.config.knx.ip.type.option.TUNNEL = Tunnel @@ -63,6 +69,12 @@ 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 thing-type.config.knx.serial.autoReconnectPeriod.description = Seconds between connect retries when KNX link has been lost, 0 means never retry +thing-type.config.knx.serial.group.knxsecure.label = KNX secure +thing-type.config.knx.serial.group.knxsecure.description = Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation. +thing-type.config.knx.serial.keyringFile.label = Keyring file +thing-type.config.knx.serial.keyringFile.description = Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. knx.knxkeys. This file is mandatory to decode secure GAs. +thing-type.config.knx.serial.keyringPassword.label = Keyring password +thing-type.config.knx.serial.keyringPassword.description = Keyring file password (set during export from ETS). thing-type.config.knx.serial.readRetriesLimit.label = Read Retries Limit thing-type.config.knx.serial.readRetriesLimit.description = Limits the read retries while initialization from the KNX bus thing-type.config.knx.serial.readingPause.label = Reading Pause @@ -152,11 +164,6 @@ channel-type.config.knx.rollershutter.upDown.description = The group address (GA channel-type.config.knx.single.ga.label = Address channel-type.config.knx.single.ga.description = The group address (GA) for this channel. Additional listening (status) GAs can be added by concatenating GAs with a "+" (e.g. "1/7/18+2/9/15"). The DPT can be defined by prepending it to the first GA, separated by ":" (e.g. "2.001:1/4/17"). GAs prepended with a "<" (e.g. "<5/1/125") are read from the bus on startup. -# 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. - # add-on specific strings error.knx-secure-routing-backbonegroupkey-invalid = backbonegroupkey invalid, please check if it is specified correctly 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 6a8d7427d25bb..06853a57b4de4 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 @@ -73,29 +73,60 @@ Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s 60 + + + Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. + knx.knxkeys. This file is + mandatory to decode secure GAs. It can provide settings + and credentials for IP Secure if not configured separately. + true + + + password + + Keyring file password (set during export from ETS). + true + password Backbone key for secure router mode. 16 bytes in hex notation. Can also be found - in ETS security report. + in ETS security report. + Optional, can be read from + keyring file if it is configured. + true + + + + Physical KNX address of tunnel in secure mode. Optional, only used in combination + with keyring file to + uniquely identify a tunnel. If given, openHAB will try to read user id, user + password and device authentication from + keyring. true - Tunnel user id for secure tunnel mode. + Tunnel user id for secure tunnel mode. Optional, can be read from + keyring file if tunnelSourceAddr is + configured. true password - Tunnel user key for secure tunnel mode. + Tunnel user key for secure tunnel mode. Optional, can be read from + keyring file if tunnelSourceAddr is + configured. true password - Tunnel device authentication for secure tunnel mode. + Tunnel device authentication for secure tunnel mode. Optional, can be read from + keyring file if + tunnelSourceAddr is configured. true diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml index 4357d0ef9b1dc..9ff895016e6f4 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml @@ -8,6 +8,10 @@ This is a serial interface for accessing the KNX bus + + + Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation. + serial-port false @@ -42,6 +46,19 @@ Seconds between connect retries when KNX link has been lost, 0 means never retry 0 + + + Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. + knx.knxkeys. This file is + mandatory to decode secure GAs. + true + + + password + + Keyring file password (set during export from ETS). + true + diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/client/DummyClient.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/client/DummyClient.java index 379d680e8ff93..3a1beef7cff55 100644 --- a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/client/DummyClient.java +++ b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/client/DummyClient.java @@ -32,7 +32,7 @@ public class DummyClient extends AbstractKNXClient { public DummyClient() { super(0, new ThingUID("dummy connection"), 0, 0, 0, null, new CommandExtensionData(Collections.emptyMap()), - null); + null, null); } @Override 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 index 8ae930142c720..4c331a812b8f6 100644 --- 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 @@ -15,8 +15,13 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; +import java.io.File; +import java.net.URISyntaxException; +import java.util.Properties; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; +import org.openhab.core.OpenHAB; import org.openhab.core.net.NetworkAddressService; import org.openhab.core.thing.Bridge; @@ -48,28 +53,53 @@ void testInitializeSecurity() { IPBridgeThingHandler handler = new IPBridgeThingHandler(bridge, nas); // no config given - assertFalse(handler.initializeSecurity("", "", "", "")); + assertFalse(handler.initializeSecurity("", "", "", "", "", "", "")); // router password configured, length must be 16 bytes in hex notation - assertTrue(handler.initializeSecurity("D947B12DDECAD528B1D5A88FD347F284", "", "", "")); - assertTrue(handler.initializeSecurity("0xD947B12DDECAD528B1D5A88FD347F284", "", "", "")); + assertTrue(handler.initializeSecurity("", "", "D947B12DDECAD528B1D5A88FD347F284", "", "", "", "")); + assertTrue(handler.initializeSecurity("", "", "0xD947B12DDECAD528B1D5A88FD347F284", "", "", "", "")); assertThrows(KnxSecureException.class, () -> { - handler.initializeSecurity("wrongLength", "", "", ""); + handler.initializeSecurity("", "", "wrongLength", "", "", "", ""); }); // tunnel configuration - assertTrue(handler.initializeSecurity("", "da", "1", "pw")); + assertTrue(handler.initializeSecurity("", "", "", "da", "1", "pw", "")); // cTunnelUser is restricted to a number >0 assertThrows(KnxSecureException.class, () -> { - handler.initializeSecurity("", "da", "0", "pw"); + handler.initializeSecurity("", "", "", "da", "0", "pw", ""); }); assertThrows(KnxSecureException.class, () -> { - handler.initializeSecurity("", "da", "eins", "pw"); + 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")); + assertTrue(handler.initializeSecurity("", "", "", "da", "", "", "")); + assertTrue(handler.initializeSecurity("", "", "", "", "1", "", "")); + assertTrue(handler.initializeSecurity("", "", "", "", "", "pw", "")); + + assertThrows(KnxSecureException.class, () -> { + handler.initializeSecurity("nonExistingFile.xml", "", "", "", "", "", ""); + }); + + Properties pBackup = new Properties(System.getProperties()); + try { + final File testFile = new File( + getClass().getClassLoader().getResource("misc" + File.separator + "openhab6.knxkeys").toURI()); + final String passwordString = "habopen"; + + Properties p = new Properties(System.getProperties()); + p.put(OpenHAB.CONFIG_DIR_PROG_ARGUMENT, testFile.getParent().replaceAll("misc$", "")); + System.setProperties(p); + + assertTrue(handler.initializeSecurity(testFile.getName().toString(), passwordString, "", "", "", "pw", "")); + + assertThrows(KnxSecureException.class, () -> { + assertTrue(handler.initializeSecurity(testFile.getName().toString(), "wrong", "", "", "", "pw", "")); + }); + } catch (URISyntaxException e) { + } finally { + // properties are not persistent, but change may interference with other tests -> restore + System.setProperties(pBackup); + } } } diff --git a/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/security/KNXSecurityTest.java b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/security/KNXSecurityTest.java new file mode 100644 index 0000000000000..29f3648956ad8 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/security/KNXSecurityTest.java @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2010-2024 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.security; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler; + +import tuwien.auto.calimero.GroupAddress; +import tuwien.auto.calimero.IndividualAddress; +import tuwien.auto.calimero.knxnetip.SecureConnection; +import tuwien.auto.calimero.secure.Keyring; +import tuwien.auto.calimero.secure.KnxSecureException; +import tuwien.auto.calimero.secure.Security; + +/** + * + * @author Holger Friedrich - initial contribution + * + */ +@NonNullByDefault +public class KNXSecurityTest { + + @Test + public void testCalimeroKeyring() { + @SuppressWarnings("null") + final String testFile = getClass().getClassLoader().getResource("misc" + File.separator + "openhab6.knxkeys") + .toString(); + final String passwordString = "habopen"; + + final char[] password = passwordString.toCharArray(); + assertNotEquals("", testFile); + + Keyring keys = Keyring.load(testFile); + + // System.out.println(keys.devices().toString()); + // System.out.println(keys.groups().toString()); + // System.out.println(keys.interfaces().toString()); + + GroupAddress ga = new GroupAddress(8, 0, 0); + byte[] key800enc = keys.groups().get(ga); + assertNotNull(key800enc); + if (key800enc != null) { + assertNotEquals(0, key800enc.length); + } + byte[] key800dec = keys.decryptKey(key800enc, password); + assertEquals(16, key800dec.length); + + IndividualAddress nopa = new IndividualAddress(2, 8, 20); + Keyring.Device nodev = keys.devices().get(nopa); + assertNull(nodev); + + IndividualAddress pa = new IndividualAddress(1, 1, 42); + Keyring.Device dev = keys.devices().get(pa); + assertNotNull(dev); + // cannot check this for dummy test file, needs real device to be included + // assertNotEquals(0, dev.sequenceNumber()); + + Security openhabSecurity = Security.newSecurity(); + openhabSecurity.useKeyring(keys, password); + Map groupKeys = openhabSecurity.groupKeys(); + assertEquals(3, groupKeys.size()); + groupKeys.remove(ga); + assertEquals(2, groupKeys.size()); + openhabSecurity.useKeyring(keys, password); + Map groupKeys2 = openhabSecurity.groupKeys(); + assertEquals(3, groupKeys2.size()); + assertEquals(3, groupKeys.size()); + ga = new GroupAddress(1, 0, 0); + groupKeys.put(ga, new byte[1]); + assertEquals(4, groupKeys2.size()); + assertEquals(4, groupKeys.size()); + openhabSecurity.useKeyring(keys, password); + assertEquals(4, groupKeys2.size()); + assertEquals(4, groupKeys.size()); + } + + // check tunnel settings, this file does not contain any key + @Test + public void testSecurityHelperEmpty() { + @SuppressWarnings("null") + final String testFile = getClass().getClassLoader() + .getResource("misc" + File.separator + "openhab6-minimal-ipif.knxkeys").toString(); + final String passwordString = "habopen"; + + final char[] password = passwordString.toCharArray(); + assertNotEquals("", testFile); + + Keyring keys = Keyring.load(testFile); + Security openhabSecurity = Security.newSecurity(); + openhabSecurity.useKeyring(keys, password); + + assertThrows(KnxSecureException.class, () -> { + KNXBridgeBaseThingHandler.secHelperReadBackboneKey(Optional.empty(), passwordString); + }); + assertTrue(KNXBridgeBaseThingHandler.secHelperReadBackboneKey(Optional.ofNullable(keys), passwordString) + .isEmpty()); + + // now check tunnel (expected to fail, not included) + IndividualAddress secureTunnelSourceAddr = new IndividualAddress(2, 8, 20); + assertThrows(KnxSecureException.class, () -> { + KNXBridgeBaseThingHandler.secHelperReadTunnelConfig(Optional.empty(), passwordString, + secureTunnelSourceAddr); + }); + assertTrue(KNXBridgeBaseThingHandler + .secHelperReadTunnelConfig(Optional.ofNullable(keys), passwordString, secureTunnelSourceAddr) + .isEmpty()); + } + + // check tunnel settings, this file does not contain any key + @Test + public void testSecurityHelperRouterKey() { + @SuppressWarnings("null") + final String testFile = getClass().getClassLoader() + .getResource("misc" + File.separator + "openhab6-minimal-sipr.knxkeys").toString(); + final String passwordString = "habopen"; + + final char[] password = passwordString.toCharArray(); + assertNotEquals("", testFile); + + Keyring keys = Keyring.load(testFile); + Security openhabSecurity = Security.newSecurity(); + openhabSecurity.useKeyring(keys, password); + + assertThrows(KnxSecureException.class, () -> { + KNXBridgeBaseThingHandler.secHelperReadBackboneKey(Optional.empty(), passwordString); + }); + assertTrue(KNXBridgeBaseThingHandler.secHelperReadBackboneKey(Optional.ofNullable(keys), passwordString) + .isPresent()); + + // now check tunnel (expected to fail, not included) + IndividualAddress secureTunnelSourceAddr = new IndividualAddress(2, 8, 20); + assertThrows(KnxSecureException.class, () -> { + KNXBridgeBaseThingHandler.secHelperReadTunnelConfig(Optional.empty(), passwordString, + secureTunnelSourceAddr); + }); + assertTrue(KNXBridgeBaseThingHandler + .secHelperReadTunnelConfig(Optional.ofNullable(keys), passwordString, secureTunnelSourceAddr) + .isEmpty()); + } + + // check tunnel settings, this file contains a secure interface, but no router password + @Test + public void testSecurityHelperTunnelKey() { + @SuppressWarnings("null") + final String testFile = getClass().getClassLoader() + .getResource("misc" + File.separator + "openhab6-minimal-sipif.knxkeys").toString(); + final String passwordString = "habopen"; + + final char[] password = passwordString.toCharArray(); + assertNotEquals("", testFile); + + Keyring keys = Keyring.load(testFile); + Security openhabSecurity = Security.newSecurity(); + openhabSecurity.useKeyring(keys, password); + + assertThrows(KnxSecureException.class, () -> { + KNXBridgeBaseThingHandler.secHelperReadBackboneKey(Optional.empty(), passwordString); + }); + assertTrue(KNXBridgeBaseThingHandler.secHelperReadBackboneKey(Optional.ofNullable(keys), passwordString) + .isEmpty()); + + // now check tunnel + IndividualAddress secureTunnelSourceAddr = new IndividualAddress(1, 1, 2); + assertThrows(KnxSecureException.class, () -> { + KNXBridgeBaseThingHandler.secHelperReadTunnelConfig(Optional.empty(), passwordString, + secureTunnelSourceAddr); + }); + assertTrue(KNXBridgeBaseThingHandler + .secHelperReadTunnelConfig(Optional.ofNullable(keys), passwordString, secureTunnelSourceAddr) + .isPresent()); + } + + @Test + public void testSecurityHelpers() { + @SuppressWarnings("null") + final String testFile = getClass().getClassLoader().getResource("misc" + File.separator + "openhab6.knxkeys") + .toString(); + final String passwordString = "habopen"; + + final char[] password = passwordString.toCharArray(); + assertNotEquals("", testFile); + + Keyring keys = Keyring.load(testFile); + // this is done during load() in v2.5, but check it once.... + assertTrue(keys.verifySignature(password)); + + Security openhabSecurity = Security.newSecurity(); + openhabSecurity.useKeyring(keys, password); + + // now check router settings: + assertThrows(KnxSecureException.class, () -> { + KNXBridgeBaseThingHandler.secHelperReadBackboneKey(Optional.empty(), passwordString); + }); + String bbKeyHex = "D947B12DDECAD528B1D5A88FD347F284"; + byte[] bbKeyParsedLower = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex.toLowerCase()); + byte[] bbKeyParsedUpper = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex); + Optional bbKeyRead = KNXBridgeBaseThingHandler.secHelperReadBackboneKey(Optional.ofNullable(keys), + passwordString); + assertEquals(16, bbKeyParsedUpper.length); + assertArrayEquals(bbKeyParsedUpper, bbKeyParsedLower); + assertTrue(bbKeyRead.isPresent()); + assertArrayEquals(bbKeyParsedUpper, bbKeyRead.get()); + // System.out.print("Backbone key: \""); + // for (byte i : backboneGroupKey) + // System.out.print(String.format("%02X", i)); + // System.out.println("\""); + + // now check tunnel settings: + IndividualAddress secureTunnelSourceAddr = new IndividualAddress(1, 1, 2); + IndividualAddress noSecureTunnelSourceAddr = new IndividualAddress(2, 8, 20); + assertThrows(KnxSecureException.class, () -> { + KNXBridgeBaseThingHandler.secHelperReadTunnelConfig(Optional.empty(), passwordString, + secureTunnelSourceAddr); + }); + assertTrue(KNXBridgeBaseThingHandler + .secHelperReadTunnelConfig(Optional.ofNullable(keys), passwordString, noSecureTunnelSourceAddr) + .isEmpty()); + + var config = KNXBridgeBaseThingHandler.secHelperReadTunnelConfig(Optional.ofNullable(keys), passwordString, + secureTunnelSourceAddr); + assertTrue(config.isPresent()); + assertEquals(2, config.get().user); + + assertArrayEquals(SecureConnection.hashUserPassword("mytunnel1".toCharArray()), config.get().userKey); + assertArrayEquals(SecureConnection.hashDeviceAuthenticationPassword("myauthcode".toCharArray()), + config.get().devKey); + + // secure group addresses should contain at least one address marked as "surrogate" + final String secureAddresses = KNXBridgeBaseThingHandler.secHelperGetSecureGroupAddresses(openhabSecurity); + assertTrue(secureAddresses.contains("(S)")); + assertTrue(secureAddresses.contains("8/4/0")); + } +} diff --git a/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-ipif.knxkeys b/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-ipif.knxkeys new file mode 100644 index 0000000000000..11aaa3f7f06b8 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-ipif.knxkeys @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-sipif.knxkeys b/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-sipif.knxkeys new file mode 100644 index 0000000000000..b5811a318f73e --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-sipif.knxkeys @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-sipr.knxkeys b/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-sipr.knxkeys new file mode 100644 index 0000000000000..0ae1e61b3c5c5 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6-minimal-sipr.knxkeys @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6.knxkeys b/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6.knxkeys new file mode 100644 index 0000000000000..1182b17b9b4c6 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/resources/misc/openhab6.knxkeys @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 7a937ac21320431f646d605c55d65ba04ff594b2 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Sun, 8 Sep 2024 23:34:58 +0200 Subject: [PATCH 2/4] review comments Signed-off-by: Holger Friedrich --- bundles/org.openhab.binding.knx/README.md | 58 +++++++++---------- .../main/resources/OH-INF/i18n/knx.properties | 4 +- .../src/main/resources/OH-INF/thing/ip.xml | 5 +- .../main/resources/OH-INF/thing/serial.xml | 2 +- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/bundles/org.openhab.binding.knx/README.md b/bundles/org.openhab.binding.knx/README.md index 21450c3657df8..238bd8e0671d1 100644 --- a/bundles/org.openhab.binding.knx/README.md +++ b/bundles/org.openhab.binding.knx/README.md @@ -50,40 +50,40 @@ as multicast traffic is typically not forwarded. The IP Gateway is the most commonly used way to connect to the KNX bus. At its base, the _ip_ bridge accepts the following configuration parameters: -| Name | Required | Description | Default value | -|---------------------|--------------|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------| -| 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 | -| localSourceAddr | No | The (virtual) individual address for identification of this openHAB Thing within the KNX bus

Note: Use a free address, not the one of the interface. Or leave it at `0.0.0` and let openHAB decide which address to use. When using knxd, make sure _not to use_ one of the addresses reserved for tunneling clients. | 0.0.0 | -| useNAT | No | Whether there is network address translation between the server and the gateway | false | -| readingPause | No | Time in milliseconds of how long should be paused between two read requests to the bus during initialization | 50 | -| 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 | - | -| keyringFile | No | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure GAs. | - | -| keyringPassword | No | KNX secure: Keyring file password (set during export from ETS) | - | -| tunnelSourceAddress | No | KNX secure: Physical KNX address of tunnel in secure mode to identify tunnel. If given, openHAB will read tunnelUserId, tunnelUserPassword, tunnelDeviceAuthentication from keyring | - | +| Name | Required | Description | Default value | +|---------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------| +| 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 | +| localSourceAddr | No | The (virtual) individual address for identification of this openHAB Thing within the KNX bus

Note: Use a free address, not the one of the interface. Or leave it at `0.0.0` and let openHAB decide which address to use.
When using knxd, make sure _not to use_ one of the addresses reserved for tunneling clients. | 0.0.0 | +| useNAT | No | Whether there is network address translation between the server and the gateway | false | +| readingPause | No | Time in milliseconds of how long should be paused between two read requests to the bus during initialization | 50 | +| 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 | - | +| keyringFile | No | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure group addresses. | - | +| keyringPassword | No | KNX secure: Keyring file password (set during export from ETS) | - | +| tunnelSourceAddress | No | KNX secure: Physical KNX address of tunnel in secure mode to identify tunnel. If given, openHAB will read tunnelUserId, tunnelUserPassword, tunnelDeviceAuthentication from keyring. | - | ### Serial Gateway The _serial_ bridge accepts the following configuration parameters: -| Name | Required | Description | Default value | -|---------------------|----------|--------------------------------------------------------------------------------------------------------------|---------------| -| serialPort | Y | The serial port to use for connecting to the KNX bus | - | -| readingPause | N | Time in milliseconds of how long should be paused between two read requests to the bus during initialization | 50 | -| responseTimeout | N | Timeout in seconds to wait for a response from the KNX bus | 10 | -| readRetriesLimit | N | Limits the read retries while initialization from the KNX bus | 3 | -| autoReconnectPeriod | N | Seconds between connect retries when KNX link has been lost, 0 means never retry | 0 | -| useCemi | N | Use newer CEMI message format, useful for newer devices like KNX RF sticks, kBerry, etc. | false | -| keyringFile | N | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure GAs. | - | -| keyringPassword | N | KNX secure: Keyring file password (set during export from ETS) | - | +| Name | Required | Description | Default value | +|---------------------|----------|----------------------------------------------------------------------------------------------------------------------------------|---------------| +| serialPort | Y | The serial port to use for connecting to the KNX bus | - | +| readingPause | N | Time in milliseconds of how long should be paused between two read requests to the bus during initialization | 50 | +| responseTimeout | N | Timeout in seconds to wait for a response from the KNX bus | 10 | +| readRetriesLimit | N | Limits the read retries while initialization from the KNX bus | 3 | +| autoReconnectPeriod | N | Seconds between connect retries when KNX link has been lost, 0 means never retry | 0 | +| useCemi | N | Use newer CEMI message format, useful for newer devices like KNX RF sticks, kBerry, etc. | false | +| keyringFile | N | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure group addresses. | - | +| keyringPassword | N | KNX secure: Keyring file password (set during export from ETS) | - | ## Things 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 ccfbd898fe80e..43c67a3b0b425 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 @@ -34,7 +34,7 @@ thing-type.config.knx.ip.group.knxsecure.description = Settings for KNX secure. 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.keyringFile.label = Keyring file -thing-type.config.knx.ip.keyringFile.description = Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. knx.knxkeys. This file is mandatory to decode secure GAs. It can provide settings and credentials for IP Secure if not configured separately. +thing-type.config.knx.ip.keyringFile.description = Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. knx.knxkeys. This file is mandatory to decode secure group addresses. It can provide settings and credentials for IP Secure if not configured separately. thing-type.config.knx.ip.keyringPassword.label = Keyring password thing-type.config.knx.ip.keyringPassword.description = Keyring file password (set during export from ETS). thing-type.config.knx.ip.localIp.label = Local Network Address @@ -72,7 +72,7 @@ thing-type.config.knx.serial.autoReconnectPeriod.description = Seconds between c thing-type.config.knx.serial.group.knxsecure.label = KNX secure thing-type.config.knx.serial.group.knxsecure.description = Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation. thing-type.config.knx.serial.keyringFile.label = Keyring file -thing-type.config.knx.serial.keyringFile.description = Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. knx.knxkeys. This file is mandatory to decode secure GAs. +thing-type.config.knx.serial.keyringFile.description = Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. knx.knxkeys. This file is mandatory to decode secure group addresses. thing-type.config.knx.serial.keyringPassword.label = Keyring password thing-type.config.knx.serial.keyringPassword.description = Keyring file password (set during export from ETS). thing-type.config.knx.serial.readRetriesLimit.label = Read Retries Limit 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 06853a57b4de4..8e9310fd2ecc7 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 @@ -77,8 +77,9 @@ Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. knx.knxkeys. This file is - mandatory to decode secure GAs. It can provide settings - and credentials for IP Secure if not configured separately. + mandatory to decode secure group addresses. It can provide settings + and credentials for IP Secure if not configured + separately. true diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml index 9ff895016e6f4..5875f08f46535 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml @@ -50,7 +50,7 @@ Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. knx.knxkeys. This file is - mandatory to decode secure GAs. + mandatory to decode secure group addresses. true From ba315e7fd45d4f833f4fa0ac4dd87a9a4c1ca455 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Sun, 15 Sep 2024 19:05:23 +0200 Subject: [PATCH 3/4] Fix serial.xml, parameter groups cannot be tagged as advanced Signed-off-by: Holger Friedrich --- .../src/main/resources/OH-INF/thing/serial.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml index 5875f08f46535..b24d52171acb0 100644 --- a/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml +++ b/bundles/org.openhab.binding.knx/src/main/resources/OH-INF/thing/serial.xml @@ -8,7 +8,7 @@ This is a serial interface for accessing the KNX bus - + Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation. From dfd8a652e4c89f0c22efc360aa98f45cfb3e4b5d Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Sun, 15 Sep 2024 21:28:09 +0200 Subject: [PATCH 4/4] Review Signed-off-by: Holger Friedrich --- .../openhab/binding/knx/internal/client/AbstractKNXClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c3d1b4701554f..fd9a70f3a7106 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 @@ -238,7 +238,7 @@ private synchronized boolean connect() { deviceInfoClient = new DeviceInfoClientImpl(managementClient); // ProcessCommunicator provides main KNX communication (Calimero). - final var useGoDiagnostics = true; + final boolean useGoDiagnostics = true; ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link, sal, useGoDiagnostics); processCommunicator.responseTimeout(Duration.ofSeconds(responseTimeout)); processCommunicator.addProcessListener(processListener);