[knx] Add support for KNX IP Secure (#12709)

* [knx] Add support for KNX IP Secure

* add support for KNX IP Secure, new options SECURETUNNEL and
  SECUREROUTER, refers to #8872
* add config options for credentials for secure connections
* update user documentation
* add test cases

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
This commit is contained in:
Holger Friedrich 2022-08-08 16:55:41 +02:00 committed by GitHub
parent 21aa9c6aff
commit 8a0b7a042c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 625 additions and 57 deletions

View File

@ -29,7 +29,7 @@ The IP Gateway is the most commonly used way to connect to the KNX bus. At its b
| Name | Required | Description | Default value |
|---------------------|--------------|--------------------------------------------------------------------------------------------------------------|------------------------------------------------------|
| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL` or `ROUTER`) | - |
| type | Yes | The IP connection type for connecting to the KNX bus (`TUNNEL`, `ROUTER`, `SECURETUNNEL` or `SECUREROUTER`) | - |
| ipAddress | for `TUNNEL` | Network address of the KNX/IP gateway. If type `ROUTER` is set, the IPv4 Multicast Address can be set. | for `TUNNEL`: \<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 |
@ -39,6 +39,10 @@ The IP Gateway is the most commonly used way to connect to the KNX bus. At its b
| 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 | - |
### Serial Gateway
@ -208,6 +212,35 @@ Each configuration parameter has a `mainGA` where commands are written to and op
The `dpt` element is optional. If omitted, the corresponding default value will be used (see the channel descriptions above).
## KNX Secure
> NOTE: Support for KNX Secure is partly implemented for openHAB and should be considered as experimental.
### KNX IP Secure
KNX IP Secure protects the traffic between openHAB and your KNX installation.
It **requires a KNX Secure Router or a Secure IP Interface** and a KNX installation **with security features enabled in ETS tool**.
For *Secure routing* mode, the so called `backbone key` needs to be configured in openHAB.
It is created by the ETS tool and cannot be changed via the ETS user interface.
- The backbone key can be extracted from Security report (ETS, Reports, Security, look for a 32-digit key) and specified in parameter `routerBackboneKey`.
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.
- All information can be looked up in ETS and provided separately: `tunnelDeviceAuthentication`, `tunnelUserPassword`.
`tunnelUserId` is a number which is not directly visible in ETS, but can be looked up in keyring export or deduced (typically 2 for the first tunnel of a device, 3 for the second one, ...).
`tunnelUserPasswort` is set in ETS in the properties of the tunnel (below the IP interface you will see the different tunnels listed) denoted as "Password". `tunnelDeviceAuthentication` is set in the properties of the IP interface itself, check for a tab "IP" and a description "Authentication Code".
### 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 which support Data Secure and with **security features enabled in ETS tool**.
> NOTE: **openHAB currently ignores messages with secure group addresses.**
## Examples
The following two templates are sufficient for almost all purposes.

View File

@ -54,6 +54,10 @@ 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 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";
// The default multicast ip address (see <a
// href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP

View File

@ -13,6 +13,7 @@
package org.openhab.binding.knx.internal.client;
import java.time.Duration;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CopyOnWriteArraySet;
@ -40,6 +41,7 @@ import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.KNXIllegalArgumentException;
import tuwien.auto.calimero.datapoint.CommandDP;
import tuwien.auto.calimero.datapoint.Datapoint;
import tuwien.auto.calimero.device.ProcessCommunicationResponder;
@ -55,6 +57,7 @@ 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;
@ -66,6 +69,14 @@ import tuwien.auto.calimero.secure.Security;
*/
@NonNullByDefault
public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClient {
public enum ClientState {
INIT,
RUNNING,
INTERRUPTED,
DISPOSE
}
private ClientState state = ClientState.INIT;
private static final int MAX_SEND_ATTEMPTS = 2;
@ -146,7 +157,11 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
private boolean scheduleReconnectJob() {
if (autoReconnectPeriod > 0) {
connectJob = knxScheduler.schedule(this::connect, autoReconnectPeriod, TimeUnit.SECONDS);
// schedule connect job, for the first connection ignore autoReconnectPeriod and use 1 sec
final long reconnectDelayS = (state == ClientState.INIT) ? 1 : autoReconnectPeriod;
final String prefix = (state == ClientState.INIT) ? "re" : "";
logger.debug("Bridge {} scheduling {}connect in {}s", thingUID, prefix, reconnectDelayS);
connectJob = knxScheduler.schedule(this::connect, reconnectDelayS, TimeUnit.SECONDS);
return true;
} else {
return false;
@ -154,7 +169,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
private void cancelReconnectJob() {
ScheduledFuture<?> currentReconnectJob = connectJob;
final ScheduledFuture<?> currentReconnectJob = connectJob;
if (currentReconnectJob != null) {
currentReconnectJob.cancel(true);
connectJob = null;
@ -171,55 +186,111 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
private synchronized boolean connect() {
if (state == ClientState.INIT) {
state = ClientState.RUNNING;
} else if (state == ClientState.DISPOSE) {
logger.trace("connect() ignored, closing down");
return false;
}
if (isConnected()) {
return true;
}
try {
// We have a valid "connection" object, this is ensured by IPClient.java.
// "releaseConnection" is actually removing all registered users of this connection and stopping
// all threads.
// Note that this will also kill this function in the following call to sleep in case of a
// connection loss -> restart is via triggered via scheduledReconnect in handler for InterruptedException.
releaseConnection();
Thread.sleep(1000);
logger.debug("Bridge {} is connecting to KNX bus", thingUID);
logger.debug("Bridge {} is connecting to the KNX bus", thingUID);
// now establish (possibly encrypted) connection, according to settings (tunnel, routing, secure...)
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);
// 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);
managementClient.responseTimeout(Duration.ofSeconds(responseTimeout));
this.managementClient = managementClient;
// 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);
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
ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link,
new SecureApplicationLayer(link, Security.defaultInstallation()));
this.responseCommunicator = responseCommunicator;
// register this class, callbacks will be triggered
link.addLinkListener(this);
// create a job carrying out read requests
busJob = knxScheduler.scheduleWithFixedDelay(() -> readNextQueuedDatapoint(), 0, readingPause,
TimeUnit.MILLISECONDS);
statusUpdateCallback.updateStatus(ThingStatus.ONLINE);
connectJob = null;
logger.info("Bridge {} connected to KNX bus", thingUID);
state = ClientState.RUNNING;
return true;
} catch (KNXException | InterruptedException e) {
logger.debug("Error connecting to the bus: {}", e.getMessage(), e);
} catch (InterruptedException e) {
final var lastState = state;
state = ClientState.INTERRUPTED;
logger.trace("Bridge {}, connection interrupted", thingUID);
disconnect(e);
if (lastState != ClientState.DISPOSE) {
scheduleReconnectJob();
}
return false;
} catch (KNXException | KnxSecureException e) {
logger.debug("Bridge {} cannot connect: {}", thingUID, e.getMessage());
disconnect(e);
scheduleReconnectJob();
return false;
} catch (KNXIllegalArgumentException e) {
logger.debug("Bridge {} cannot connect: {}", thingUID, e.getMessage());
disconnect(e, Optional.of(ThingStatusDetail.CONFIGURATION_ERROR));
return false;
}
}
private void disconnect(@Nullable Exception e) {
disconnect(e, Optional.empty());
}
private synchronized void disconnect(@Nullable Exception e, Optional<ThingStatusDetail> detail) {
releaseConnection();
if (e != null) {
String message = e.getLocalizedMessage();
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
final String message = e.getLocalizedMessage();
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, detail.orElse(ThingStatusDetail.COMMUNICATION_ERROR),
message != null ? message : "");
} else {
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE);
@ -227,22 +298,27 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
@SuppressWarnings("null")
private void releaseConnection() {
logger.debug("Bridge {} is disconnecting from the KNX bus", thingUID);
readDatapoints.clear();
protected void releaseConnection() {
logger.debug("Bridge {} is disconnecting from KNX bus", thingUID);
var tmplink = link;
if (tmplink != null) {
link.removeLinkListener(this);
}
busJob = nullify(busJob, j -> j.cancel(true));
deviceInfoClient = null;
managementProcedures = nullify(managementProcedures, mp -> mp.detach());
managementClient = nullify(managementClient, mc -> mc.detach());
link = nullify(link, l -> l.close());
processCommunicator = nullify(processCommunicator, pc -> {
pc.removeProcessListener(processListener);
pc.detach();
});
readDatapoints.clear();
responseCommunicator = nullify(responseCommunicator, rc -> {
rc.removeProcessListener(processListener);
rc.detach();
});
processCommunicator = nullify(processCommunicator, pc -> {
pc.removeProcessListener(processListener);
pc.detach();
});
deviceInfoClient = null;
managementClient = nullify(managementClient, mc -> mc.detach());
managementProcedures = nullify(managementProcedures, mp -> mp.detach());
link = nullify(link, l -> l.close());
logger.trace("Bridge {} disconnected from KNX bus", thingUID);
}
private <T> @Nullable T nullify(T target, @Nullable Consumer<T> lastWill) {
@ -276,6 +352,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
return typeHelper.toDPTValue(type, dpt);
}
// datapoint is null at end of the list, warning is misleading
@SuppressWarnings("null")
private void readNextQueuedDatapoint() {
if (!connectIfNotAutomatic()) {
@ -316,6 +393,8 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
}
public void dispose() {
state = ClientState.DISPOSE;
cancelReconnectJob();
disconnect(null);
}
@ -420,7 +499,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
ProcessCommunicator processCommunicator = this.processCommunicator;
KNXNetworkLink link = this.link;
if (processCommunicator == null || link == null) {
logger.debug("Cannot write to the KNX bus (processCommuicator: {}, link: {})",
logger.debug("Cannot write to KNX bus (processCommuicator: {}, link: {})",
processCommunicator == null ? "Not OK" : "OK",
link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
return;
@ -439,7 +518,7 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
ProcessCommunicationResponder responseCommunicator = this.responseCommunicator;
KNXNetworkLink link = this.link;
if (responseCommunicator == null || link == null) {
logger.debug("Cannot write to the KNX bus (responseCommunicator: {}, link: {})",
logger.debug("Cannot write to KNX bus (responseCommunicator: {}, link: {})",
responseCommunicator == null ? "Not OK" : "OK",
link == null ? "Not OK" : (link.isOpen() ? "Open" : "Closed"));
return;
@ -475,10 +554,10 @@ public abstract class AbstractKNXClient implements NetworkLinkListener, KNXClien
break;
} catch (KNXException e) {
if (i < MAX_SEND_ATTEMPTS - 1) {
logger.debug("Value '{}' could not be sent to the KNX bus using datapoint '{}': {}. Will retry.",
type, datapoint, e.getLocalizedMessage());
logger.debug("Value '{}' could not be sent to KNX bus using datapoint '{}': {}. Will retry.", type,
datapoint, e.getLocalizedMessage());
} else {
logger.warn("Value '{}' could not be sent to the KNX bus using datapoint '{}': {}. Giving up now.",
logger.warn("Value '{}' could not be sent to KNX bus using datapoint '{}': {}. Giving up now.",
type, datapoint, e.getLocalizedMessage());
throw e;
}

View File

@ -30,6 +30,7 @@ import tuwien.auto.calimero.link.medium.KNXMediumSettings;
public class CustomKNXNetworkLinkIP extends KNXNetworkLinkIP {
public static final int TUNNELING = KNXNetworkLinkIP.TUNNELING;
public static final int TUNNELINGV2 = KNXNetworkLinkIP.TunnelingV2;
public static final int ROUTING = KNXNetworkLinkIP.ROUTING;
CustomKNXNetworkLinkIP(final int serviceMode, KNXnetIPConnection conn, KNXMediumSettings settings)

View File

@ -61,6 +61,11 @@ public class DeviceInfoClientImpl implements DeviceInfoClient {
return result;
} catch (KNXException e) {
logger.debug("Could not {} of {}: {}", task, address, e.getMessage());
try {
// avoid trashing the log on connection loss
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
} catch (InterruptedException e) {
logger.trace("Interrupted to {}", task);
return null;

View File

@ -17,6 +17,7 @@ import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
@ -32,6 +33,9 @@ import tuwien.auto.calimero.knxnetip.KNXnetIPConnection;
import tuwien.auto.calimero.knxnetip.KNXnetIPRouting;
import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel;
import tuwien.auto.calimero.knxnetip.KNXnetIPTunnel.TunnelingLayer;
import tuwien.auto.calimero.knxnetip.SecureConnection;
import tuwien.auto.calimero.knxnetip.TcpConnection;
import tuwien.auto.calimero.knxnetip.TcpConnection.SecureSession;
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.medium.KNXMediumSettings;
@ -46,23 +50,43 @@ import tuwien.auto.calimero.link.medium.TPSettings;
@NonNullByDefault
public class IPClient extends AbstractKNXClient {
public enum IpConnectionType {
TUNNEL,
ROUTER,
SECURE_TUNNEL,
SECURE_ROUTER
};
private final Logger logger = LoggerFactory.getLogger(IPClient.class);
private static final String MODE_ROUTER = "ROUTER";
private static final String MODE_TUNNEL = "TUNNEL";
private static final String MODE_SECURE_ROUTER = "SECURE ROUTER";
private static final String MODE_SECURE_TUNNEL = "SECURE TUNNEL";
private static final long PAUSE_ON_TCP_SESSION_CLOSE_MS = 1000;
private final int ipConnectionType;
private final IpConnectionType ipConnectionType;
private final String ip;
private final String localSource;
private final int port;
@Nullable
private final InetSocketAddress localEndPoint;
private final boolean useNAT;
private final byte[] secureRoutingBackboneGroupKey;
private final long secureRoutingLatencyToleranceMs;
private final byte[] secureTunnelDevKey;
private final int secureTunnelUser;
private final byte[] secureTunnelUserKey;
private final ThingUID thingUID;
public IPClient(int ipConnectionType, String ip, String localSource, int port,
@Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod, ThingUID thingUID,
int responseTimeout, int readingPause, int readRetriesLimit, ScheduledExecutorService knxScheduler,
StatusUpdateCallback statusUpdateCallback) {
@Nullable
SecureSession tcpSession;
public IPClient(IpConnectionType ipConnectionType, String ip, String localSource, int port,
@Nullable InetSocketAddress localEndPoint, boolean useNAT, int autoReconnectPeriod,
byte[] secureRoutingBackboneGroupKey, long secureRoutingLatencyToleranceMs, byte[] secureTunnelDevKey,
int secureTunnelUser, byte[] secureTunnelUserKey, ThingUID thingUID, int responseTimeout, int readingPause,
int readRetriesLimit, ScheduledExecutorService knxScheduler, StatusUpdateCallback statusUpdateCallback) {
super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler,
statusUpdateCallback);
this.ipConnectionType = ipConnectionType;
@ -71,6 +95,13 @@ public class IPClient extends AbstractKNXClient {
this.port = port;
this.localEndPoint = localEndPoint;
this.useNAT = useNAT;
this.secureRoutingBackboneGroupKey = secureRoutingBackboneGroupKey;
this.secureRoutingLatencyToleranceMs = secureRoutingLatencyToleranceMs;
this.secureTunnelDevKey = secureTunnelDevKey;
this.secureTunnelUser = secureTunnelUser;
this.secureTunnelUserKey = secureTunnelUserKey;
this.thingUID = thingUID;
tcpSession = null;
}
@Override
@ -82,23 +113,44 @@ public class IPClient extends AbstractKNXClient {
}
private String connectionTypeToString() {
return ipConnectionType == CustomKNXNetworkLinkIP.ROUTING ? MODE_ROUTER : MODE_TUNNEL;
if (ipConnectionType == IpConnectionType.ROUTER) {
return MODE_ROUTER;
}
if (ipConnectionType == IpConnectionType.TUNNEL) {
return MODE_TUNNEL;
}
if (ipConnectionType == IpConnectionType.SECURE_ROUTER) {
return MODE_SECURE_ROUTER;
}
if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
return MODE_SECURE_TUNNEL;
}
return "unknown connection type";
}
private KNXNetworkLinkIP createKNXNetworkLinkIP(int serviceMode, @Nullable InetSocketAddress localEP,
@Nullable InetSocketAddress remoteEP, boolean useNAT, KNXMediumSettings settings)
throws KNXException, InterruptedException {
private KNXNetworkLinkIP createKNXNetworkLinkIP(IpConnectionType ipConnectionType,
@Nullable InetSocketAddress localEP, @Nullable InetSocketAddress remoteEP, boolean useNAT,
KNXMediumSettings settings) throws KNXException, InterruptedException {
// Calimero service mode, ROUTING for both classic and secure routing
int serviceMode = CustomKNXNetworkLinkIP.ROUTING;
if (ipConnectionType == IpConnectionType.TUNNEL) {
serviceMode = CustomKNXNetworkLinkIP.TUNNELING;
} else if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
serviceMode = CustomKNXNetworkLinkIP.TUNNELINGV2;
}
// creating the connection here as a workaround for
// https://github.com/calimero-project/calimero-core/issues/57
KNXnetIPConnection conn = getConnection(serviceMode, localEP, remoteEP, useNAT);
KNXnetIPConnection conn = getConnection(ipConnectionType, localEP, remoteEP, useNAT);
return new CustomKNXNetworkLinkIP(serviceMode, conn, settings);
}
private KNXnetIPConnection getConnection(int serviceMode, @Nullable InetSocketAddress localEP,
private KNXnetIPConnection getConnection(IpConnectionType ipConnectionType, @Nullable InetSocketAddress localEP,
@Nullable InetSocketAddress remoteEP, boolean useNAT) throws KNXException, InterruptedException {
KNXnetIPConnection conn;
switch (serviceMode) {
case CustomKNXNetworkLinkIP.TUNNELING:
switch (ipConnectionType) {
case TUNNEL:
case SECURE_TUNNEL:
InetSocketAddress local = localEP;
if (local == null) {
try {
@ -107,9 +159,23 @@ public class IPClient extends AbstractKNXClient {
throw new KNXException("no local host available");
}
}
conn = new KNXnetIPTunnel(TunnelingLayer.LinkLayer, local, remoteEP, useNAT);
if (ipConnectionType == IpConnectionType.SECURE_TUNNEL) {
logger.trace("creating new TCP connection");
if (tcpSession != null) {
logger.debug("tcpSession might still be open");
}
// using .clone for the keys is essential - otherwise Calimero clears the array and a reconnect will
// fail
tcpSession = TcpConnection.newTcpConnection(localEP, remoteEP).newSecureSession(secureTunnelUser,
secureTunnelUserKey.clone(), secureTunnelDevKey.clone());
conn = SecureConnection.newTunneling(TunnelingLayer.LinkLayer, tcpSession,
new IndividualAddress(localSource));
} else {
conn = new KNXnetIPTunnel(TunnelingLayer.LinkLayer, local, remoteEP, useNAT);
}
break;
case CustomKNXNetworkLinkIP.ROUTING:
case ROUTER:
case SECURE_ROUTER:
NetworkInterface netIf = null;
if (localEP != null && !localEP.isUnresolved()) {
try {
@ -119,11 +185,39 @@ public class IPClient extends AbstractKNXClient {
}
}
final InetAddress mcast = remoteEP != null ? remoteEP.getAddress() : null;
conn = new KNXnetIPRouting(netIf, mcast);
if (ipConnectionType == IpConnectionType.SECURE_ROUTER) {
conn = SecureConnection.newRouting(netIf, mcast, secureRoutingBackboneGroupKey,
Duration.ofMillis(secureRoutingLatencyToleranceMs));
} else {
conn = new KNXnetIPRouting(netIf, mcast);
}
break;
default:
throw new KNXIllegalArgumentException("unknown service mode");
}
return conn;
}
private void closeTcpConnection() {
final SecureSession toBeClosed = tcpSession;
if (toBeClosed != null) {
tcpSession = null;
logger.debug("Bridge {} closing TCP connection", thingUID);
try {
toBeClosed.close();
try {
Thread.sleep(PAUSE_ON_TCP_SESSION_CLOSE_MS);
} catch (InterruptedException e) {
}
} catch (Exception e) {
logger.debug("closing TCP connection failed: {}", e.getMessage());
}
}
}
@Override
protected void releaseConnection() {
closeTcpConnection();
super.releaseConnection();
}
}

View File

@ -31,6 +31,10 @@ public class IPBridgeConfiguration extends BridgeConfiguration {
private BigDecimal portNumber = BigDecimal.valueOf(0);
private String localIp = "";
private String localSourceAddr = "";
private String routerBackboneKey = "";
private String tunnelUserId = "";
private String tunnelUserPassword = "";
private String tunnelDeviceAuthentication = "";
public Boolean getUseNAT() {
return useNAT;
@ -55,4 +59,20 @@ public class IPBridgeConfiguration extends BridgeConfiguration {
public String getLocalSourceAddr() {
return localSourceAddr;
}
public String getRouterBackboneKey() {
return routerBackboneKey;
}
public String getTunnelUserId() {
return tunnelUserId;
}
public String getTunnelUserPassword() {
return tunnelUserPassword;
}
public String getTunnelDeviceAuthentication() {
return tunnelDeviceAuthentication;
}
}

View File

@ -162,7 +162,8 @@ public abstract class AbstractKNXThingHandler extends BaseThingHandler implement
}
}
} catch (KNXException e) {
logger.debug("An error occurred while testing the reachability of a thing '{}'", getThing().getUID(), e);
logger.debug("An error occurred while testing the reachability of a thing '{}': {}", getThing().getUID(),
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getLocalizedMessage());
}
}
@ -191,7 +192,8 @@ public abstract class AbstractKNXThingHandler extends BaseThingHandler implement
updateStatus(ThingStatus.ONLINE);
}
} catch (KNXFormatException e) {
logger.debug("An exception occurred while setting the individual address '{}'", config.getAddress(), e);
logger.debug("An exception occurred while setting the individual address '{}': {}", config.getAddress(),
e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getLocalizedMessage());
}
getClient().registerGroupAddressListener(this);

View File

@ -14,11 +14,11 @@ package org.openhab.binding.knx.internal.handler;
import java.net.InetSocketAddress;
import java.text.MessageFormat;
import java.util.concurrent.Future;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.KNXBindingConstants;
import org.openhab.binding.knx.internal.client.CustomKNXNetworkLinkIP;
import org.openhab.binding.knx.internal.client.IPClient;
import org.openhab.binding.knx.internal.client.KNXClient;
import org.openhab.binding.knx.internal.client.NoOpClient;
@ -30,6 +30,8 @@ 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/IP Gateway, that either acts a a
@ -43,10 +45,13 @@ import org.slf4j.LoggerFactory;
public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
private static final String MODE_ROUTER = "ROUTER";
private static final String MODE_TUNNEL = "TUNNEL";
private static final String MODE_SECURE_ROUTER = "SECUREROUTER";
private static final String MODE_SECURE_TUNNEL = "SECURETUNNEL";
private @Nullable Future<?> initJob = null;
private final Logger logger = LoggerFactory.getLogger(IPBridgeThingHandler.class);
private @Nullable IPClient client;
private @Nullable IPClient client = null;
private final NetworkAddressService networkAddressService;
public IPBridgeThingHandler(Bridge bridge, NetworkAddressService networkAddressService) {
@ -56,7 +61,40 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
@Override
public void initialize() {
// initialisation would take too long and show a warning during binding startup
// KNX secure is adding serious delay
updateStatus(ThingStatus.UNKNOWN);
initJob = scheduler.submit(() -> {
initializeLater();
});
}
public void initializeLater() {
IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class);
boolean securityAvailable = false;
try {
securityAvailable = initializeSecurity(config.getRouterBackboneKey(),
config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), config.getTunnelUserPassword());
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"));
} 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;
}
int autoReconnectPeriod = config.getAutoReconnectPeriod();
if (autoReconnectPeriod != 0 && autoReconnectPeriod < 30) {
logger.info("autoReconnectPeriod for {} set to {}s, allowed range is 0 (never) or >30", thing.getUID(),
@ -70,20 +108,64 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
String ip = config.getIpAddress();
InetSocketAddress localEndPoint = null;
boolean useNAT = false;
int ipConnectionType;
IPClient.IpConnectionType ipConnectionType;
if (MODE_TUNNEL.equalsIgnoreCase(connectionTypeString)) {
useNAT = config.getUseNAT();
ipConnectionType = CustomKNXNetworkLinkIP.TUNNELING;
ipConnectionType = IPClient.IpConnectionType.TUNNEL;
} else if (MODE_SECURE_TUNNEL.equalsIgnoreCase(connectionTypeString)) {
useNAT = config.getUseNAT();
ipConnectionType = IPClient.IpConnectionType.SECURE_TUNNEL;
if (!securityAvailable) {
logger.warn("Bridge {} missing security configuration for secure tunnel", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Security configuration missing for secure tunnel");
return;
}
boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16)
&& (secureTunnel.userKey.length == 16));
if (!tunnelOk) {
logger.warn("Bridge {} incomplete security configuration for secure tunnel", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Security configuration for secure tunnel is incomplete");
return;
}
logger.debug("KNX secure tunneling needs a few seconds to establish connection");
// user id, key, devAuth are already stored
} else if (MODE_ROUTER.equalsIgnoreCase(connectionTypeString)) {
useNAT = false;
if (ip.isEmpty()) {
ip = KNXBindingConstants.DEFAULT_MULTICAST_IP;
}
ipConnectionType = CustomKNXNetworkLinkIP.ROUTING;
ipConnectionType = IPClient.IpConnectionType.ROUTER;
} else if (MODE_SECURE_ROUTER.equalsIgnoreCase(connectionTypeString)) {
useNAT = false;
if (ip.isEmpty()) {
ip = KNXBindingConstants.DEFAULT_MULTICAST_IP;
}
ipConnectionType = IPClient.IpConnectionType.SECURE_ROUTER;
if (!securityAvailable) {
logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Security configuration missing for secure routing");
return;
}
if (secureRouting.backboneGroupKey.length != 16) {
// failed to read shared backbone group key from config
logger.warn("Bridge {} missing security configuration for secure routing", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"backboneGroupKey required for secure routing; please configure");
return;
}
logger.debug("KNX secure routing needs a few seconds to establish connection");
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
MessageFormat.format("Unknown IP connection type {0}. Known types are either 'TUNNEL' or 'ROUTER'",
connectionTypeString));
logger.debug("Bridge {} unknown connection type", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, MessageFormat.format(
"Unknown IP connection type {0}. Known types are either 'TUNNEL', 'ROUTER', 'SECURETUNNEL', or 'SECUREROUTER'",
connectionTypeString));
return;
}
@ -95,23 +177,39 @@ public class IPBridgeThingHandler extends KNXBridgeBaseThingHandler {
updateStatus(ThingStatus.UNKNOWN);
client = new IPClient(ipConnectionType, ip, localSource, port, localEndPoint, useNAT, autoReconnectPeriod,
thing.getUID(), config.getResponseTimeout().intValue(), config.getReadingPause().intValue(),
config.getReadRetriesLimit().intValue(), getScheduler(), this);
secureRouting.backboneGroupKey, secureRouting.latencyToleranceMs, secureTunnel.devKey,
secureTunnel.user, secureTunnel.userKey, thing.getUID(), config.getResponseTimeout().intValue(),
config.getReadingPause().intValue(), config.getReadRetriesLimit().intValue(), getScheduler(), this);
final var tmpClient = client;
if (tmpClient != null) {
tmpClient.initialize();
}
logger.trace("Bridge {} completed KNX scheduled initialization", thing.getUID());
}
@Override
public void dispose() {
super.dispose();
final var tmpInitJob = initJob;
if (tmpInitJob != null) {
while (!tmpInitJob.isDone()) {
logger.trace("Bridge {}, shutdown during init, trying to cancel", thing.getUID());
tmpInitJob.cancel(true);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.trace("Bridge {}, cancellation interrupted", thing.getUID());
}
}
initJob = null;
}
final var tmpClient = client;
if (tmpClient != null) {
tmpClient.dispose();
client = null;
}
super.dispose();
}
@Override

View File

@ -29,27 +29,135 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.knxnetip.SecureConnection;
import tuwien.auto.calimero.mgmt.Destination;
import tuwien.auto.calimero.secure.KnxSecureException;
/**
* The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Simon Kaufmann - Initial contribution and API
* @author Holger Friedrich - KNX Secure configuration
*/
@NonNullByDefault
public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implements StatusUpdateCallback {
public static class SecureTunnelConfig {
public SecureTunnelConfig() {
devKey = new byte[0];
userKey = new byte[0];
user = 0;
}
public byte[] devKey;
public byte[] userKey;
public int user = 0;
}
public static class SecureRoutingConfig {
public SecureRoutingConfig() {
backboneGroupKey = new byte[0];
latencyToleranceMs = 0;
}
public byte[] backboneGroupKey;
public long latencyToleranceMs = 0;
}
protected ConcurrentHashMap<IndividualAddress, Destination> destinations = new ConcurrentHashMap<>();
private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx");
private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor();
protected SecureRoutingConfig secureRouting;
protected SecureTunnelConfig secureTunnel;
public KNXBridgeBaseThingHandler(Bridge bridge) {
super(bridge);
secureRouting = new SecureRoutingConfig();
secureTunnel = new SecureTunnelConfig();
}
protected abstract KNXClient getClient();
/***
* Initialize KNX secure if configured (full interface)
*
* @param cRouterBackboneGroupKey shared key for secure router mode.
* @param cTunnelDevAuth device password for IP interface in tunnel mode.
* @param cTunnelUser user id for tunnel mode. Must be an integer >0.
* @param cTunnelPassword user password for tunnel mode.
* @return
*/
protected boolean initializeSecurity(String cRouterBackboneGroupKey, String cTunnelDevAuth, String cTunnelUser,
String cTunnelPassword) throws KnxSecureException {
secureRouting = new SecureRoutingConfig();
secureTunnel = new SecureTunnelConfig();
boolean securityInitialized = false;
// step 1: secure routing, backbone group key manually specified in OH config
if (!cRouterBackboneGroupKey.isBlank()) {
// provided in config
String key = cRouterBackboneGroupKey.trim().replaceFirst("^0x", "").trim().replace(" ", "");
if (!key.isEmpty()) {
// helper may throw KnxSecureException
secureRouting.backboneGroupKey = secHelperParseBackboneKey(key);
securityInitialized = true;
}
}
// step 2: check if valid tunnel parameters are specified in config
if (!cTunnelDevAuth.isBlank()) {
secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(cTunnelDevAuth.toCharArray());
securityInitialized = true;
}
if (!cTunnelPassword.isBlank()) {
secureTunnel.userKey = SecureConnection.hashUserPassword(cTunnelPassword.toCharArray());
securityInitialized = true;
}
if (!cTunnelUser.isBlank()) {
String user = cTunnelUser.trim();
try {
secureTunnel.user = Integer.decode(user);
} catch (NumberFormatException e) {
throw new KnxSecureException("tunnelUser must be a number >0");
}
if (secureTunnel.user <= 0) {
throw new KnxSecureException("tunnelUser must be a number >0");
}
securityInitialized = true;
}
// step 5: router: load latencyTolerance
// default to 2000ms
// this parameter is currently not exposed in config, it may later be set by using the keyring
secureRouting.latencyToleranceMs = 2000;
return securityInitialized;
}
/***
* converts hex string (32 characters) to byte[16]
*
* @param hexstring 32 characters hex
* @return key in byte array format
*/
public static byte[] secHelperParseBackboneKey(String hexstring) throws KnxSecureException {
if (hexstring.length() != 32) {
throw new KnxSecureException("backbone key must be 32 characters (16 byte hex notation)");
}
byte[] parsed = new byte[16];
try {
for (byte i = 0; i < 16; i++) {
parsed[i] = (byte) Integer.parseInt(hexstring.substring(2 * i, 2 * i + 2), 16);
}
} catch (NumberFormatException e) {
throw new KnxSecureException("backbone key configured, cannot parse hex string, illegal character", e);
}
return parsed;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Nothing to do here

View File

@ -50,8 +50,8 @@ public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler {
@Override
public void dispose() {
super.dispose();
client.dispose();
super.dispose();
}
@Override

View File

@ -24,6 +24,8 @@ thing-type.config.knx.device.readInterval.label = Read Interval
thing-type.config.knx.device.readInterval.description = Interval (in seconds) between attempts to read the status group addresses on the bus
thing-type.config.knx.ip.autoReconnectPeriod.label = Auto Reconnect Period
thing-type.config.knx.ip.autoReconnectPeriod.description = Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s
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.localIp.label = Local Network Address
@ -38,10 +40,20 @@ thing-type.config.knx.ip.readingPause.label = Reading Pause
thing-type.config.knx.ip.readingPause.description = Time in milliseconds of how long should be paused between two read requests to the bus during initialization
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.tunnelDeviceAuthentication.label = Tunnel device authentication
thing-type.config.knx.ip.tunnelDeviceAuthentication.description = Tunnel device authentication for secure tunnel mode.
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.tunnelUserPassword.label = Tunnel user password
thing-type.config.knx.ip.tunnelUserPassword.description = Tunnel user key for secure tunnel mode.
thing-type.config.knx.ip.type.label = IP Connection Type
thing-type.config.knx.ip.type.description = The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER
thing-type.config.knx.ip.type.description = The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or SECUREROUTER
thing-type.config.knx.ip.type.option.TUNNEL = Tunnel
thing-type.config.knx.ip.type.option.ROUTER = Router
thing-type.config.knx.ip.type.option.SECURETUNNEL = Secure tunnel (experimental, use advanced options to configure)
thing-type.config.knx.ip.type.option.SECUREROUTER = Secure router (experimental, use advanced options to configure)
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
@ -134,3 +146,8 @@ channel-type.config.knx.rollershutter.upDown.label = Address
channel-type.config.knx.rollershutter.upDown.description = The group address(es) in Group Address Notation to move the shutter in the DOWN or UP direction
channel-type.config.knx.single.ga.label = Address
channel-type.config.knx.single.ga.description = The group address(es) in Group Address Notation
# 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.

View File

@ -9,12 +9,19 @@
<description>This is a KNX IP interface or router</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="type" type="text" required="true">
<label>IP Connection Type</label>
<description>The ip connection type for connecting to the KNX bus. Could be either TUNNEL or ROUTER</description>
<description>The IP connection type for connecting to the KNX bus. Could be either TUNNEL, ROUTER, SECURETUNNEL, or
SECUREROUTER</description>
<options>
<option value="TUNNEL">Tunnel</option>
<option value="ROUTER">Router</option>
<option value="SECURETUNNEL">Secure tunnel (experimental, use advanced options to configure)</option>
<option value="SECUREROUTER">Secure router (experimental, use advanced options to configure)</option>
</options>
</parameter>
<parameter name="ipAddress" type="text">
@ -64,6 +71,31 @@
<description>Seconds between connection retries when KNX link has been lost, 0 means never retry, minimum 30s</description>
<default>60</default>
</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>
<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>
<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>
<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>
<advanced>true</advanced>
</parameter>
</config-description>
</bridge-type>

View File

@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.knx.internal.handler;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Bridge;
import tuwien.auto.calimero.secure.KnxSecureException;
/**
*
* @author Holger Friedrich - initial contribution
*
*/
@NonNullByDefault
public class KNXBridgeBaseThingHandlerTest {
@Test
public void testSecurityHelpers() {
// now check router settings:
String bbKeyHex = "D947B12DDECAD528B1D5A88FD347F284";
byte[] bbKeyParsedLower = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex.toLowerCase());
byte[] bbKeyParsedUpper = KNXBridgeBaseThingHandler.secHelperParseBackboneKey(bbKeyHex);
assertEquals(16, bbKeyParsedUpper.length);
assertArrayEquals(bbKeyParsedUpper, bbKeyParsedLower);
}
@Test
@SuppressWarnings("null")
public void testInitializeSecurity() {
Bridge bridge = mock(Bridge.class);
NetworkAddressService nas = mock(NetworkAddressService.class);
IPBridgeThingHandler handler = new IPBridgeThingHandler(bridge, nas);
// no config given
assertFalse(handler.initializeSecurity("", "", "", ""));
// router password configured, length must be 16 bytes in hex notation
assertTrue(handler.initializeSecurity("D947B12DDECAD528B1D5A88FD347F284", "", "", ""));
assertTrue(handler.initializeSecurity("0xD947B12DDECAD528B1D5A88FD347F284", "", "", ""));
assertThrows(KnxSecureException.class, () -> {
handler.initializeSecurity("wrongLength", "", "", "");
});
// tunnel configuration
assertTrue(handler.initializeSecurity("", "da", "1", "pw"));
// cTunnelUser is restricted to a number >0
assertThrows(KnxSecureException.class, () -> {
handler.initializeSecurity("", "da", "0", "pw");
});
assertThrows(KnxSecureException.class, () -> {
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"));
}
}