From c7114f50bed46a35f7e3d317709943c2d95f7ae5 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Tue, 8 Dec 2020 01:50:09 +0100 Subject: [PATCH] [knx] Add initial support for KNX secure [WIP] * add support for KNX IP Secure, new options SECURETUNNEL and SECUREROUTER * add config options for keyring file and password, and credentials for secure connections * add passive (listening only) access for KNX data secure frames, #8872 * add tests for security functions * add useCEMI option for newer serial devices like KNX RF sticks, kBerry, etc., inspired by #10407 * update user documentation Signed-off-by: Holger Friedrich --- bundles/org.openhab.binding.knx/README.md | 33 +- .../knx/internal/KNXBindingConstants.java | 8 + .../knx/internal/channel/TypeColor.java | 3 +- .../knx/internal/channel/TypeDimmer.java | 3 +- .../internal/channel/TypeRollershutter.java | 3 +- .../internal/client/AbstractKNXClient.java | 54 ++- .../client/CustomKNXNetworkLinkIP.java | 1 + .../binding/knx/internal/client/IPClient.java | 136 +++++++- .../knx/internal/client/SerialClient.java | 17 +- .../internal/config/BridgeConfiguration.java | 10 + .../config/IPBridgeConfiguration.java | 25 ++ .../config/SerialBridgeConfiguration.java | 5 + .../handler/AbstractKNXThingHandler.java | 5 +- .../internal/handler/DeviceThingHandler.java | 4 +- .../handler/IPBridgeThingHandler.java | 137 +++++++- .../handler/KNXBridgeBaseThingHandler.java | 326 ++++++++++++++++++ .../handler/SerialBridgeThingHandler.java | 40 ++- .../main/resources/OH-INF/i18n/knx.properties | 2 +- .../src/main/resources/OH-INF/thing/ip.xml | 61 +++- .../main/resources/OH-INF/thing/serial.xml | 22 ++ .../internal/security/KNXSecurityTest.java | 244 +++++++++++++ .../resources/openhab6-minimal-ipif.knxkeys | 7 + .../resources/openhab6-minimal-sipif.knxkeys | 13 + .../resources/openhab6-minimal-sipr.knxkeys | 13 + .../src/test/resources/openhab6.knxkeys | 35 ++ 25 files changed, 1152 insertions(+), 55 deletions(-) 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/openhab6-minimal-ipif.knxkeys create mode 100644 bundles/org.openhab.binding.knx/src/test/resources/openhab6-minimal-sipif.knxkeys create mode 100644 bundles/org.openhab.binding.knx/src/test/resources/openhab6-minimal-sipr.knxkeys create mode 100644 bundles/org.openhab.binding.knx/src/test/resources/openhab6.knxkeys diff --git a/bundles/org.openhab.binding.knx/README.md b/bundles/org.openhab.binding.knx/README.md index 5339c28629775..14891f96d0918 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 | @@ -52,6 +52,7 @@ The *serial* bridge accepts the following configuration parameters: | 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, could be useful for newer devices like KNX RF sticks, kBerry, etc. | false | ## Things @@ -199,6 +200,36 @@ Each configuration parameter has a `mainGA` where commands are written to and op The `dpt` element is optional. If ommitted, 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 either a KNX Secure Router or a Secure IP Interface 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. There are two possible ways to provide the key to openHAB: + +- The backbone key can be extracted Security report (ETS, Reports, Security, look for a 32-digit key) and specified in parameter `backboneKey`. +- 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 `keyringPasswort`. + +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: `tunnelDevAuth`, `tunnelPasswort`. `tunnelUser` 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, ...) +- 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 `keyringPasswort`. In addition, `tunnelSourceAddr` needs to be set to uniquely identify the tunnel in use. + + +### KNX Data Secure + +Data secure protects the content of messages on the KNX bus. In a KNX installation, both classic and secure group addresses can coexist. + +openHAB typically 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 secured GAs is done via the "GO diagnostics" feature described in KNX AN170 and is currently limited. Expect a timeout if a data value is written too often. 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 `keyringPasswort`. + + ## 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 1896c04a27cfe..260fa395c367f 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 @@ -53,6 +53,14 @@ public class KNXBindingConstants { public static final String LOCAL_SOURCE_ADDRESS = "localSourceAddr"; 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/channel/TypeColor.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeColor.java index a0cecb897877a..fc71b8521f27b 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeColor.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeColor.java @@ -40,7 +40,8 @@ class TypeColor extends KNXChannelType { @Override protected Set getAllGAKeys() { - return Stream.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA, HSB_GA).collect(toSet()); + final var tmp = Stream.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA, HSB_GA).collect(toSet()); + return tmp; } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java index 46cc6661d1669..70386c3c94ff8 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeDimmer.java @@ -40,7 +40,8 @@ class TypeDimmer extends KNXChannelType { @Override protected Set getAllGAKeys() { - return Stream.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA).collect(toSet()); + final var tmp = Stream.of(SWITCH_GA, POSITION_GA, INCREASE_DECREASE_GA).collect(toSet()); + return tmp; } @Override diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java index 2e0b300991396..b5af5cb857a9a 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/channel/TypeRollershutter.java @@ -53,6 +53,7 @@ protected String getDefaultDPT(String gaConfigKey) { @Override protected Set getAllGAKeys() { - return Stream.of(UP_DOWN_GA, STOP_MOVE_GA, POSITION_GA).collect(toSet()); + final var tmp = Stream.of(UP_DOWN_GA, STOP_MOVE_GA, POSITION_GA).collect(toSet()); + return tmp; } } 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 88e3386c8dd6d..0f87e9911f9b0 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 @@ -39,6 +39,7 @@ import tuwien.auto.calimero.GroupAddress; import tuwien.auto.calimero.IndividualAddress; import tuwien.auto.calimero.KNXException; +import tuwien.auto.calimero.KnxRuntimeException; import tuwien.auto.calimero.datapoint.CommandDP; import tuwien.auto.calimero.datapoint.Datapoint; import tuwien.auto.calimero.device.ProcessCommunicationResponder; @@ -54,6 +55,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; @@ -78,6 +80,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien private final int readRetriesLimit; private final StatusUpdateCallback statusUpdateCallback; private final ScheduledExecutorService knxScheduler; + protected final Security openhabSecurity; private @Nullable ProcessCommunicator processCommunicator; private @Nullable ProcessCommunicationResponder responseCommunicator; @@ -91,6 +94,9 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien private final Set groupAddressListeners = new CopyOnWriteArraySet<>(); private final LinkedBlockingQueue readDatapoints = new LinkedBlockingQueue<>(); + private boolean firstConnect; + private long lastDisconnectSysMillis; + @FunctionalInterface private interface ListenerNotification { void apply(BusMessageListener listener, IndividualAddress source, GroupAddress destination, byte[] asdu); @@ -101,6 +107,7 @@ private interface ListenerNotification { @Override public void detached(DetachEvent e) { + lastDisconnectSysMillis = System.currentTimeMillis(); logger.debug("The KNX network link was detached from the process communicator"); } @@ -127,7 +134,8 @@ public void groupReadResponse(ProcessEvent e) { }; public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause, - int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback) { + int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback, + Security openhabSecurity) { this.autoReconnectPeriod = autoReconnectPeriod; this.thingUID = thingUID; this.responseTimeout = responseTimeout; @@ -135,6 +143,9 @@ public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int respons this.readRetriesLimit = readRetriesLimit; this.knxScheduler = knxScheduler; this.statusUpdateCallback = statusUpdateCallback; + this.openhabSecurity = openhabSecurity; + firstConnect = true; + lastDisconnectSysMillis = System.currentTimeMillis(); } public void initialize() { @@ -145,7 +156,9 @@ 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 + connectJob = knxScheduler.schedule(this::connect, firstConnect ? 1 : autoReconnectPeriod, TimeUnit.SECONDS); + firstConnect = false; return true; } else { return false; @@ -173,6 +186,12 @@ private synchronized boolean connect() { if (isConnected()) { return true; } + long now = System.currentTimeMillis(); + if ((now - lastDisconnectSysMillis) < 1000) { + logger.debug("fast reconnect"); + // SECURETUNNEL: TCP Connections need additional efforts to avoid grabbing all tunnel connections + // the interface, this is implemented in IPClient + } try { releaseConnection(); @@ -195,7 +214,7 @@ private synchronized boolean connect() { this.processCommunicator = processCommunicator; ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link, - new SecureApplicationLayer(link, Security.defaultInstallation())); + new SecureApplicationLayer(link, openhabSecurity)); this.responseCommunicator = responseCommunicator; link.addLinkListener(this); @@ -206,7 +225,7 @@ private synchronized boolean connect() { statusUpdateCallback.updateStatus(ThingStatus.ONLINE); connectJob = null; return true; - } catch (KNXException | InterruptedException e) { + } catch (KNXException | InterruptedException | KnxSecureException e) { logger.debug("Error connecting to the bus: {}", e.getMessage(), e); disconnect(e); scheduleReconnectJob(); @@ -215,18 +234,19 @@ private synchronized boolean connect() { } private void disconnect(@Nullable Exception e) { + lastDisconnectSysMillis = System.currentTimeMillis(); + releaseConnection(); if (e != null) { - String message = e.getLocalizedMessage(); statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - message != null ? message : ""); + "" + e.getLocalizedMessage()); } else { statusUpdateCallback.updateStatus(ThingStatus.OFFLINE); } } @SuppressWarnings("null") - private void releaseConnection() { + protected void releaseConnection() { logger.debug("Bridge {} is disconnecting from the KNX bus", thingUID); readDatapoints.clear(); busJob = nullify(busJob, j -> j.cancel(true)); @@ -275,6 +295,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()) { @@ -285,7 +306,15 @@ private void readNextQueuedDatapoint() { return; } 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()); @@ -299,6 +328,11 @@ private void readNextQueuedDatapoint() { logger.warn("Giving up reading datapoint {}, the number of maximum retries ({}) is reached.", datapoint.getDatapoint().getMainAddress(), datapoint.getLimit()); } + } catch (KnxRuntimeException e) { + // KnxRuntimeException is _not_ a subclass of KnxException. Fail gracefully for this case as well, + // fixes #7239 + logger.warn("Error reading datapoint {}: {}", datapoint.getDatapoint().getMainAddress(), + e.getMessage()); } catch (InterruptedException e) { logger.debug("Interrupted sending KNX read request"); return; @@ -450,6 +484,12 @@ private void sendToKNX(ProcessCommunication communicator, KNXNetworkLink link, G 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, dpt); String mappedValue = toDPTValue(type, dpt); 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/IPClient.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/client/IPClient.java index 045fefadb63fe..94ee8b53eeb47 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,10 +33,14 @@ 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; import tuwien.auto.calimero.link.medium.TPSettings; +import tuwien.auto.calimero.secure.Security; /** * IP specific {@link AbstractKNXClient} implementation. @@ -46,59 +51,106 @@ @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 = 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; + + @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, + Security openhabSecurity) { super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler, - statusUpdateCallback); + statusUpdateCallback, openhabSecurity); this.ipConnectionType = ipConnectionType; this.ip = ip; this.localSource = localSource; this.port = port; this.localEndPoint = localEndPoint; this.useNAT = useNAT; + this.secureRoutingBackboneGroupKey = secureRoutingBackboneGroupKey; + this.secureRoutingLatencyToleranceMs = secureRoutingLatencyToleranceMs; + this.secureTunnelDevKey = secureTunnelDevKey; + this.secureTunnelUser = secureTunnelUser; + this.secureTunnelUserKey = secureTunnelUserKey; + tcpSession = null; } @Override protected KNXNetworkLink establishConnection() throws KNXException, InterruptedException { - logger.debug("Establishing connection to KNX bus on {}:{} in mode {}.", ip, port, connectionTypeToString()); + logger.info("Establishing connection to KNX bus on {}:{} in mode {}.", ip, port, connectionTypeToString()); TPSettings settings = new TPSettings(new IndividualAddress(localSource)); return createKNXNetworkLinkIP(ipConnectionType, localEndPoint, new InetSocketAddress(ip, port), useNAT, settings); } 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,29 @@ 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) { + // the follwing would create a secure conn via UDP, default should be TCP + // conn = SecureConnection.newTunneling(TunnelingLayer.LinkLayer, local, remoteEP, useNAT, + // secureTunnelDevKey, secureTunnelUser, secureTunnelUserKey); + + // TODO: possibly implement TCP connection management + logger.debug("creating new TCP connection"); + // using .clone for the keys is essential - otherwise Calimero clears the array and a reconnect will + // fail + if (tcpSession != null) { + logger.warn("tcpSession might still be open"); + } + tcpSession = TcpConnection.newTcpConnection(localEP, remoteEP).newSecureSession(secureTunnelUser, + secureTunnelUserKey.clone(), secureTunnelDevKey.clone()); + logger.debug("creating new tunnel"); + 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 +191,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("force close TCP connection"); + try { + toBeClosed.close(); + try { + Thread.sleep(PAUSE_ON_TCP_SESSION_CLOSE); + } catch (InterruptedException e) { + } + } catch (Exception e) { + logger.warn("closing TCP connection failed: {}", e.getMessage()); + } + } + } + + @Override + protected void releaseConnection() { + super.releaseConnection(); + closeTcpConnection(); + } } 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 3f6113837c901..aa219d05cf4ca 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 @@ -26,6 +26,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; /** * Serial specific {@link AbstractKNXClient} implementation. @@ -39,20 +40,28 @@ public class SerialClient extends AbstractKNXClient { private final Logger logger = LoggerFactory.getLogger(SerialClient.class); private final String serialPort; + private final boolean useCemi; public SerialClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause, - int readRetriesLimit, ScheduledExecutorService knxScheduler, String serialPort, - StatusUpdateCallback statusUpdateCallback) { + int readRetriesLimit, ScheduledExecutorService knxScheduler, String serialPort, boolean useCemi, + StatusUpdateCallback statusUpdateCallback, Security openhabSecurity) { super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler, - statusUpdateCallback); + statusUpdateCallback, openhabSecurity); this.serialPort = serialPort; + this.useCemi = useCemi; } @Override protected KNXNetworkLink establishConnection() throws KNXException, InterruptedException { try { RXTXVersion.getVersion(); - logger.debug("Establishing connection to KNX bus through FT1.2 on serial port {}.", serialPort); + logger.debug("Establishing connection to KNX bus through FT1.2 on serial port {}{}.", serialPort, + (useCemi ? " using CEMI" : "")); + // cemi support by Calimero library, may be userful for newer serial devices like KNX RF sticks, kBerry, + // etc. + if (useCemi) { + return KNXNetworkLinkFT12.newCemiLink(serialPort, new TPSettings()); + } return new KNXNetworkLinkFT12(serialPort, new TPSettings()); } catch (NoClassDefFoundError e) { 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 3c0909db17c51..25a565324be98 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 @@ -28,6 +28,8 @@ public class BridgeConfiguration { private BigDecimal readingPause = BigDecimal.valueOf(0); private BigDecimal readRetriesLimit = BigDecimal.valueOf(0); private BigDecimal responseTimeout = BigDecimal.valueOf(0); + private String keyringFile = ""; + private String keyringPassword = ""; public int getAutoReconnectPeriod() { return autoReconnectPeriod; @@ -48,4 +50,12 @@ public BigDecimal 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 6c6b8816aab71..68a8a86864d11 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,11 @@ 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 = ""; + private String tunnelSourceAddress = ""; public Boolean getUseNAT() { return useNAT; @@ -55,4 +60,24 @@ 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; + } + + public String getTunnelSourceAddress() { + return tunnelSourceAddress; + } } diff --git a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/SerialBridgeConfiguration.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/SerialBridgeConfiguration.java index b267ad3338dd0..791c45ffd132d 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/SerialBridgeConfiguration.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/config/SerialBridgeConfiguration.java @@ -24,8 +24,13 @@ public class SerialBridgeConfiguration extends BridgeConfiguration { private String serialPort = ""; + private boolean useCemi = false; public String getSerialPort() { return serialPort; } + + public boolean useCemi() { + return useCemi; + } } 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..5fc2bf886b3ea 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,7 @@ 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()); 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/DeviceThingHandler.java b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/DeviceThingHandler.java index d017dca4c6c75..9dc50abc8fba7 100644 --- a/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/DeviceThingHandler.java +++ b/bundles/org.openhab.binding.knx/src/main/java/org/openhab/binding/knx/internal/handler/DeviceThingHandler.java @@ -72,8 +72,8 @@ public class DeviceThingHandler extends AbstractKNXThingHandler { private final Set groupAddresses = new HashSet<>(); private final Set groupAddressesWriteBlockedOnce = new HashSet<>(); private final Set groupAddressesRespondingSpec = new HashSet<>(); - private final Map> readFutures = new HashMap<>(); - private final Map> channelFutures = new HashMap<>(); + private final Map> readFutures = new HashMap<>(); + private final Map> channelFutures = new HashMap<>(); private int readInterval; public DeviceThingHandler(Thing thing) { 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..2a4a31b59af83 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; @@ -43,6 +43,9 @@ 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); @@ -56,7 +59,62 @@ 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 + initJob = scheduler.submit(() -> { + initializeLater(); + }); + } + + public void initializeLater() { + logger.trace("KNX scheduled initialization started"); + + dispose(); + IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class); + boolean securityAvailable = false; + try { + if (!config.getKeyringFile().isEmpty()) { + logger.info("KNX secure: keyring is configured, opening may take some time"); + } + + securityAvailable = initializeSecurity(config.getKeyringFile(), config.getKeyringPassword(), + config.getRouterBackboneKey(), config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), + config.getTunnelUserPassword(), config.getTunnelSourceAddress()); + if (securityAvailable) { + logger.info("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.info("KNX secure: tunnel keys are {} set", (tunnelOk ? "properly" : "not")); + logger.info("KNX secure: keyring is {}available", (keyring.isPresent() ? "" : "not ")); + 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: {}", + secHelperGetSecureGroupAdresses(openhabSecurity)); + } + + } else { + logger.debug("KNX security not configured"); + } + } catch (Exception e) { + // KnxSecureException or others + if (e instanceof NullPointerException) { + logger.warn("{}: {}", e.toString(), e.getStackTrace()); + } else { + logger.warn("{}", 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 +128,59 @@ 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) { + 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) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Security configuration for secure tunnel is incomplete"); + return; + } + + logger.info("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) { + 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 or keyring + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "backboneGroupKey required for secure routing; please check keyring or configure manually"); + return; + } + logger.info("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)); + 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 +192,35 @@ 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, + openhabSecurity); final var tmpClient = client; if (tmpClient != null) { tmpClient.initialize(); } + + logger.trace("KNX scheduled initialization completed"); } @Override public void dispose() { - super.dispose(); - final var tmpClient = client; - if (tmpClient != null) { - tmpClient.dispose(); - client = null; + { + final var tmpInitJob = initJob; + if (tmpInitJob != null) { + tmpInitJob.cancel(true); + } + } + { + 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..c96c9816618f2 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,6 +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.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -20,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; @@ -28,28 +38,344 @@ 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.mgmt.Destination; +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; /** * The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are * sent to one of the channels. * * @author Simon Kaufmann - Initial contribution and API + * -- */ @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 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; public KNXBridgeBaseThingHandler(Bridge bridge) { super(bridge); + keyring = Optional.empty(); + openhabSecurity = Security.newSecurity(); + secureRouting = new SecureRoutingConfig(); + secureTunnel = new SecureTunnelConfig(); } protected abstract KNXClient getClient(); + /*** + * 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) { + return initializeSecurity(cKeyringFile, cKeyringPassword, "", "", "", "", ""); + } + + /*** + * Initialize KNX secure if configured (full interface) + * + * @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 cKeyringFile, String cKeyringPassword, String cRouterBackboneGroupKey, + String cTunnelDevAuth, String cTunnelUser, String cTunnelPassword, String cTunnelSourceAddr) { + 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 (typically it is read from + // keyring) + if (!cRouterBackboneGroupKey.isBlank()) { + // provided in conig, this will override whatever is read from keyring + String tmp = cRouterBackboneGroupKey.trim().replaceFirst("^0x", "").trim().replace(" ", ""); + if (!tmp.isEmpty()) { + // helper may throw KnxSecureException + secureRouting.backboneGroupKey = secHelperParseBackboneKey(tmp); + securityInitialized = true; + } + } + + // 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()) { + String pwd = cTunnelDevAuth.trim(); + secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(pwd.toCharArray()); + securityInitialized = true; + } + if (!cTunnelPassword.isBlank()) { + String pwd = cTunnelPassword.trim(); + secureTunnel.userKey = SecureConnection.hashUserPassword(pwd.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 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.trim(); + keyring = Optional.ofNullable(Keyring.load(keyringUri)); + 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 (Exception e) { + // load() may throw KnxSecureException or other undecladed exceptions, e.g. UncheckedIOException when + // file is not found + keyring = Optional.empty(); + keyringPassword = ""; + throw 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, 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 parametes 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 + * @return key in byte array format + */ + public static byte[] secHelperParseBackboneKey(String hexstring) throws NumberFormatException, 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"); + } + 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 adresses 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 secHelperGetSecureGroupAdresses(final Security openhabSecurity) { + Map> groupSendersWithSurrogate = new HashMap>(); + final Map> senders = openhabSecurity.groupSenders(); + for (var entry : senders.entrySet()) { + final GroupAddress ga = entry.getKey(); + // the following aproach is uses by Calimero to decuce 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 5e6738646a463..4ee44a9464f5b 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 @@ -12,12 +12,19 @@ */ package org.openhab.binding.knx.internal.handler; +import java.util.concurrent.Future; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.knx.internal.client.AbstractKNXClient; import org.openhab.binding.knx.internal.client.SerialClient; import org.openhab.binding.knx.internal.config.SerialBridgeConfiguration; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ThingStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import tuwien.auto.calimero.secure.KnxSecureException; /** * The {@link IPBridgeThingHandler} is responsible for handling commands, which are @@ -31,26 +38,55 @@ @NonNullByDefault public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(SerialBridgeThingHandler.class); private final SerialClient client; + private @Nullable Future initJob = null; public SerialBridgeThingHandler(Bridge bridge) { super(bridge); SerialBridgeConfiguration config = getConfigAs(SerialBridgeConfiguration.class); client = new SerialClient(config.getAutoReconnectPeriod(), thing.getUID(), config.getResponseTimeout().intValue(), config.getReadingPause().intValue(), - config.getReadRetriesLimit().intValue(), getScheduler(), config.getSerialPort(), this); + config.getReadRetriesLimit().intValue(), getScheduler(), config.getSerialPort(), config.useCemi(), this, + openhabSecurity); } @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() { + 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: {}", + secHelperGetSecureGroupAdresses(openhabSecurity)); + } + } + } catch (KnxSecureException e) { + logger.warn("{}", e.toString()); + } client.initialize(); } @Override public void dispose() { - super.dispose(); + final var initJ = initJob; + if (initJ != null) { + initJ.cancel(true); + } 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 01c6da0176556..1c71cd75309bd 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 @@ -39,7 +39,7 @@ 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.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.useNAT.label = Use NAT 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..f990406427bcd 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,20 @@ This is a KNX IP interface or router + + + Settings for KNX secure. Requires KNX secure features to be active in KNX installation. + true + - 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 +72,57 @@ Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s 60 + + + KNX secure, optional: Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. + knx.knxkeys. This file is mandatory to decode secure GAs and optional to provide IP secure settings if not + specified. + + + + password + + KNX secure, optional: Keyring file password (set during export from ETS) + + + + password + + KNX secure, optional: Backbone key for secure router mode. Typically read from keyring file. 16 bytes + in hex notation. Can also be found in ETS security report. + + + + + KNX secure, optional: 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. + + + + + KNX secure, optional: Tunnel user id for secure tunnel mode. Optional, can be read from keyring file + if + tunnelSourceAddr is configured. + + + + password + + KNX secure, optional: Tunnel user key for secure tunnel mode. Optional, can be read from keyring file + if tunnelSourceAddr is configured. + + + + password + + KNX secure, optional: Tunnel device authentication for secure tunnel mode. Optional, can be read from + keyring file if tunnelSourceAddr is configured. + + 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 bb7705928889d..ed697fea1b08c 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. Requires KNX secure features to be active in KNX installation. + serial-port @@ -34,6 +38,24 @@ Seconds between connect retries when KNX link has been lost, 0 means never retry 0 + + + Use newer CEMI frame format. May be useful for newer serial devices like KNX RF sticks, kBerry, etc. + false + true + + + + KNX secure, optional: Keyring file exported from ETS and placed in openHAB config/misc folder, e.g. + knx.knxkeys. This file is mandatory to decode secure GAs. + + + + password + + KNX secure, optional: Keyring file password (set during export from ETS) + + 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..989eba0b5603c --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/java/org/openhab/binding/knx/internal/security/KNXSecurityTest.java @@ -0,0 +1,244 @@ +/** + * 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.security; + +import static org.junit.jupiter.api.Assertions.*; + +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("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("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("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("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("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 adresses should contain at least one address marked as "surrogate" + final String secureAdresses = KNXBridgeBaseThingHandler.secHelperGetSecureGroupAdresses(openhabSecurity); + assertTrue(secureAdresses.contains("(S)")); + assertTrue(secureAdresses.contains("8/4/0")); + } +} diff --git a/bundles/org.openhab.binding.knx/src/test/resources/openhab6-minimal-ipif.knxkeys b/bundles/org.openhab.binding.knx/src/test/resources/openhab6-minimal-ipif.knxkeys new file mode 100644 index 0000000000000..11aaa3f7f06b8 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/resources/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/openhab6-minimal-sipif.knxkeys b/bundles/org.openhab.binding.knx/src/test/resources/openhab6-minimal-sipif.knxkeys new file mode 100644 index 0000000000000..b5811a318f73e --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/resources/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/openhab6-minimal-sipr.knxkeys b/bundles/org.openhab.binding.knx/src/test/resources/openhab6-minimal-sipr.knxkeys new file mode 100644 index 0000000000000..0ae1e61b3c5c5 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/resources/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/openhab6.knxkeys b/bundles/org.openhab.binding.knx/src/test/resources/openhab6.knxkeys new file mode 100644 index 0000000000000..1182b17b9b4c6 --- /dev/null +++ b/bundles/org.openhab.binding.knx/src/test/resources/openhab6.knxkeys @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file