Skip to content

Commit

Permalink
add initial support for KNX data secure [WIP], openhab#8872
Browse files Browse the repository at this point in the history
* add config options for keyring file(s) and password(s)
* add initial support for reading secure traffic
* add tests for security functions

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
  • Loading branch information
holgerfriedrich committed Dec 11, 2021
1 parent cb3f5b6 commit 47d9b80
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.util.Set;
import java.util.stream.Stream;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;

/**
Expand All @@ -26,6 +27,7 @@
*
* @author Karel Goderis - Initial contribution
*/
@NonNullByDefault
public class KNXBindingConstants {

public static final String BINDING_ID = "knx";
Expand All @@ -51,6 +53,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 @@ -54,6 +54,8 @@
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;
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.
Expand Down Expand Up @@ -192,7 +194,8 @@ private synchronized boolean connect() {
processCommunicator.addProcessListener(processListener);
this.processCommunicator = processCommunicator;

ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link, null);
ProcessCommunicationResponder responseCommunicator = new ProcessCommunicationResponder(link,
new SecureApplicationLayer(link, Security.defaultInstallation()));
this.responseCommunicator = responseCommunicator;

link.addLinkListener(this);
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 @@ -30,6 +30,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import tuwien.auto.calimero.secure.KnxSecureException;
import tuwien.auto.calimero.secure.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 @@ -103,7 +106,19 @@ public void initialize() {
thing.getUID(), config.getResponseTimeout().intValue(), config.getReadingPause().intValue(),
config.getReadRetriesLimit().intValue(), getScheduler(), this);

client.initialize();
scheduler.submit(() -> {
try {
if (initializeSecurity(config.getKeyringFile(), config.getKeyringPassword()))
logger.info("KNX security available for {} group addresses, {} devices",
Security.defaultInstallation().groupKeys().size(),
Security.defaultInstallation().deviceToolKeys().size());
} catch (KnxSecureException e) {
logger.warn("{}", e.toString());
}
logger.debug("Security: {}", Security.defaultInstallation().groupSenders().toString());

client.initialize();
});
}

@Override
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 @@ -30,6 +32,9 @@

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

/**
* The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are
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 boolean initializeSecurity(String keyringFile, String password) {
keyring = null;
keyringPassword = null;

if (keyringFile == null || keyringFile.trim().isEmpty())
return false;
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;
}
return true;
}

@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 @@ -64,6 +64,17 @@
<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>KNX Secure installations: 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>KNX Secure installations: 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 @@ -34,6 +34,17 @@
<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">
<label>Keyring file</label>
<description>KNX Secure installations: 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>KNX Secure installations: 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,98 @@
/**
* Copyright (c) 2010-2021 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.net.URL;
import java.util.Map;

import javax.validation.constraints.NotNull;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;

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

/**
*
* @author Holger Friedrich - initial contribution
*
* Test KNX security features provided by calimero library.
*
*/
@NonNullByDefault
public class KNXSecurityTest {

private void testCalimeroKeyringFile(@NotNull String keyring, @NotNull String password) {
assertNotEquals("", keyring);
final URL testFileUrl = getClass().getClassLoader().getResource(keyring);
assertNotNull(testFileUrl, "keyring file cannot be opened, \"" + keyring + "\"");
final String testFile = testFileUrl.toString();

Keyring keys = Keyring.load(testFile);
assertTrue(keys.verifySignature(password.toCharArray()));

// System.out.println(keys.devices().toString());
// System.out.println(keys.groups().toString());
// System.out.println(keys.interfaces().toString());

assertEquals(2, keys.devices().size());
assertEquals(3, keys.groups().size());
assertEquals(1, keys.interfaces().size());

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

GroupAddress ga = new GroupAddress(8, 0, 0);
byte[] key800enc = keys.groups().get(ga);
assertNotEquals(0, key800enc.length);
byte[] key800dec = keys.decryptKey(key800enc, password.toCharArray());
assertEquals(16, key800dec.length);

// Calimero uses _one_ static map to store all keys: "defaultInstallation".
// For the test, use separate instances to allow testing different input files.
Security secInstance = Security.newSecurity();
secInstance.useKeyring(keys, password.toCharArray());
Map<GroupAddress, byte[]> groupKeys = secInstance.groupKeys();
assertEquals(3, groupKeys.size());
groupKeys.remove(ga);
assertEquals(2, groupKeys.size());
// reload to add removed GA again
secInstance.useKeyring(keys, password.toCharArray());
ga = new GroupAddress(1, 0, 0);
groupKeys.put(ga, new byte[1]);
assertEquals(4, groupKeys.size());

// now add to Security.defaultInstallation
Security.defaultInstallation().useKeyring(keys, password.toCharArray());
}

@Test
public void testCalimero_keyring() {
assertEquals(0, Security.defaultInstallation().groupKeys().size());
assertEquals(0, Security.defaultInstallation().deviceToolKeys().size());

testCalimeroKeyringFile("openhab5.knxkeys", "habopen");
testCalimeroKeyringFile("openhab6.knxkeys", "habopen");

assertNotEquals(0, Security.defaultInstallation().groupKeys().size());
assertNotEquals(0, Security.defaultInstallation().deviceToolKeys().size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Keyring Project="openHAB test" CreatedBy="ETS 5.7.4 (Build 1093)" Created="2020-10-31T06:35:17" Signature="YYyfHr5YuutGoK9bOPLcGg==" xmlns="http://knx.org/xml/keyring/1">
<Backbone MulticastAddress="224.0.23.12" Latency="2000" Key="480bQt90oCDjByEZwzxi8A==" />
<Interface Type="Backbone" IndividualAddress="1.0.128">
<Group Address="16384" Senders="1.2.72" />
<Group Address="17408" Senders="1.2.72" />
</Interface>
<GroupAddresses>
<Group Address="16384" Key="kIhS9Tv+cR0pNJoIIyhByg==" />
<Group Address="17408" Key="OSwUn/dq/Mn+phMCZuU5ww==" />
<Group Address="17409" Key="V0xUCUr4Ft6qqF6UffraMA==" />
</GroupAddresses>
<Devices>
<Device IndividualAddress="1.0.128" ToolKey="dY1PEXGT7EA8ZrPne2Msaw==" ManagementPassword="aRlmkq2B3UOoWiMen5eHkrDRuw8sSGLZTlIP/uqHmV8=" Authentication="PrC1PZ5yDQ+5TCfbwTkuc0Ci8f+bxt1Ej1LZVfv0yNA=" />
<Device IndividualAddress="1.2.72" ToolKey="7tKHLiig7Uxv1mOUc85mdA==" />
</Devices>
</Keyring>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Keyring Project="openHAB test" CreatedBy="6.0.0" Created="2021-11-22T08:00:40" Signature="PuosqSdHUmHiGOELSvL13g==" xmlns="http://knx.org/xml/keyring/1">
<Backbone MulticastAddress="224.0.23.12" Latency="2000" Key="k9ss2OZ9vcjVjQs2glccKQ==" />
<Interface IndividualAddress="1.0.128" Type="Backbone">
<Group Address="16384" Senders="1.2.72" />
<Group Address="17408" Senders="1.2.72" />
</Interface>
<GroupAddresses>
<Group Address="16384" Key="yIY7WF/Gb7E0lTAKsUDIpA==" />
<Group Address="17408" Key="U3plS6TawMuiMXmmRv+yKw==" />
<Group Address="17409" Key="Wb4wZnY0XGmc2CGV3lzgNg==" />
</GroupAddresses>
<Devices>
<Device IndividualAddress="1.0.128" ToolKey="1h9EQZII1/oDyF9S1zjQZA==" ManagementPassword="Hg1lYsFViqfDamBaS7zX9hB/6YcC+/gSYLAjC522cXE=" Authentication="hFeR7Gnr0tMJN+AY/eSWKWLbOh3C7OjVELaL+Y52LEo=" />
<Device IndividualAddress="1.2.72" ToolKey="yybKPPqZffrhxWX7laF0sA==" />
</Devices>
</Keyring>

0 comments on commit 47d9b80

Please sign in to comment.