Skip to content

Commit

Permalink
[knx] Allow decoding of KNX Data Secure frames
Browse files Browse the repository at this point in the history
* add passive (listening only) access for KNX Data Secure frames, openhab#8872
* add config options for KNX keyring file and password
* ease setup if IP Secure, as required parameters can be read from keyring
* add tests for security functions
* update user documentation

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
  • Loading branch information
holgerfriedrich committed May 20, 2024
1 parent 330c776 commit 1451a63
Show file tree
Hide file tree
Showing 19 changed files with 769 additions and 44 deletions.
20 changes: 19 additions & 1 deletion bundles/org.openhab.binding.knx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ At its base, the _ip_ bridge accepts the following configuration parameters:
| tunnelUserId | No | KNX secure: Tunnel user id for secure tunnel mode (if specified, it must be a number >0) | - |
| tunnelUserPassword | No | KNX secure: Tunnel user key for secure tunnel mode | - |
| tunnelDeviceAuthentication | No | KNX secure: Tunnel device authentication for secure tunnel mode | - |
| keyringFile | No | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure GAs. | - |
| keyringPassword | No | KNX secure: Keyring file password (set during export from ETS) | - |
| tunnelSourceAddress | No | KNX secure: Physical KNX address of tunnel in secure mode to identify tunnel. If given, openHAB will read tunnelUserId, tunnelUserPassword, tunnelDeviceAuthentication from keyring | - |

### Serial Gateway

Expand All @@ -79,6 +82,8 @@ The _serial_ bridge accepts the following configuration parameters:
| readRetriesLimit | N | Limits the read retries while initialization from the KNX bus | 3 |
| autoReconnectPeriod | N | Seconds between connect retries when KNX link has been lost, 0 means never retry | 0 |
| useCemi | N | Use newer CEMI message format, useful for newer devices like KNX RF sticks, kBerry, etc. | false |
| keyringFile | N | KNX secure: Keyring file exported from ETS and placed in openHAB config/misc folder. Mandatory to decode secure GAs. | - |
| keyringPassword | N | KNX secure: Keyring file password (set during export from ETS) | - |

## Things

Expand Down Expand Up @@ -452,24 +457,37 @@ It **requires a KNX Secure Router or a Secure IP Interface** and a KNX installat

For _Secure routing_ mode, the so-called `backbone key` needs to be configured in openHAB.
It is created by the ETS tool and cannot be changed via the ETS user interface.
There are two possible ways to provide the key to openHAB:

- The backbone key can be extracted from Security report (ETS, Reports, Security, look for a 32-digit key) and specified in parameter `routerBackboneKey`.
- The backbone key is included in ETS keyring export (ETS, project settings, export keyring). Keyring file is configured using `keyringFile` (put it in `config\misc` folder of the openHAB installation) and also requires `keyringPassword`.

For _Secure tunneling_ with a Secure IP Interface (or a router in tunneling mode), more parameters are required.
A unique device authentication key, and a specific tunnel identifier and password need to be available.
It can be provided to openHAB in two different ways:

- All information can be looked up in ETS and provided separately: `tunnelDeviceAuthentication`, `tunnelUserPassword`.
`tunnelUserId` is a number that is not directly visible in ETS, but can be looked up in keyring export or deduced (typically 2 for the first tunnel of a device, 3 for the second one, ...).
`tunnelUserPasswort` is set in ETS in the properties of the tunnel (below the IP interface, you will see the different tunnels listed) and denoted as "Password".
`tunnelDeviceAuthentication` is set in the properties of the IP interface itself; check for the tab "IP" and the description "Authentication Code".
- All necessary information is included in ETS keyring export (ETS, project settings, export keyring).
Keyring file is configured using `keyringFile` (put it in `config\misc` folder of the openHAB installation) and `keyringPassword`.
In addition, `tunnelSourceAddress` needs to be set to uniquely identify the tunnel in use.

### KNX Data Secure

KNX Data Secure protects the content of messages on the KNX bus.
In a KNX installation, both classic and secure group addresses can coexist.
Data Secure does _not_ necessarily require a KNX Secure Router or a Secure IP Interface, but a KNX installation with newer KNX devices that support Data Secure and with **security features enabled in the ETS tool**.

> NOTE: **openHAB currently ignores messages with secure group addresses.**
**openHAB ignores messages with secure group addresses, unless data secure is configured.**

> NOTE: openHAB currently does fully support passive (listening) access to secure group addresses.
Write access to secure group addresses is currently disabled in openHAB.
Initial/periodic read will fail, avoid automatic read (< in thing definition).

All necessary information to decode secure group addresses is included in ETS keyring export (ETS, project settings, export keyring).
Keyring file is configured using `keyringFile` (put it in `config\misc` folder of the openHAB installation) and also requires `keyringPassword`.

## Examples

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,13 @@ public class KNXBindingConstants {
public static final String PORT_NUMBER = "portNumber";
public static final String SERIAL_PORT = "serialPort";
public static final String USE_CEMI = "useCemi";
public static final String KEYRING_FILE = "keyringFile";
public static final String KEYRING_PASSWORD = "keyringPassword";
public static final String ROUTER_BACKBONE_GROUP_KEY = "routerBackboneGroupKey";
public static final String TUNNEL_USER_ID = "tunnelUserId";
public static final String TUNNEL_USER_PASSWORD = "tunnelUserPassword";
public static final String TUNNEL_DEVICE_AUTHENTICATION = "tunnelDeviceAuthentication";
public static final String TUNNEL_SOURCE_ADDRESS = "tunnelSourceAddress";

// The default multicast ip address (see <a
// href="http://www.iana.org/assignments/multicast-addresses/multicast-addresses.xml">iana</a> EIBnet/IP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public enum ClientState {
private final StatusUpdateCallback statusUpdateCallback;
private final ScheduledExecutorService knxScheduler;
private final CommandExtensionData commandExtensionData;
protected final Security openhabSecurity;

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

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

public void initialize() {
Expand Down Expand Up @@ -189,7 +191,6 @@ private synchronized boolean connect() {
logger.trace("connect() ignored, closing down");
return false;
}

if (isConnected()) {
return true;
}
Expand Down Expand Up @@ -360,7 +361,15 @@ private void readNextQueuedDatapoint() {
return;
}
ReadDatapoint datapoint = readDatapoints.poll();

if (datapoint != null) {
// TODO #8872: allow write access, currently only listening mode
if (openhabSecurity.groupKeys().containsKey(datapoint.getDatapoint().getMainAddress())) {
logger.debug("outgoing secure communication not implemented, explicit read from GA '{}' skipped",
datapoint.getDatapoint().getMainAddress());
return;
}

datapoint.incrementRetries();
try {
logger.trace("Sending a Group Read Request telegram for {}", datapoint.getDatapoint().getMainAddress());
Expand Down Expand Up @@ -532,9 +541,16 @@ private void sendToKNX(ProcessCommunication communicator, GroupAddress groupAddr
return;
}

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

Datapoint datapoint = new CommandDP(groupAddress, thingUID.toString(), 0,
NORMALIZED_DPT.getOrDefault(dpt, dpt));
String mappedValue = ValueEncoder.encode(type, dpt);

if (mappedValue == null) {
logger.debug("Value '{}' of type '{}' cannot be mapped to datapoint '{}'", type, type.getClass(),
datapoint);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.medium.KNXMediumSettings;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.secure.Security;

/**
* IP specific {@link AbstractKNXClient} implementation.
Expand Down Expand Up @@ -88,9 +89,9 @@ public IPClient(IpConnectionType ipConnectionType, String ip, String localSource
byte[] secureRoutingBackboneGroupKey, long secureRoutingLatencyToleranceMs, byte[] secureTunnelDevKey,
int secureTunnelUser, byte[] secureTunnelUserKey, ThingUID thingUID, int responseTimeout, int readingPause,
int readRetriesLimit, ScheduledExecutorService knxScheduler, CommandExtensionData commandExtensionData,
StatusUpdateCallback statusUpdateCallback) {
Security openhabSecurity, StatusUpdateCallback statusUpdateCallback) {
super(autoReconnectPeriod, thingUID, responseTimeout, readingPause, readRetriesLimit, knxScheduler,
commandExtensionData, statusUpdateCallback);
commandExtensionData, openhabSecurity, statusUpdateCallback);
this.ipConnectionType = ipConnectionType;
this.ip = ip;
this.localSource = localSource;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.link.KNXNetworkLinkFT12;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.secure.Security;
import tuwien.auto.calimero.serial.FT12Connection;

/**
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -46,4 +48,12 @@ public int getResponseTimeout() {
public void setAutoReconnectPeriod(int period) {
autoReconnectPeriod = period;
}

public String getKeyringFile() {
return keyringFile;
}

public String getKeyringPassword() {
return keyringPassword;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,4 +74,8 @@ public String getTunnelUserPassword() {
public String getTunnelDeviceAuthentication() {
return tunnelDeviceAuthentication;
}

public String getTunnelSourceAddress() {
return tunnelSourceAddress;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,25 @@ public void initializeLater() {
IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class);
boolean securityAvailable = false;
try {
securityAvailable = initializeSecurity(config.getRouterBackboneKey(),
config.getTunnelDeviceAuthentication(), config.getTunnelUserId(), config.getTunnelUserPassword());
securityAvailable = initializeSecurity(config.getKeyringFile(), config.getKeyringPassword(),
config.getRouterBackboneKey(), config.getTunnelDeviceAuthentication(), config.getTunnelUserId(),
config.getTunnelUserPassword(), config.getTunnelSourceAddress());
if (securityAvailable) {
logger.debug("KNX secure: router backboneGroupKey is {} set",
((secureRouting.backboneGroupKey.length == 16) ? "properly" : "not"));
boolean tunnelOk = ((secureTunnel.user > 0) && (secureTunnel.devKey.length == 16)
&& (secureTunnel.userKey.length == 16));
logger.debug("KNX secure: tunnel keys are {} set", (tunnelOk ? "properly" : "not"));

if (keyring.isPresent()) {
logger.debug("KNX secure available for {} devices, {} group addresses",
openhabSecurity.deviceToolKeys().size(), openhabSecurity.groupKeys().size());

logger.debug("Secure group addresses and associated devices: {}",
secHelperGetSecureGroupAdresses(openhabSecurity));
} else {
logger.debug("KNX secure: keyring is not available");
}
} else {
logger.debug("KNX security not configured");
}
Expand Down Expand Up @@ -154,7 +165,7 @@ public void initializeLater() {
return;
}
if (secureRouting.backboneGroupKey.length != 16) {
// failed to read shared backbone group key from config
// failed to read shared backbone group key from config or keyring
logger.warn("Bridge {} invalid security configuration for secure routing", thing.getUID());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/error.knx-secure-routing-backbonegroupkey-invalid");
Expand Down Expand Up @@ -186,7 +197,7 @@ public void initializeLater() {
secureRouting.backboneGroupKey, secureRouting.latencyToleranceMs, secureTunnel.devKey,
secureTunnel.user, secureTunnel.userKey, thing.getUID(), config.getResponseTimeout(),
config.getReadingPause(), config.getReadRetriesLimit(), getScheduler(), getCommandExtensionData(),
this);
openhabSecurity, this);

IPClient tmpClient = client;
if (tmpClient != null) {
Expand All @@ -200,7 +211,7 @@ public void initializeLater() {
public void dispose() {
Future<?> tmpInitJob = initJob;
if (tmpInitJob != null) {
while (!tmpInitJob.isDone()) {
if (!tmpInitJob.isDone()) {
logger.trace("Bridge {}, shutdown during init, trying to cancel", thing.getUID());
tmpInitJob.cancel(true);
try {
Expand Down
Loading

0 comments on commit 1451a63

Please sign in to comment.