From e48fde1beb3c74cbf61365ea27c0cca574cc7b77 Mon Sep 17 00:00:00 2001 From: Holger Friedrich Date: Tue, 8 Dec 2020 01:50:09 +0100 Subject: [PATCH] [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 | 20 +- .../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 | 243 ++++++++++++++++- .../handler/SerialBridgeThingHandler.java | 37 ++- .../main/resources/OH-INF/i18n/knx.properties | 20 +- .../src/main/resources/OH-INF/thing/ip.xml | 39 ++- .../main/resources/OH-INF/thing/serial.xml | 17 ++ .../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 +++ 19 files changed, 769 insertions(+), 44 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/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 79f4fb2f1d071..6d8cfbd856c96 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 @@ -93,6 +93,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; @@ -140,7 +141,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; @@ -149,6 +150,7 @@ public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int respons this.knxScheduler = knxScheduler; this.statusUpdateCallback = statusUpdateCallback; this.commandExtensionData = commandExtensionData; + this.openhabSecurity = openhabSecurity; } public void initialize() { @@ -189,7 +191,6 @@ private synchronized boolean connect() { logger.trace("connect() ignored, closing down"); return false; } - if (isConnected()) { return true; } @@ -360,7 +361,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()); @@ -532,9 +541,16 @@ 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); + if (mappedValue == null) { logger.debug("Value '{}' of type '{}' cannot be mapped to datapoint '{}'", type, type.getClass(), datapoint); 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..7450ff7284bbf 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: {}", + secHelperGetSecureGroupAdresses(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..2ed88e6f76253 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,11 +195,89 @@ 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 undecladed 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 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; } @@ -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 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 5cb4692de9655..7665549542e66 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 @@ -38,12 +41,10 @@ @NonNullByDefault public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler { - private @Nullable SerialClient client = null; - private @Nullable Future initJob = null; - private final Logger logger = LoggerFactory.getLogger(SerialBridgeThingHandler.class); - + private @Nullable SerialClient client = null; private final SerialPortManager serialPortManager; + private @Nullable Future initJob = null; public SerialBridgeThingHandler(Bridge bridge, final SerialPortManager serialPortManager) { super(bridge); @@ -57,7 +58,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 +66,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: {}", + secHelperGetSecureGroupAdresses(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..0ab66ca4b1e4f 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 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/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..128c5b5cb1bbb --- /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-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.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 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/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