Skip to content

Commit

Permalink
[knx] add initial support for KNX data secure [WIP], openhab#8872
Browse files Browse the repository at this point in the history
* use Calimero library in latest version 2.5-SNAPSHOT (needs to be installed locally, mvn install)
  (to be replaced once a release is available)
* add config options for keyring file(s) and password(s)
* add tests for security functions
* TODO replace ProcessCommunicationResponder, SAL required modification
  of Calimero lib

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
  • Loading branch information
holgerfriedrich committed Dec 8, 2020
1 parent e19c165 commit 4fe7539
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public class KNXBindingConstants {
public static final String LOCAL_SOURCE_ADDRESS = "localSourceAddr";
public static final String PORT_NUMBER = "portNumber";
public static final String SERIAL_PORT = "serialPort";
public static final String KEYRING_FILE = "keyringFile";
public static final String KEYRING_PASSWORD = "keyringPassword";

// 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 @@ -12,6 +12,9 @@
*/
package org.openhab.binding.knx.internal.client;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.LinkedBlockingQueue;
Expand All @@ -25,6 +28,7 @@
import org.openhab.binding.knx.internal.KNXTypeMapper;
import org.openhab.binding.knx.internal.dpt.KNXCoreTypeMapper;
import org.openhab.binding.knx.internal.handler.GroupAddressListener;
import org.openhab.core.OpenHAB;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingUID;
Expand All @@ -34,12 +38,14 @@

import tuwien.auto.calimero.CloseEvent;
import tuwien.auto.calimero.DetachEvent;
import tuwien.auto.calimero.DeviceDescriptor;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.KNXException;
import tuwien.auto.calimero.datapoint.CommandDP;
import tuwien.auto.calimero.datapoint.Datapoint;
import tuwien.auto.calimero.device.BaseKnxDevice;
import tuwien.auto.calimero.device.ProcessCommunicationResponder;
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.link.NetworkLinkListener;
Expand All @@ -48,7 +54,7 @@
import tuwien.auto.calimero.mgmt.ManagementClientImpl;
import tuwien.auto.calimero.mgmt.ManagementProcedures;
import tuwien.auto.calimero.mgmt.ManagementProceduresImpl;
import tuwien.auto.calimero.process.ProcessCommunicationBase;
import tuwien.auto.calimero.process.ProcessCommunication;
import tuwien.auto.calimero.process.ProcessCommunicator;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import tuwien.auto.calimero.process.ProcessEvent;
Expand Down Expand Up @@ -186,12 +192,38 @@ private synchronized boolean connect() {

deviceInfoClient = new DeviceInfoClientImpl(managementClient);

ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link);
// KNX secure "device" needs to store properties, e.g. sequence numbers.
// Store in userdata folder for now.
// TODO: ensure this is written from time to time to safeguard against sudden crashes
final String folder = OpenHAB.getUserDataFolder();
URI ios = null;
try {
ios = new URI(folder + File.separator + "knx_secure_device.xml");
} catch (URISyntaxException e) {
logger.warn("failure specifying setting file, {}", e.toString());
}
// no encryption of property file for now
final String iosPassword = "";

// public BaseKnxDevice(final String name, final DeviceDescriptor dd, final ProcessCommunicationService
// process,
// final ManagementService mgmt, final URI iosResource, final char[] iosPassword) throws
// KnxPropertyException
BaseKnxDevice dev = new BaseKnxDevice("openHAB", DeviceDescriptor.DD0.TYPE_07B0, null, null, ios,
iosPassword.toCharArray());
// use existing link, derive address from link setting
dev.setDeviceLink(link);
// manually setting the address is only allowed if we inherit BaseKnxDevice
// dev.setAddress(new IndividualAddress("1.0.128"));

ProcessCommunicator processCommunicator = new ProcessCommunicatorImpl(link, dev.secureApplicationLayer());
processCommunicator.setResponseTimeout(responseTimeout);
processCommunicator.addProcessListener(processListener);
this.processCommunicator = processCommunicator;

ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link);
ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link,
dev.secureApplicationLayer()); // new
// tuwien.auto.calimero.device.DeviceSecureApplicationLayer(dev));
this.responseCommunicator = responseCommunicator;

link.addLinkListener(this);
Expand All @@ -213,9 +245,11 @@ private synchronized boolean connect() {
private void disconnect(@Nullable Exception e) {
releaseConnection();
if (e != null) {
String message = e.getLocalizedMessage();
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
message != null ? message : "");
String s = e.getLocalizedMessage();
if (s == null) {
s = e.getMessage();
}
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, s);
} else {
statusUpdateCallback.updateStatus(ThingStatus.OFFLINE);
}
Expand Down Expand Up @@ -439,7 +473,7 @@ public void respondToKNX(OutboundSpec responseSpec) throws KNXException {
}
}

private void sendToKNX(ProcessCommunicationBase communicator, KNXNetworkLink link, GroupAddress groupAddress,
private void sendToKNX(ProcessCommunication communicator, KNXNetworkLink link, GroupAddress groupAddress,
String dpt, Type type) throws KNXException {
if (!connectIfNotAutomatic()) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class BridgeConfiguration {
private BigDecimal readingPause;
private BigDecimal readRetriesLimit;
private BigDecimal responseTimeout;
private String keyringFile;
private String keyringPassword;

public int getAutoReconnectPeriod() {
return autoReconnectPeriod;
Expand All @@ -47,4 +49,12 @@ public BigDecimal 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 @@ -72,8 +72,8 @@ public class DeviceThingHandler extends AbstractKNXThingHandler {
private final Set<GroupAddress> groupAddresses = new HashSet<>();
private final Set<GroupAddress> groupAddressesWriteBlockedOnce = new HashSet<>();
private final Set<OutboundSpec> groupAddressesRespondingSpec = new HashSet<>();
private final Map<GroupAddress, ScheduledFuture<?>> readFutures = new HashMap<>();
private final Map<ChannelUID, ScheduledFuture<?>> channelFutures = new HashMap<>();
private final Map<GroupAddress, @Nullable ScheduledFuture<?>> readFutures = new HashMap<>();
private final Map<ChannelUID, @Nullable ScheduledFuture<?>> channelFutures = new HashMap<>();
private int readInterval;

public DeviceThingHandler(Thing thing) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tuwien.auto.calimero.KnxSecureException;
import tuwien.auto.calimero.internal.Security;

/**
* 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
Expand Down Expand Up @@ -57,6 +60,15 @@ public IPBridgeThingHandler(Bridge bridge, NetworkAddressService networkAddressS
@Override
public void initialize() {
IPBridgeConfiguration config = getConfigAs(IPBridgeConfiguration.class);
try {
inializeSecurity(config.getKeyringFile(), config.getKeyringPassword());
} catch (KnxSecureException e) {
logger.warn("{}", e.toString());
}
logger.debug("Security enabled for {} group addresses, {} devices",
Security.defaultInstallation().groupKeys().size(),
Security.defaultInstallation().deviceToolKeys().size());
logger.debug("Security: {}", Security.defaultInstallation().groupSenders().toString());
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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
package org.openhab.binding.knx.internal.handler;

import java.io.File;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
Expand All @@ -20,6 +21,7 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.knx.internal.client.KNXClient;
import org.openhab.binding.knx.internal.client.StatusUpdateCallback;
import org.openhab.core.OpenHAB;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
Expand All @@ -29,6 +31,9 @@
import org.openhab.core.types.Command;

import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.Keyring;
import tuwien.auto.calimero.KnxSecureException;
import tuwien.auto.calimero.internal.Security;
import tuwien.auto.calimero.mgmt.Destination;

/**
Expand All @@ -43,13 +48,47 @@ public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implem
protected ConcurrentHashMap<IndividualAddress, Destination> destinations = new ConcurrentHashMap<>();
private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx");
private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor();
private @Nullable Keyring keyring;
private @Nullable String keyringPassword;

public KNXBridgeBaseThingHandler(Bridge bridge) {
super(bridge);
}

protected abstract KNXClient getClient();

protected void inializeSecurity(String keyringFile, String password) {
keyring = null;
keyringPassword = null;

if (keyringFile != null && !keyringFile.trim().isEmpty()) {
try {
// load keyring file from config dir, folder misc
String keyringUri = OpenHAB.getConfigFolder() + File.separator + "misc" + File.separator
+ keyringFile.trim();
keyring = Keyring.load(keyringUri);
if (keyring == null)
throw new KnxSecureException("keyring file configured, but loading failed: " + keyringUri);

// loading was successful, check signatures
if (!keyring.verifySignature(password.toCharArray()))
throw new KnxSecureException(
"signature verification failed, please check keyring file: " + keyringUri);
keyringPassword = password;

// Add to global static key(ring) storage of Calimero library.
// More than one can be added ONLY IF addresses are different,
// as Calimero adds all information to this static object.
// -> to be discussed with owner of Calimero lib.
Security.defaultInstallation().useKeyring(keyring, keyringPassword.toCharArray());
} catch (KnxSecureException e) {
keyring = null;
keyringPassword = null;
throw e;
}
}
}

@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Nothing to do here
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
import org.openhab.binding.knx.internal.config.SerialBridgeConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tuwien.auto.calimero.KnxSecureException;
import tuwien.auto.calimero.internal.Security;

/**
* The {@link IPBridgeThingHandler} is responsible for handling commands, which are
Expand All @@ -31,6 +36,7 @@
@NonNullByDefault
public class SerialBridgeThingHandler extends KNXBridgeBaseThingHandler {

private final Logger logger = LoggerFactory.getLogger(SerialBridgeThingHandler.class);
private final SerialClient client;

public SerialBridgeThingHandler(Bridge bridge) {
Expand All @@ -44,6 +50,15 @@ public SerialBridgeThingHandler(Bridge bridge) {
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN);
SerialBridgeConfiguration config = getConfigAs(SerialBridgeConfiguration.class);
try {
inializeSecurity(config.getKeyringFile(), config.getKeyringPassword());
} catch (KnxSecureException e) {
logger.warn("{}", e.toString());
}
logger.debug("Security enabled for {} group addresses, {} devices",
Security.defaultInstallation().groupKeys().size(),
Security.defaultInstallation().deviceToolKeys().size());
client.initialize();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@
<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">
<label>Keyring file</label>
<description>Keyring file exported from ETS and placed in openhab config/misc folder, e.g. knx.knxkeys</description>
<default></default>
</parameter>
<parameter name="keyringPassword" type="text">
<label>Keyring password</label>
<description>Keyring file password (set during exported from ETS)</description>
<default></default>
</parameter>
</config-description>
</bridge-type>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
<required>true</required>
<default>0</default>
</parameter>
<parameter name="keyringFile" type="text">
<label>Keyring file</label>
<description>Keyring file exported from ETS and placed in openhab config/misc folder, e.g. knx.knxkeys</description>
<default></default>
</parameter>
<parameter name="keyringPassword" type="text">
<label>Keyring password</label>
<description>Keyring file password (set during exported from ETS)</description>
<default></default>
</parameter>
</config-description>
</bridge-type>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Copyright (c) 2010-2020 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.util.Map;

import org.junit.jupiter.api.Test;

import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.IndividualAddress;
import tuwien.auto.calimero.Keyring;
import tuwien.auto.calimero.internal.Security;

/**
*
* @author Simon Kaufmann - initial contribution and API
*
*/
public class KNXSecurityTest {

@Test
public void testCalimero_keyring() {
final String testFile = getClass().getClassLoader().getResource("test.knxkeys").toString();
final char[] password = "habopen".toCharArray();

assertNotEquals("", testFile);
Keyring keys = Keyring.load(testFile);
assertTrue(keys.verifySignature(password));

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);
assertNotEquals(0, key800enc.length);
byte[] key800dec = keys.decryptKey(key800enc, password);
assertEquals(16, key800dec.length);

IndividualAddress pa = new IndividualAddress(1, 2, 72);
Keyring.Device dev = keys.devices().get(pa);
// cannot check this for dummy test file, needs real device to be included
// assertNotEquals(0, dev.sequenceNumber());

// currently Calimero uses _one_ static map to store all keys
// -> check if this is still the case
Security.defaultInstallation().useKeyring(keys, password);
Map<GroupAddress, byte[]> groupKeys = Security.defaultInstallation().groupKeys();
assertEquals(3, groupKeys.size());
groupKeys.remove(ga);
assertEquals(2, groupKeys.size());
Security.defaultInstallation().useKeyring(keys, password);
Map<GroupAddress, byte[]> groupKeys2 = Security.defaultInstallation().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());
Security.defaultInstallation().useKeyring(keys, password);
assertEquals(4, groupKeys2.size());
assertEquals(4, groupKeys.size());
}
}
Loading

0 comments on commit 4fe7539

Please sign in to comment.