Skip to content

Commit

Permalink
[knx] Allow decoding of KNX Data Secure frames
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
holgerfriedrich committed Aug 20, 2024
1 parent ba6cef3 commit 48488cd
Show file tree
Hide file tree
Showing 23 changed files with 932 additions and 84 deletions.
20 changes: 19 additions & 1 deletion bundles/org.openhab.binding.knx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -452,24 +457,37 @@ 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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a
// href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.dpt.ValueEncoder;
import org.openhab.binding.knx.internal.handler.GroupAddressListener;
import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler;
import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler.CommandExtensionData;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.thing.ThingStatus;
Expand All @@ -51,20 +52,18 @@
import tuwien.auto.calimero.link.NetworkLinkListener;
import tuwien.auto.calimero.mgmt.Destination;
import tuwien.auto.calimero.mgmt.ManagementClient;
import tuwien.auto.calimero.mgmt.ManagementClientImpl;
import tuwien.auto.calimero.mgmt.ManagementProcedures;
import tuwien.auto.calimero.mgmt.ManagementProceduresImpl;
import tuwien.auto.calimero.mgmt.TransportLayerImpl;
import tuwien.auto.calimero.process.ProcessCommunication;
import tuwien.auto.calimero.process.ProcessCommunicator;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;
import tuwien.auto.calimero.secure.KnxSecureException;
import tuwien.auto.calimero.secure.SecureApplicationLayer;
import tuwien.auto.calimero.secure.Security;

/**
* KNX Client which encapsulates the communication with the KNX bus via the calimero libary.
* KNX Client which encapsulates the communication with the KNX bus via the calimero library.
*
* @author Simon Kaufmann - initial contribution and API.
*
Expand Down Expand Up @@ -92,6 +91,7 @@ public enum ClientState {
private final StatusUpdateCallback statusUpdateCallback;
private final ScheduledExecutorService knxScheduler;
private final CommandExtensionData commandExtensionData;
protected final Security openhabSecurity;

private @Nullable ProcessCommunicator processCommunicator;
private @Nullable ProcessCommunicationResponder responseCommunicator;
Expand Down Expand Up @@ -139,7 +139,7 @@ public void groupReadResponse(ProcessEvent e) {

public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause,
int readRetriesLimit, ScheduledExecutorService knxScheduler, CommandExtensionData commandExtensionData,
StatusUpdateCallback statusUpdateCallback) {
Security openhabSecurity, StatusUpdateCallback statusUpdateCallback) {
this.autoReconnectPeriod = autoReconnectPeriod;
this.thingUID = thingUID;
this.responseTimeout = responseTimeout;
Expand All @@ -148,6 +148,7 @@ public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int respons
this.knxScheduler = knxScheduler;
this.statusUpdateCallback = statusUpdateCallback;
this.commandExtensionData = commandExtensionData;
this.openhabSecurity = openhabSecurity;
}

public void initialize() {
Expand Down Expand Up @@ -206,38 +207,46 @@ private synchronized boolean connect() {
KNXNetworkLink link = establishConnection();
this.link = link;

// ManagementProcedures provided by Calimero: allow managing other KNX devices, e.g. check if an address is
// reachable.
// Note for KNX Secure: ManagmentProcedueresImpl currently does not provide a ctor with external SAL,
// it internally creates an instance of ManagementClientImpl, which uses
// Security.defaultInstallation().deviceToolKeys()
// Protected ctor using given ManagementClientImpl is avalable (custom class to be inherited)
managementProcedures = new ManagementProceduresImpl(link);
// one transport layer implementation, to be shared by all following classes
TransportLayerImpl tl = new TransportLayerImpl(link);

// new SecureManagement / SecureApplicationLayer, based on the keyring (if any)
// SecureManagement does not offer a public ctor which can use a given TL.
// Protected ctor using given TransportLayerImpl is available (custom class to be inherited)
// which also copies the relevant content of the supplied SAL to a new SAL instance created
// by SecureManagement ctor.
CustomSecureManagement sal = new CustomSecureManagement(tl, openhabSecurity);

logger.debug("GAs: {} Send: {}, S={}", sal.security().groupKeys().size(),
sal.security().groupSenders().size(),
KNXBridgeBaseThingHandler.secHelperGetSecureGroupAddresses(sal.security()));

// ManagementClient provided by Calimero: allow reading device info, etc.
// Note for KNX Secure: ManagementClientImpl does not provide a ctor with external SAL in Calimero 2.5,
// is uses global Security.defaultInstalltion().deviceToolKeys()
// Current main branch includes a protected ctor (custom class to be inherited)
// TODO Calimero>2.5: check if there is a new way to provide security info, there is a new protected ctor
// TODO check if we can avoid creating another ManagementClient and re-use this from ManagemntProcedures
ManagementClient managementClient = new ManagementClientImpl(link);
// Note for KNX Secure: ManagementClientImpl does not provide a ctor with external SAL in Calimero 2.5.
// Protected ctor using given ManagementClientImpl is available in >2.5 (custom class to be inherited)
ManagementClient managementClient = new CustomManagementClientImpl(link, sal);
managementClient.responseTimeout(Duration.ofSeconds(responseTimeout));
this.managementClient = managementClient;

// ManagementProcedures provided by Calimero: allow managing other KNX devices, e.g. check if an address is
// reachable.
// Note for KNX Secure: ManagementProceduresImpl currently does not provide a public ctor with external SAL.
// Protected ctor using given ManagementClientImpl is available (custom class to be inherited)
managementProcedures = new CustomManagementProceduresImpl(managementClient, tl);

// OH helper for reading device info, based on managementClient above
deviceInfoClient = new DeviceInfoClientImpl(managementClient);

// ProcessCommunicator provides main KNX communication (Calimero).
// Note for KNX Secure: SAL to be provided
ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link);
final var useGoDiagnostics = true;
ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link, sal, useGoDiagnostics);
processCommunicator.responseTimeout(Duration.ofSeconds(responseTimeout));
processCommunicator.addProcessListener(processListener);
this.processCommunicator = processCommunicator;

// ProcessCommunicationResponder provides responses to requests from KNX bus (Calimero).
// Note for KNX Secure: SAL to be provided
this.responseCommunicator = new ProcessCommunicationResponder(link,
new SecureApplicationLayer(link, Security.defaultInstallation()));
ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link, sal);
this.responseCommunicator = responseCommunicator;

// register this class, callbacks will be triggered
link.addLinkListener(this);
Expand Down Expand Up @@ -277,7 +286,7 @@ private synchronized boolean connect() {
}
}

private void disconnect(@Nullable Exception e) {
private synchronized void disconnect(@Nullable Exception e) {
disconnect(e, null);
}

Expand All @@ -294,23 +303,23 @@ private synchronized void disconnect(@Nullable Exception e, @Nullable ThingStatu

protected void releaseConnection() {
logger.debug("Bridge {} is disconnecting from KNX bus", thingUID);
var tmplink = link;
if (tmplink != null) {
tmplink.removeLinkListener(this);
var tmpLink = link;
if (tmpLink != null) {
tmpLink.removeLinkListener(this);
}
busJob = nullify(busJob, j -> j.cancel(true));
readDatapoints.clear();
responseCommunicator = nullify(responseCommunicator, rc -> {
rc.removeProcessListener(processListener);
rc.detach();
});
busJob = nullify(busJob, j -> j.cancel(true));
deviceInfoClient = null;
managementProcedures = nullify(managementProcedures, ManagementProcedures::detach);
managementClient = nullify(managementClient, ManagementClient::detach);
processCommunicator = nullify(processCommunicator, pc -> {
pc.removeProcessListener(processListener);
pc.detach();
});
deviceInfoClient = null;
managementClient = nullify(managementClient, ManagementClient::detach);
managementProcedures = nullify(managementProcedures, ManagementProcedures::detach);
responseCommunicator = nullify(responseCommunicator, rc -> {
rc.removeProcessListener(processListener);
rc.detach();
});
link = nullify(link, KNXNetworkLink::close);
logger.trace("Bridge {} disconnected from KNX bus", thingUID);
}
Expand Down Expand Up @@ -361,13 +370,20 @@ private void readNextQueuedDatapoint() {
}
ReadDatapoint datapoint = readDatapoints.poll();
if (datapoint != null) {
// TODO #8872: allow write access, currently only listening mode
if (openhabSecurity.groupKeys().containsKey(datapoint.getDatapoint().getMainAddress())) {
logger.debug("outgoing secure communication not implemented, explicit read from GA '{}' skipped",
datapoint.getDatapoint().getMainAddress());
return;
}

datapoint.incrementRetries();
try {
logger.trace("Sending a Group Read Request telegram for {}", datapoint.getDatapoint().getMainAddress());
processCommunicator.read(datapoint.getDatapoint());
} catch (KNXException e) {
// Note: KnxException does not cover KnxRuntimeException and subclasses KnxSecureException,
// KnxIllegArgumentException
// KnxIllegalArgumentException
if (datapoint.getRetries() < datapoint.getLimit()) {
readDatapoints.add(datapoint);
logger.debug("Could not read value for datapoint {}: {}. Going to retry.",
Expand Down Expand Up @@ -532,6 +548,12 @@ private void sendToKNX(ProcessCommunication communicator, GroupAddress groupAddr
return;
}

// TODO #8872: allow write access, currently only listening mode
if (openhabSecurity.groupKeys().containsKey(groupAddress)) {
logger.debug("outgoing secure communication not implemented, write to GA '{}' skipped", groupAddress);
return;
}

Datapoint datapoint = new CommandDP(groupAddress, thingUID.toString(), 0,
NORMALIZED_DPT.getOrDefault(dpt, dpt));
String mappedValue = ValueEncoder.encode(type, dpt);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.client;

import org.eclipse.jdt.annotation.NonNullByDefault;

import tuwien.auto.calimero.link.KNXLinkClosedException;
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.mgmt.ManagementClientImpl;
import tuwien.auto.calimero.mgmt.SecureManagement;

/**
* This class is to provide access to protected constructors in the Calimero library.
* Reason is to provide custom KNX keyring data.
*
* @author Holger Friedrich - initial contribution
*
*/
@NonNullByDefault
public class CustomManagementClientImpl extends ManagementClientImpl {
public CustomManagementClientImpl(final KNXNetworkLink link, final SecureManagement secureManagement)
throws KNXLinkClosedException {
// super(link, secureManagement) is available since Calimero 2.5.1
super(link, secureManagement);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.client;

import org.eclipse.jdt.annotation.NonNullByDefault;

import tuwien.auto.calimero.link.KNXLinkClosedException;
import tuwien.auto.calimero.mgmt.ManagementClient;
import tuwien.auto.calimero.mgmt.ManagementProceduresImpl;
import tuwien.auto.calimero.mgmt.TransportLayer;

/**
* This class is to provide access to protected constructors in the Calimero library.
* Reason is to provide custom KNX keyring data.
*
* @author Holger Friedrich - initial contribution
*
*/
@NonNullByDefault
public class CustomManagementProceduresImpl extends ManagementProceduresImpl {
public CustomManagementProceduresImpl(final ManagementClient mgmtClient, final TransportLayer transportLayer)
throws KNXLinkClosedException {
// super(mgmtClient, transportLayer) is protected
super(mgmtClient, transportLayer);
}
}
Loading

0 comments on commit 48488cd

Please sign in to comment.