[knx] Allow decoding of KNX Data Secure frames (#12434)

* [knx] Allow decoding of KNX Data Secure frames

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
This commit is contained in:
Holger Friedrich 2024-09-15 21:55:30 +02:00 committed by GitHub
parent 104f32892e
commit dbc607abcf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 958 additions and 109 deletions

View File

@ -50,35 +50,40 @@ as multicast traffic is typically not forwarded.
The IP Gateway is the most commonly used way to connect to the KNX bus.
At its base, the _ip_ bridge accepts the following configuration parameters:
| Name | Required | Description | Default value |
|---------------------|--------------|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------|
| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL`, `ROUTER`, `SECURETUNNEL` or `SECUREROUTER`) | - |
| ipAddress | for `TUNNEL` | Network address of the KNX/IP gateway. If type `ROUTER` is set, the IPv4 Multicast Address can be set. | for `TUNNEL`: \<nothing\>, for `ROUTER`: 224.0.23.12 |
| portNumber | for `TUNNEL` | Port number of the KNX/IP gateway | 3671 |
| localIp | No | Network address of the local host to be used to set up the connection to the KNX/IP gateway | the system-wide configured primary interface address |
| localSourceAddr | No | The (virtual) individual address for identification of this openHAB Thing within the KNX bus <br/><br/>Note: Use a free address, not the one of the interface. Or leave it at `0.0.0` and let openHAB decide which address to use. When using knxd, make sure _not to use_ one of the addresses reserved for tunneling clients. | 0.0.0 |
| useNAT | No | Whether there is network address translation between the server and the gateway | false |
| readingPause | No | Time in milliseconds of how long should be paused between two read requests to the bus during initialization | 50 |
| responseTimeout | No | Timeout in seconds to wait for a response from the KNX bus | 10 |
| readRetriesLimit | No | Limits the read retries while initialization from the KNX bus | 3 |
| autoReconnectPeriod | No | Seconds between connect retries when KNX link has been lost (0 means never). | 0 |
| routerBackboneKey | No | KNX secure: Backbone key for secure router mode | - |
| tunnelUserId | No | KNX secure: Tunnel user id for secure tunnel mode (if specified, it must be a number >0) | - |
| tunnelUserPassword | No | KNX secure: Tunnel user key for secure tunnel mode | - |
| tunnelDeviceAuthentication | No | KNX secure: Tunnel device authentication for secure tunnel mode | - |
| Name | Required | Description | Default value |
|---------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------|
| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL`, `ROUTER`, `SECURETUNNEL` or `SECUREROUTER`) | - |
| ipAddress | for `TUNNEL` | Network address of the KNX/IP gateway. If type `ROUTER` is set, the IPv4 Multicast Address can be set. | for `TUNNEL`: \<nothing\>, for `ROUTER`: 224.0.23.12 |
| portNumber | for `TUNNEL` | Port number of the KNX/IP gateway | 3671 |
| localIp | No | Network address of the local host to be used to set up the connection to the KNX/IP gateway | the system-wide configured primary interface address |
| localSourceAddr | No | The (virtual) individual address for identification of this openHAB Thing within the KNX bus <br/><br/>Note: Use a free address, not the one of the interface. Or leave it at `0.0.0` and let openHAB decide which address to use.<br/>When using knxd, make sure _not to use_ one of the addresses reserved for tunneling clients. | 0.0.0 |
| useNAT | No | Whether there is network address translation between the server and the gateway | false |
| readingPause | No | Time in milliseconds of how long should be paused between two read requests to the bus during initialization | 50 |
| responseTimeout | No | Timeout in seconds to wait for a response from the KNX bus | 10 |
| readRetriesLimit | No | Limits the read retries while initialization from the KNX bus | 3 |
| autoReconnectPeriod | No | Seconds between connect retries when KNX link has been lost (0 means never). | 0 |
| routerBackboneKey | No | KNX secure: Backbone key for secure router mode | - |
| tunnelUserId | No | KNX secure: Tunnel user id for secure tunnel mode (if specified, it must be a number >0) | - |
| tunnelUserPassword | No | KNX secure: Tunnel user key for secure tunnel mode | - |
| tunnelDeviceAuthentication | No | KNX secure: Tunnel device authentication for secure tunnel mode | - |
| keyringFile | No | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure group addresses. | - |
| keyringPassword | No | KNX secure: Keyring file password (set during export from ETS) | - |
| tunnelSourceAddress | No | KNX secure: Physical KNX address of tunnel in secure mode to identify tunnel. If given, openHAB will read tunnelUserId, tunnelUserPassword, tunnelDeviceAuthentication from keyring. | - |
### Serial Gateway
The _serial_ bridge accepts the following configuration parameters:
| Name | Required | Description | Default value |
|---------------------|----------|--------------------------------------------------------------------------------------------------------------|---------------|
| serialPort | Y | The serial port to use for connecting to the KNX bus | - |
| readingPause | N | Time in milliseconds of how long should be paused between two read requests to the bus during initialization | 50 |
| responseTimeout | N | Timeout in seconds to wait for a response from the KNX bus | 10 |
| readRetriesLimit | N | Limits the read retries while initialization from the KNX bus | 3 |
| autoReconnectPeriod | N | Seconds between connect retries when KNX link has been lost, 0 means never retry | 0 |
| useCemi | N | Use newer CEMI message format, useful for newer devices like KNX RF sticks, kBerry, etc. | false |
| Name | Required | Description | Default value |
|---------------------|----------|----------------------------------------------------------------------------------------------------------------------------------|---------------|
| serialPort | Y | The serial port to use for connecting to the KNX bus | - |
| readingPause | N | Time in milliseconds of how long should be paused between two read requests to the bus during initialization | 50 |
| responseTimeout | N | Timeout in seconds to wait for a response from the KNX bus | 10 |
| readRetriesLimit | N | Limits the read retries while initialization from the KNX bus | 3 |
| autoReconnectPeriod | N | Seconds between connect retries when KNX link has been lost, 0 means never retry | 0 |
| useCemi | N | Use newer CEMI message format, useful for newer devices like KNX RF sticks, kBerry, etc. | false |
| keyringFile | N | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure group addresses. | - |
| keyringPassword | N | KNX secure: Keyring file password (set during export from ETS) | - |
## Things
@ -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

View File

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

View File

@ -28,6 +28,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.dpt.ValueEncoder;
import org.openhab.binding.knx.internal.handler.GroupAddressListener;
import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler;
import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler.CommandExtensionData;
import org.openhab.binding.knx.internal.i18n.KNXTranslationProvider;
import org.openhab.core.thing.ThingStatus;
@ -51,20 +52,18 @@ import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.link.NetworkLinkListener;
import tuwien.auto.calimero.mgmt.Destination;
import tuwien.auto.calimero.mgmt.ManagementClient;
import tuwien.auto.calimero.mgmt.ManagementClientImpl;
import tuwien.auto.calimero.mgmt.ManagementProcedures;
import tuwien.auto.calimero.mgmt.ManagementProceduresImpl;
import tuwien.auto.calimero.mgmt.TransportLayerImpl;
import tuwien.auto.calimero.process.ProcessCommunication;
import tuwien.auto.calimero.process.ProcessCommunicator;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;
import tuwien.auto.calimero.secure.KnxSecureException;
import tuwien.auto.calimero.secure.SecureApplicationLayer;
import tuwien.auto.calimero.secure.Security;
/**
* KNX Client which encapsulates the communication with the KNX bus via the calimero libary.
* KNX Client which encapsulates the communication with the KNX bus via the calimero library.
*
* @author Simon Kaufmann - initial contribution and API.
*
@ -92,6 +91,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
private final StatusUpdateCallback statusUpdateCallback;
private final ScheduledExecutorService knxScheduler;
private final CommandExtensionData commandExtensionData;
protected final Security openhabSecurity;
private @Nullable ProcessCommunicator processCommunicator;
private @Nullable ProcessCommunicationResponder responseCommunicator;
@ -139,7 +139,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
public AbstractKNXClient(int autoReconnectPeriod, ThingUID thingUID, int responseTimeout, int readingPause,
int readRetriesLimit, ScheduledExecutorService knxScheduler, CommandExtensionData commandExtensionData,
StatusUpdateCallback statusUpdateCallback) {
Security openhabSecurity, StatusUpdateCallback statusUpdateCallback) {
this.autoReconnectPeriod = autoReconnectPeriod;
this.thingUID = thingUID;
this.responseTimeout = responseTimeout;
@ -148,6 +148,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
this.knxScheduler = knxScheduler;
this.statusUpdateCallback = statusUpdateCallback;
this.commandExtensionData = commandExtensionData;
this.openhabSecurity = openhabSecurity;
}
public void initialize() {
@ -206,38 +207,46 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
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 boolean useGoDiagnostics = true;
ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link, sal, useGoDiagnostics);
processCommunicator.responseTimeout(Duration.ofSeconds(responseTimeout));
processCommunicator.addProcessListener(processListener);
this.processCommunicator = processCommunicator;
// ProcessCommunicationResponder provides responses to requests from KNX bus (Calimero).
// Note for KNX Secure: SAL to be provided
this.responseCommunicator = new ProcessCommunicationResponder(link,
new SecureApplicationLayer(link, Security.defaultInstallation()));
ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link, sal);
this.responseCommunicator = responseCommunicator;
// register this class, callbacks will be triggered
link.addLinkListener(this);
@ -277,7 +286,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
}
private void disconnect(@Nullable Exception e) {
private synchronized void disconnect(@Nullable Exception e) {
disconnect(e, null);
}
@ -294,23 +303,23 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
protected void releaseConnection() {
logger.debug("Bridge {} is disconnecting from KNX bus", thingUID);
var tmplink = link;
if (tmplink != null) {
tmplink.removeLinkListener(this);
var tmpLink = link;
if (tmpLink != null) {
tmpLink.removeLinkListener(this);
}
busJob = nullify(busJob, j -> j.cancel(true));
readDatapoints.clear();
responseCommunicator = nullify(responseCommunicator, rc -> {
rc.removeProcessListener(processListener);
rc.detach();
});
busJob = nullify(busJob, j -> j.cancel(true));
deviceInfoClient = null;
managementProcedures = nullify(managementProcedures, ManagementProcedures::detach);
managementClient = nullify(managementClient, ManagementClient::detach);
processCommunicator = nullify(processCommunicator, pc -> {
pc.removeProcessListener(processListener);
pc.detach();
});
deviceInfoClient = null;
managementClient = nullify(managementClient, ManagementClient::detach);
managementProcedures = nullify(managementProcedures, ManagementProcedures::detach);
responseCommunicator = nullify(responseCommunicator, rc -> {
rc.removeProcessListener(processListener);
rc.detach();
});
link = nullify(link, KNXNetworkLink::close);
logger.trace("Bridge {} disconnected from KNX bus", thingUID);
}
@ -361,13 +370,20 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
ReadDatapoint datapoint = readDatapoints.poll();
if (datapoint != null) {
// TODO #8872: allow write access, currently only listening mode
if (openhabSecurity.groupKeys().containsKey(datapoint.getDatapoint().getMainAddress())) {
logger.debug("outgoing secure communication not implemented, explicit read from GA '{}' skipped",
datapoint.getDatapoint().getMainAddress());
return;
}
datapoint.incrementRetries();
try {
logger.trace("Sending a Group Read Request telegram for {}", datapoint.getDatapoint().getMainAddress());
processCommunicator.read(datapoint.getDatapoint());
} catch (KNXException e) {
// Note: KnxException does not cover KnxRuntimeException and subclasses KnxSecureException,
// KnxIllegArgumentException
// KnxIllegalArgumentException
if (datapoint.getRetries() < datapoint.getLimit()) {
readDatapoints.add(datapoint);
logger.debug("Could not read value for datapoint {}: {}. Going to retry.",
@ -532,6 +548,12 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
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);

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.client;
import org.eclipse.jdt.annotation.NonNullByDefault;
import tuwien.auto.calimero.SerialNumber;
import tuwien.auto.calimero.link.KNXLinkClosedException;
import tuwien.auto.calimero.mgmt.SecureManagement;
import tuwien.auto.calimero.mgmt.TransportLayer;
import tuwien.auto.calimero.mgmt.TransportLayerImpl;
import tuwien.auto.calimero.secure.Security;
/**
* This class is to provide access to protected constructors in the Calimero library.
* Reason is to provide custom KNX keyring data.
*
* @author Holger Friedrich - initial contribution
*
*/
@NonNullByDefault
public class CustomSecureManagement extends SecureManagement {
public CustomSecureManagement(final TransportLayer transportLayer, final Security security)
throws KNXLinkClosedException {
// super(link, secureManagement) is not yet available in Calimero 2.5.1
super((TransportLayerImpl) transportLayer, SerialNumber.Zero, 0, security.deviceToolKeys());
// instance of Security has been created in ctor of SecureManagement, but only using deviceToolKeys
// no need to clear, just copy (otherwise SAL would lack the group keys)
final Security sal = security();
sal.groupKeys().putAll(security.groupKeys());
sal.groupSenders().putAll(security.groupSenders());
}
public final Security security() {
return super.security();
}
}

View File

@ -41,6 +41,7 @@ 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.
@ -88,9 +89,9 @@ public class IPClient extends AbstractKNXClient {
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;

View File

@ -32,6 +32,7 @@ import tuwien.auto.calimero.KNXException;
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;

View File

@ -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 class BridgeConfiguration {
public void setAutoReconnectPeriod(int period) {
autoReconnectPeriod = period;
}
public String getKeyringFile() {
return keyringFile;
}
public String getKeyringPassword() {
return keyringPassword;
}
}

View File

@ -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 class IPBridgeConfiguration extends BridgeConfiguration {
public String getTunnelDeviceAuthentication() {
return tunnelDeviceAuthentication;
}
public String getTunnelSourceAddress() {
return tunnelSourceAddress;
}
}

View File

@ -72,14 +72,25 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class);
boolean securityAvailable = false;
try {
securityAvailable = initializeSecurity(config.getRouterBackboneKey(),
config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), config.getTunnelUserPassword());
securityAvailable = initializeSecurity(config.getKeyringFile(), config.getKeyringPassword(),
config.getRouterBackboneKey(), config.getTunnelDeviceAuthentication(), config.getTunnelUserId(),
config.getTunnelUserPassword(), config.getTunnelSourceAddress());
if (securityAvailable) {
logger.debug("KNX secure: router backboneGroupKey is {} set",
((secureRouting.backboneGroupKey.length == 16) ? "properly" : "not"));
boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16)
&& (secureTunnel.userKey.length == 16));
logger.debug("KNX secure: tunnel keys are {} set", (tunnelOk ? "properly" : "not"));
if (keyring.isPresent()) {
logger.debug("KNX secure available for {} devices, {} group addresses",
openhabSecurity.deviceToolKeys().size(), openhabSecurity.groupKeys().size());
logger.debug("Secure group addresses and associated devices: {}",
secHelperGetSecureGroupAddresses(openhabSecurity));
} else {
logger.debug("KNX secure: keyring is not available");
}
} else {
logger.debug("KNX security not configured");
}
@ -154,7 +165,7 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
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 class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
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 class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
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 {

View File

@ -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.NonNullByDefault;
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.ThingStatusDetail;
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 abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implem
private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx");
private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor();
protected Optional<Keyring> 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<>());
@ -91,24 +116,47 @@ public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implem
}
/***
* Initialize KNX secure if configured (full interface)
* Initialize KNX secure if configured (simple 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
* @return
*/
protected boolean initializeSecurity(String cRouterBackboneGroupKey, String cTunnelDevAuth, String cTunnelUser,
String cTunnelPassword) throws KnxSecureException {
protected boolean initializeSecurity(String cKeyringFile, String cKeyringPassword) throws KnxSecureException {
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)
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 @@ public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implem
}
// step 2: check if valid tunnel parameters are specified in config
if (!cTunnelSourceAddr.isBlank()) {
try {
secureTunnelSourceAddr = new IndividualAddress(cTunnelSourceAddr.trim());
securityInitialized = true;
} catch (KNXFormatException e) {
throw new KnxSecureException("tunnel source address cannot be parsed, valid format is x.y.z");
}
}
if (!cTunnelDevAuth.isBlank()) {
secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(cTunnelDevAuth.toCharArray());
securityInitialized = true;
@ -139,29 +195,107 @@ public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implem
securityInitialized = true;
}
// step 3: keyring
if (!cKeyringFile.isBlank()) {
// filename defined in config, start parsing
try {
// load keyring file from config dir, folder misc
String keyringUri = OpenHAB.getConfigFolder() + File.separator + "misc" + File.separator + cKeyringFile;
try {
keyring = Optional.ofNullable(Keyring.load(keyringUri));
} catch (KNXMLException e) {
throw new KnxSecureException("keyring file configured, but loading failed: ", e);
}
if (!keyring.isPresent()) {
throw new KnxSecureException("keyring file configured, but loading failed: " + keyringUri);
}
// loading was successful, check signatures
// -> disabled, as Calimero v2.5 does this within the load() function
// if (!keyring.verifySignature(cKeyringPassword.toCharArray()))
// throw new KnxSecureException(
// "signature verification failed, please check keyring file: " + keyringUri);
keyringPassword = cKeyringPassword;
// We use a specific Security instance instead of default Calimero static instance
// Security.defaultInstallation().
// This necessary as it seems there is no possibility to clear the global instance on config changes.
openhabSecurity.useKeyring(keyring.get(), keyringPassword.toCharArray());
securityInitialized = true;
} catch (KnxSecureException e) {
keyring = Optional.empty();
keyringPassword = "";
throw e;
} catch (Exception e) {
// load() may throw KnxSecureException or other undeclared exceptions, e.g. UncheckedIOException when
// file is not found
keyring = Optional.empty();
keyringPassword = "";
throw new KnxSecureException("keyring file configured, but loading failed", e);
}
}
// step 4: router: load backboneGroupKey from keyring if not manually specified
if ((secureRouting.backboneGroupKey.length == 0) && (keyring.isPresent())) {
// backbone group key is only available if secure routers are present
final Optional<byte[]> 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<Backbone> bb = keyring.get().backbone();
if (bb.isPresent()) {
final long toleranceMs = bb.get().latencyTolerance().toMillis();
secureRouting.latencyToleranceMs = toleranceMs;
}
}
// step 6: tunnel: load data from keyring
if (secureTunnelSourceAddr != null) {
// requires a valid keyring
if (!keyring.isPresent()) {
throw new KnxSecureException("valid keyring specification required for secure tunnel mode");
}
// other parameters will not be accepted, since all is read from keyring in this case
if ((secureTunnel.userKey.length > 0) || secureTunnel.user != 0 || (secureTunnel.devKey.length > 0)) {
throw new KnxSecureException(
"tunnelSourceAddr is configured, please do not specify other parameters of secure tunnel");
}
Optional<SecureTunnelConfig> config = secHelperReadTunnelConfig(keyring, keyringPassword,
secureTunnelSourceAddr);
if (config.isEmpty()) {
throw new KnxSecureException("tunnel definition cannot be read from keyring");
}
secureTunnel = config.get();
}
return securityInitialized;
}
/***
* converts hex string (32 characters) to byte[16]
*
* @param hexstring 32 characters hex
* @param hexString 32 characters hex
* @return key in byte array format
*/
public static byte[] secHelperParseBackboneKey(String hexstring) throws KnxSecureException {
if (hexstring.length() != 32) {
public static byte[] secHelperParseBackboneKey(String hexString) throws KnxSecureException {
if (hexString.length() != 32) {
throw new KnxSecureException("backbone key must be 32 characters (16 byte hex notation)");
}
byte[] parsed = new byte[16];
try {
for (byte i = 0; i < 16; i++) {
parsed[i] = (byte) Integer.parseInt(hexstring.substring(2 * i, 2 * i + 2), 16);
parsed[i] = (byte) Integer.parseInt(hexString.substring(2 * i, 2 * i + 2), 16);
}
} catch (NumberFormatException e) {
throw new KnxSecureException("backbone key configured, cannot parse hex string, illegal character", e);
@ -169,6 +303,97 @@ public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implem
return parsed;
}
public static Optional<byte[]> secHelperReadBackboneKey(Optional<Keyring> keyring, String keyringPassword) {
if (keyring.isEmpty()) {
throw new KnxSecureException("keyring not available, cannot read backbone key");
}
final Optional<Backbone> bb = keyring.get().backbone();
if (bb.isPresent()) {
final Optional<byte[]> 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<SecureTunnelConfig> secHelperReadTunnelConfig(Optional<Keyring> 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<List<Interface>> itInterface = keyring.get().interfaces().values().iterator();
boolean complete = false;
while (!complete && itInterface.hasNext()) {
List<Interface> eInterface = itInterface.next();
// tunnels are nested
Iterator<Interface> itTunnel = eInterface.iterator();
while (!complete && itTunnel.hasNext()) {
Interface eTunnel = itTunnel.next();
if (secureTunnelSourceAddr.equals(eTunnel.address())) {
String pw = "";
final Optional<byte[]> 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<byte[]> auBytes = eTunnel.authentication();
if (auBytes.isPresent()) {
au = new String(keyring.get().decryptPassword(auBytes.get(), keyringPassword.toCharArray()));
secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(au.toCharArray())
.clone();
}
// set user, 0=fail
secureTunnel.user = eTunnel.user();
return Optional.of(secureTunnel);
}
}
}
return Optional.empty();
}
/***
* Show all secure group addresses and surrogates. A surrogate is the device which is asked to carry out an indirect
* read/write request.
* Simpler approach w/o surrogates: Security.defaultInstallation().groupSenders().toString());
*/
public static String secHelperGetSecureGroupAddresses(final Security openhabSecurity) {
Map<GroupAddress, Set<String>> groupSendersWithSurrogate = new HashMap<GroupAddress, Set<String>>();
final Map<GroupAddress, Set<IndividualAddress>> senders = openhabSecurity.groupSenders();
for (var entry : senders.entrySet()) {
final GroupAddress ga = entry.getKey();
// the following approach is uses by Calimero to deduce the surrogate for GA diagnostics
// see calimero-core security/SecureApplicationLayer.java, surrogate(...)
IndividualAddress surrogate = null;
try {
surrogate = senders.getOrDefault(ga, Set.of()).stream().findAny().get();
} catch (NoSuchElementException e) {
}
Set<String> devices = new HashSet<String>();
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

View File

@ -23,9 +23,12 @@ import org.openhab.binding.knx.internal.config.SerialBridgeConfiguration;
import org.openhab.core.io.transport.serial.SerialPortManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tuwien.auto.calimero.secure.KnxSecureException;
/**
* The {@link IPBridgeThingHandler} is responsible for handling commands, which are
* sent to one of the channels. It implements a KNX Serial/USB Gateway, that either acts as a
@ -57,7 +60,7 @@ public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler {
SerialBridgeConfiguration config = getConfigAs(SerialBridgeConfiguration.class);
client = new SerialClient(config.getAutoReconnectPeriod(), thing.getUID(), config.getResponseTimeout(),
config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), config.getSerialPort(),
config.useCemi(), serialPortManager, getCommandExtensionData(), this);
config.useCemi(), serialPortManager, getCommandExtensionData(), openhabSecurity, this);
updateStatus(ThingStatus.UNKNOWN);
// delay actual initialization, allow for longer runtime of actual initialization
@ -65,6 +68,32 @@ public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler {
}
public void initializeLater() {
SerialBridgeConfiguration config = getConfigAs(SerialBridgeConfiguration.class);
try {
if (initializeSecurity(config.getKeyringFile(), config.getKeyringPassword())) {
if (keyring.isPresent()) {
logger.info("KNX secure available for {} devices, {} group addresses",
openhabSecurity.deviceToolKeys().size(), openhabSecurity.groupKeys().size());
logger.debug("Secure group addresses and associated devices: {}",
secHelperGetSecureGroupAddresses(openhabSecurity));
} else {
logger.debug("KNX secure: keyring is not available");
}
} else {
logger.debug("KNX security not configured");
}
} catch (KnxSecureException e) {
logger.debug("{}, {}", thing.getUID(), e.toString());
String message = e.getLocalizedMessage();
if (message == null) {
message = e.getClass().getSimpleName();
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "KNX security: " + message);
return;
}
SerialClient tmpClient = client;
if (tmpClient != null) {
tmpClient.initialize();

View File

@ -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 group addresses. It can provide settings and credentials for IP Secure if not configured separately.
thing-type.config.knx.ip.keyringPassword.label = Keyring password
thing-type.config.knx.ip.keyringPassword.description = Keyring file password (set during export from ETS).
thing-type.config.knx.ip.localIp.label = Local Network Address
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 group addresses.
thing-type.config.knx.serial.keyringPassword.label = Keyring password
thing-type.config.knx.serial.keyringPassword.description = Keyring file password (set during export from ETS).
thing-type.config.knx.serial.readRetriesLimit.label = Read Retries Limit
thing-type.config.knx.serial.readRetriesLimit.description = Limits the read retries while initialization from the KNX bus
thing-type.config.knx.serial.readingPause.label = Reading Pause
@ -152,11 +164,6 @@ channel-type.config.knx.rollershutter.upDown.description = The group address (GA
channel-type.config.knx.single.ga.label = Address
channel-type.config.knx.single.ga.description = The group address (GA) for this channel. Additional listening (status) GAs can be added by concatenating GAs with a "+" (e.g. "1/7/18+2/9/15"). The DPT can be defined by prepending it to the first GA, separated by ":" (e.g. "2.001:1/4/17"). GAs prepended with a "<" (e.g. "<5/1/125") are read from the bus on startup.
# thing types config
thing-type.config.knx.serial.group.knxsecure.label = KNX secure
thing-type.config.knx.serial.group.knxsecure.description = Settings for KNX secure. Requires KNX secure features to be active in KNX installation.
# add-on specific strings
error.knx-secure-routing-backbonegroupkey-invalid = backbonegroupkey invalid, please check if it is specified correctly

View File

@ -73,29 +73,61 @@
<description>Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s</description>
<default>60</default>
</parameter>
<parameter name="keyringFile" type="text" groupName="knxsecure">
<label>Keyring file</label>
<description>Keyring file exported from ETS and placed in openHAB config/misc folder, e.g.
knx.knxkeys. This file is
mandatory to decode secure group addresses. It can provide settings
and credentials for IP Secure if not configured
separately.</description>
<advanced>true</advanced>
</parameter>
<parameter name="keyringPassword" type="text" groupName="knxsecure">
<context>password</context>
<label>Keyring password</label>
<description>Keyring file password (set during export from ETS).</description>
<advanced>true</advanced>
</parameter>
<parameter name="routerBackboneKey" type="text" groupName="knxsecure">
<context>password</context>
<label>Router backbone key</label>
<description>Backbone key for secure router mode. 16 bytes
in hex notation. Can also be found
in ETS security report.</description>
in ETS security report.
Optional, can be read from
keyring file if it is configured.</description>
<advanced>true</advanced>
</parameter>
<parameter name="tunnelSourceAddress" type="text" groupName="knxsecure">
<label>Tunnel source address</label>
<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.</description>
<advanced>true</advanced>
</parameter>
<parameter name="tunnelUserId" type="text" groupName="knxsecure">
<label>Tunnel user id</label>
<description>Tunnel user id for secure tunnel mode.</description>
<description>Tunnel user id for secure tunnel mode. Optional, can be read from
keyring file if tunnelSourceAddr is
configured.</description>
<advanced>true</advanced>
</parameter>
<parameter name="tunnelUserPassword" type="text" groupName="knxsecure">
<context>password</context>
<label>Tunnel user password</label>
<description>Tunnel user key for secure tunnel mode.</description>
<description>Tunnel user key for secure tunnel mode. Optional, can be read from
keyring file if tunnelSourceAddr is
configured.</description>
<advanced>true</advanced>
</parameter>
<parameter name="tunnelDeviceAuthentication" type="text" groupName="knxsecure">
<context>password</context>
<label>Tunnel device authentication</label>
<description>Tunnel device authentication for secure tunnel mode.</description>
<description>Tunnel device authentication for secure tunnel mode. Optional, can be read from
keyring file if
tunnelSourceAddr is configured.</description>
<advanced>true</advanced>
</parameter>
</config-description>

View File

@ -8,6 +8,10 @@
<label>KNX FT1.2 Interface</label>
<description>This is a serial interface for accessing the KNX bus</description>
<config-description>
<parameter-group name="knxsecure">
<label>KNX secure</label>
<description>Settings for KNX secure. Optional. Requires KNX secure features to be active in KNX installation.</description>
</parameter-group>
<parameter name="serialPort" type="text" required="true">
<context>serial-port </context>
<limitToOptions>false</limitToOptions>
@ -42,6 +46,19 @@
<description>Seconds between connect retries when KNX link has been lost, 0 means never retry</description>
<default>0</default>
</parameter>
<parameter name="keyringFile" type="text" groupName="knxsecure">
<label>Keyring file</label>
<description>Keyring file exported from ETS and placed in openHAB config/misc folder, e.g.
knx.knxkeys. This file is
mandatory to decode secure group addresses.</description>
<advanced>true</advanced>
</parameter>
<parameter name="keyringPassword" type="text" groupName="knxsecure">
<context>password</context>
<label>Keyring password</label>
<description>Keyring file password (set during export from ETS).</description>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>

View File

@ -32,7 +32,7 @@ public class DummyClient extends AbstractKNXClient {
public DummyClient() {
super(0, new ThingUID("dummy connection"), 0, 0, 0, null, new CommandExtensionData(Collections.emptyMap()),
null);
null, null);
}
@Override

View File

@ -15,8 +15,13 @@ package org.openhab.binding.knx.internal.handler;
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 @@ class KNXBridgeBaseThingHandlerTest {
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);
}
}
}

View File

@ -0,0 +1,250 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.security;
import static org.junit.jupiter.api.Assertions.*;
import java.io.File;
import java.util.Map;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.knx.internal.handler.KNXBridgeBaseThingHandler;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.knxnetip.SecureConnection;
import tuwien.auto.calimero.secure.Keyring;
import tuwien.auto.calimero.secure.KnxSecureException;
import tuwien.auto.calimero.secure.Security;
/**
*
* @author Holger Friedrich - initial contribution
*
*/
@NonNullByDefault
public class KNXSecurityTest {
@Test
public void testCalimeroKeyring() {
@SuppressWarnings("null")
final String testFile = getClass().getClassLoader().getResource("misc" + File.separator + "openhab6.knxkeys")
.toString();
final String passwordString = "habopen";
final char[] password = passwordString.toCharArray();
assertNotEquals("", testFile);
Keyring keys = Keyring.load(testFile);
// System.out.println(keys.devices().toString());
// System.out.println(keys.groups().toString());
// System.out.println(keys.interfaces().toString());
GroupAddress ga = new GroupAddress(8, 0, 0);
byte[] key800enc = keys.groups().get(ga);
assertNotNull(key800enc);
if (key800enc != null) {
assertNotEquals(0, key800enc.length);
}
byte[] key800dec = keys.decryptKey(key800enc, password);
assertEquals(16, key800dec.length);
IndividualAddress nopa = new IndividualAddress(2, 8, 20);
Keyring.Device nodev = keys.devices().get(nopa);
assertNull(nodev);
IndividualAddress pa = new IndividualAddress(1, 1, 42);
Keyring.Device dev = keys.devices().get(pa);
assertNotNull(dev);
// cannot check this for dummy test file, needs real device to be included
// assertNotEquals(0, dev.sequenceNumber());
Security openhabSecurity = Security.newSecurity();
openhabSecurity.useKeyring(keys, password);
Map<GroupAddress, byte[]> groupKeys = openhabSecurity.groupKeys();
assertEquals(3, groupKeys.size());
groupKeys.remove(ga);
assertEquals(2, groupKeys.size());
openhabSecurity.useKeyring(keys, password);
Map<GroupAddress, byte[]> 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<byte[]> bbKeyRead = KNXBridgeBaseThingHandler.secHelperReadBackboneKey(Optional.ofNullable(keys),
passwordString);
assertEquals(16, bbKeyParsedUpper.length);
assertArrayEquals(bbKeyParsedUpper, bbKeyParsedLower);
assertTrue(bbKeyRead.isPresent());
assertArrayEquals(bbKeyParsedUpper, bbKeyRead.get());
// System.out.print("Backbone key: \"");
// for (byte i : backboneGroupKey)
// System.out.print(String.format("%02X", i));
// System.out.println("\"");
// now check tunnel settings:
IndividualAddress secureTunnelSourceAddr = new IndividualAddress(1, 1, 2);
IndividualAddress noSecureTunnelSourceAddr = new IndividualAddress(2, 8, 20);
assertThrows(KnxSecureException.class, () -> {
KNXBridgeBaseThingHandler.secHelperReadTunnelConfig(Optional.empty(), passwordString,
secureTunnelSourceAddr);
});
assertTrue(KNXBridgeBaseThingHandler
.secHelperReadTunnelConfig(Optional.ofNullable(keys), passwordString, noSecureTunnelSourceAddr)
.isEmpty());
var config = KNXBridgeBaseThingHandler.secHelperReadTunnelConfig(Optional.ofNullable(keys), passwordString,
secureTunnelSourceAddr);
assertTrue(config.isPresent());
assertEquals(2, config.get().user);
assertArrayEquals(SecureConnection.hashUserPassword("mytunnel1".toCharArray()), config.get().userKey);
assertArrayEquals(SecureConnection.hashDeviceAuthenticationPassword("myauthcode".toCharArray()),
config.get().devKey);
// secure group addresses should contain at least one address marked as "surrogate"
final String secureAddresses = KNXBridgeBaseThingHandler.secHelperGetSecureGroupAddresses(openhabSecurity);
assertTrue(secureAddresses.contains("(S)"));
assertTrue(secureAddresses.contains("8/4/0"));
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Keyring Project="openhab6-minimal-ipif" CreatedBy="6.0.2" Created="2022-03-02T17:40:53" Signature="ZNKRogTN6X8uSviKxnb8FQ==" xmlns="http://knx.org/xml/keyring/1">
<Backbone MulticastAddress="224.0.23.12" />
<Devices>
<Device IndividualAddress="1.1.3" ToolKey="4TAbUJiKVy8Fb51bO8fKJw==" />
</Devices>
</Keyring>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Keyring Project="openhab6-minimal-sipif" CreatedBy="6.0.2" Created="2022-03-02T20:47:32" Signature="ITyO/PaDzSA2Lb6J2FyXgA==" xmlns="http://knx.org/xml/keyring/1">
<Backbone MulticastAddress="224.0.23.12" />
<Interface IndividualAddress="1.1.2" Type="Tunneling" Host="1.1.1" UserID="2" Password="7vI8IYOA6TG8aRDiBMb3KIfoHLPKxp4jG3+sO7uwuw4=" Authentication="AVQ2dMT+52vspyStmZ9+8g6Oskjt70o4wnqm0tsPkI0=" />
<Interface IndividualAddress="1.1.4" Type="Tunneling" Host="1.1.1" UserID="3" Password="LsurQzd7MV6bWI+KxlpFGf17gW3lagaeRGenm9riJG4=" Authentication="qk597AJwqZ/Qe0Erc2hCJCG/6mv1hGQKRINfN+7bZPU=" />
<Interface IndividualAddress="1.1.5" Type="Tunneling" Host="1.1.1" UserID="4" Password="DHdsQEFqRWlAu0lSCOyz9HoKr2hbsMCOk1VpE6jESsA=" Authentication="KEwPKUS/0fVnlPcJAA3aYzbRCKE/QcXVdeCnZJed+P0=" />
<Interface IndividualAddress="1.1.6" Type="Tunneling" Host="1.1.1" UserID="5" Password="NRQekIgTZDRXs9HNHCbaPyUeL9H02ET9oCzwYxe+HA0=" Authentication="mgSxsc6tGJfwtoSZifdghTcgqUI4XwV5QITjH0R7cNA=" />
<Interface IndividualAddress="1.1.7" Type="Tunneling" Host="1.1.1" UserID="6" Password="cvP1SzE4PWkUNv+evNCLdPUMZcZJnJxzp1bDwFT2emA=" Authentication="KyPx37deTYrh3ObQYRwmALdDXeBG4u9TT8VTEOBhTqE=" />
<Devices>
<Device IndividualAddress="1.1.1" ToolKey="MH9tidHty0OwjaWKIGH5Zg==" ManagementPassword="hqygOoW7gJyKzRwoWzZbVxKHa1TMRm5MPRmWdhMROtQ=" Authentication="2GBT98qKw96qNS573gXrExG5NIL3QMvba/h89thsaLI=" />
<Device IndividualAddress="1.1.3" ToolKey="RMumSDwjPH8quCkcp+EJxA==" />
</Devices>
</Keyring>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Keyring Project="openhab6-minimal-sipr" CreatedBy="6.0.2" Created="2022-03-02T20:21:31" Signature="Mv4HUIhAEdN0j3eHK8kvhw==" xmlns="http://knx.org/xml/keyring/1">
<Backbone MulticastAddress="224.0.23.12" Latency="2000" Key="WTq688uTJdyF+mDUyV7h9Q==" />
<Interface IndividualAddress="1.1.1" Type="Tunneling" Host="1.1.0" UserID="2" Password="Rr4I9shKloT2z4H0SDrCxvLckph2XRqQCUaBAbYnDRY=" Authentication="fTg6DhcrcYePIl/hbtTd+Munz5z1Gjf5uQIaOybsFsE=" />
<Interface IndividualAddress="1.1.2" Type="Tunneling" Host="1.1.0" UserID="3" Password="BVGPxxeXIG/9vTUbKjCcOHcBVzPDwtGtXxIV9DK80qU=" Authentication="VFw6piwppnbwsn67KnzxmoUT6cG95N8k4tor+mpBlJc=" />
<Interface IndividualAddress="1.1.4" Type="Tunneling" Host="1.1.0" UserID="4" Password="tEYKV5oCCUca6Brj4kqJ5ykWTzuXRZGVp2eXniloIz8=" Authentication="g27EgH8nAtmIDA898c1UJT3P9EZodBZd+AzsBrErRpc=" />
<Interface IndividualAddress="1.1.5" Type="Tunneling" Host="1.1.0" UserID="5" Password="AoP9ObiwOtatsqyaLkNNQVd5zTk5ZPjwXDF2YhgnQxY=" Authentication="HMZs3QDHclVlzZ5JQln6ZutxPaCLwkpV/8419SHJHM4=" />
<Interface IndividualAddress="1.1.6" Type="Tunneling" Host="1.1.0" UserID="6" Password="ImqRByvtwVOKePgyK5Gscn0a6eKPG8PEn6PZigJuUK8=" Authentication="dHvCcwhP8XrUuey7Hg6eMzRV9GBLUm/SUK4ymi1ZYKs=" />
<Devices>
<Device IndividualAddress="1.1.0" ToolKey="DPgL63v9oOT2lPTrl0znNw==" ManagementPassword="nH9hF0hEeLJB5lCd+sx+P0BcyKlfkrEZqVyBZcXYXb8=" Authentication="Coqlcx76csPb+36QQS1GvtCxWgGJTwrllzIGHW22sTQ=" />
<Device IndividualAddress="1.1.3" ToolKey="18/4WzFOCyNrlob2B4ey3g==" />
</Devices>
</Keyring>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Keyring Project="openHAB test" CreatedBy="6.0.2" Created="2022-02-16T20:14:21" Signature="7DCcy3pjn9UL/62Cd4JRNw==" xmlns="http://knx.org/xml/keyring/1">
<Backbone MulticastAddress="224.0.23.12" Latency="2000" Key="jqQQPxOaT9yQmGntjzmmcA==" />
<Interface IndividualAddress="1.1.2" Type="Tunneling" Host="1.1.1" UserID="2" Password="I/mTxaNvfOBksLSm+DpvJEOByNJ2CdFstHUApyc7vYg=" Authentication="pywl6qpSlAHikvSZ95qa8Tpz1ybtBPbLsNWr11Y+rCc=" />
<Interface IndividualAddress="1.1.3" Type="Tunneling" Host="1.1.1" UserID="3" Password="VS5jc4CYCkn3fLjXOfucRX/IyWV5vFWCVOf+o7Z2WkQ=" Authentication="Is2o/DJtci597gNs8kk3xa2PnvR73CREFS4CSsxvs4E=" />
<Interface IndividualAddress="1.1.4" Type="Tunneling" Host="1.1.1" UserID="4" Password="TEPR9kJnMBfR4ZjknYk/LVpFa9NecnMS9eVYzQ9nDtU=" Authentication="HCSeleEDD2ZUIP9Necvt8bWb7N1Lmb/rmZZ2MWKGxto=" />
<Interface IndividualAddress="1.1.5" Type="Tunneling" Host="1.1.1" UserID="5" Password="ik4IhpmfEca7E1fs1tAOJ+dw2t3VIAmNtwbcUcjV6l8=" Authentication="jZRam0aOS8pFF5jNGTWsp8dlAYSBvtFtby3zLXdGqJ4=" />
<Interface IndividualAddress="1.1.6" Type="Tunneling" Host="1.1.1" UserID="6" Password="ZLaimyAb6WK4W24vKWx+9/p7F7PsmxXlu53i1xgl/3c=" Authentication="mTAGaAAv310sfwb8150/kG+qEl4SNJEpPexGly/gEj8=" />
<Interface IndividualAddress="1.1.7" Type="Tunneling" Host="1.1.1" UserID="7" Password="iShH6XxU8ynPd2bxk9e07lOPHqfvk+pyoNhirjwmseM=" Authentication="pPg4npj9AkGr7e9i4zTTHgdIjq8pPNB/t/ThVofm1I0=" />
<Interface IndividualAddress="1.1.8" Type="Tunneling" Host="1.1.1" UserID="8" Password="Qsoe9Uql3HRm0Acm3g1MNBh4aWI9e6Z7+Bdaxr4HgBg=" Authentication="+0DEjJHPWZBXNcOz7kvK+hcYwrlj5Ep/cb/EdQB5s1o=" />
<Interface IndividualAddress="1.1.9" Type="Tunneling" Host="1.1.1" UserID="9" Password="u35VfR3rgsZo9iMbhudqMd79a1ymcXJA1nFqUqUIrOg=" Authentication="2516mjVwl1+e0uF2gMYUsv9+OkMqpnmMlDUJJtLqY54=" />
<Interface IndividualAddress="1.1.10" Type="Tunneling" Host="1.1.0" UserID="2" Password="mVhL48YOaihWHFtXa+wmzI9JckF3ABbdvlbZCJGFiSo=" Authentication="xl7KByFbKpHIVX1HwP6coo8tN9Kn7v7FODYQ9YrYvLQ=" />
<Interface IndividualAddress="1.1.11" Type="Tunneling" Host="1.1.0" UserID="3" Password="aVxzF1AAwa0Fe6VCkPIpBnYlQQNVUAESgEev0fTNez8=" Authentication="R82NqLPaZsBqseWcWWYkpb+lojfwTvXnjJJIrgD/GgM=" />
<Interface IndividualAddress="1.1.12" Type="Tunneling" Host="1.1.0" UserID="4" Password="ZfpsaaR1Z1f3xOrdl3lMB3ECEaJHGQ67hYYJtS8kJvk=" Authentication="7W0oAfAWRM4AvCgDqJaAK2D+syyaz62Z6L9JyCc5Pe8=" />
<Interface IndividualAddress="1.1.13" Type="Tunneling" Host="1.1.0" UserID="5" Password="RFHe5Rf6dOnBXFc6I5Ib/gyPHdnVrqVnA0miEg8Hf1U=" Authentication="dy10MHltqGJI5HVVPVzKFVz21NJBv+d4riTrsjgkgfk=" />
<Interface IndividualAddress="1.1.14" Type="Tunneling" Host="1.1.0" UserID="6" Password="Es9do2yHUNMKNqqqXsqPhWQHmqzpABd0bllosyHBl4M=" Authentication="FPvvbkyXtztAy/vg4p+yb/duUiR2ynD9dDbfD3ceFWI=" />
<Interface IndividualAddress="1.1.15" Type="Tunneling" Host="1.1.0" UserID="7" Password="hVHfIe+089Gsl7ONAcFnVGWwtb8H7PEgts+M5mI8ssw=" Authentication="I/NINRXOpKypAHMn41GQKtZL/YkehxGnUnPP2Mlnv4c=" />
<Interface IndividualAddress="1.1.16" Type="Tunneling" Host="1.1.0" UserID="8" Password="hIEQVBFFg7N5d4pGDUZwZljIH3k4OiCmc3cNHmwP7Ko=" Authentication="WM31k8coJR+fe/1NC15iFinZUWhrg8wSqPhnmQPVF/c=" />
<Interface IndividualAddress="1.1.17" Type="Tunneling" Host="1.1.0" UserID="9" Password="LYzsUD6qXY5fJYMyglcoYF57ImWQ9R343DcVqIpGCos=" Authentication="MJQmHxu2dS9JIs4K8vUhvcbmt1flKwPJEk/53EzRENI=" />
<Interface IndividualAddress="1.0.128" Type="Backbone">
<Group Address="16384" Senders="1.1.42" />
<Group Address="17408" Senders="1.1.42" />
</Interface>
<GroupAddresses>
<Group Address="16384" Key="aet9NVDLVrBozo0fjAZsfw==" />
<Group Address="17408" Key="JNPEYPcwjqvNVMGEqfLYFg==" />
<Group Address="17409" Key="hSZjJizZHAaEJDCGWBG02g==" />
</GroupAddresses>
<Devices>
<Device IndividualAddress="1.0.128" ToolKey="y6tTTgkjYVHzadcO3DsaYA==" ManagementPassword="eh9/MLvXQp5YI+rHKRt+eg9X4oHCiHcsx2sZllE1UAo=" Authentication="bSNdyKdQK9K36wnSF5p+FaoOnFu7vtppdYE+wgoMAHw=" />
<Device IndividualAddress="1.1.0" ToolKey="YQn06Qzc5YKOtzLH4qAasg==" ManagementPassword="hOvb9C7GWTx0kyc043QJouMCC1xKV5sq0zyf6qIri8c=" Authentication="fIvCX/e7mlRGBZ6g8tforCijY3+OZYXmAwC1DiDcBkQ=" />
<Device IndividualAddress="1.1.1" ToolKey="/dXTk0oJstzyIfOVyAAZOg==" ManagementPassword="DwfYrNbvxKev6Lk+T+vJ5Gw9TW3mfsvmvK6Eh6Slrrs=" Authentication="/Ioy/hSd4D9GSRgwrt8zP4bV8tIeews4650OKxl4AEg=" />
<Device IndividualAddress="1.1.42" ToolKey="c+Qmg96GNf8SB3idRFei6A==" />
</Devices>
</Keyring>