2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.knx.internal.handler;
16 import java.util.HashMap;
17 import java.util.HashSet;
18 import java.util.Iterator;
19 import java.util.List;
21 import java.util.NoSuchElementException;
22 import java.util.Optional;
24 import java.util.TreeMap;
25 import java.util.concurrent.Executors;
26 import java.util.concurrent.ScheduledExecutorService;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.knx.internal.client.KNXClient;
31 import org.openhab.binding.knx.internal.client.StatusUpdateCallback;
32 import org.openhab.core.OpenHAB;
33 import org.openhab.core.common.ThreadPoolManager;
34 import org.openhab.core.thing.Bridge;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.binding.BaseBridgeHandler;
39 import org.openhab.core.types.Command;
41 import tuwien.auto.calimero.GroupAddress;
42 import tuwien.auto.calimero.IndividualAddress;
43 import tuwien.auto.calimero.KNXFormatException;
44 import tuwien.auto.calimero.knxnetip.SecureConnection;
45 import tuwien.auto.calimero.secure.Keyring;
46 import tuwien.auto.calimero.secure.Keyring.Backbone;
47 import tuwien.auto.calimero.secure.Keyring.Interface;
48 import tuwien.auto.calimero.secure.KnxSecureException;
49 import tuwien.auto.calimero.secure.Security;
50 import tuwien.auto.calimero.xml.KNXMLException;
53 * The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are
54 * sent to one of the channels.
56 * @author Simon Kaufmann - Initial contribution and API
57 * @author Holger Friedrich - KNX Secure configuration
60 public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implements StatusUpdateCallback {
62 public static class SecureTunnelConfig {
63 public SecureTunnelConfig() {
65 userKey = new byte[0];
70 public byte[] userKey;
74 public static class SecureRoutingConfig {
75 public SecureRoutingConfig() {
76 backboneGroupKey = new byte[0];
77 latencyToleranceMs = 0;
80 public byte[] backboneGroupKey;
81 public long latencyToleranceMs = 0;
85 * Helper class to carry information which can be used by the
86 * command line extension (openHAB console).
88 public record CommandExtensionData(Map<String, Long> unknownGA) {
91 private final ScheduledExecutorService knxScheduler = ThreadPoolManager.getScheduledPool("knx");
92 private final ScheduledExecutorService backgroundScheduler = Executors.newSingleThreadScheduledExecutor();
93 protected Optional<Keyring> keyring;
94 // password used to protect content of the keyring
95 private String keyringPassword = "";
96 // backbone key (shared password used for secure router mode)
98 protected Security openhabSecurity;
99 protected SecureRoutingConfig secureRouting;
100 protected SecureTunnelConfig secureTunnel;
101 private CommandExtensionData commandExtensionData;
103 public KNXBridgeBaseThingHandler(Bridge bridge) {
105 keyring = Optional.empty();
106 openhabSecurity = Security.newSecurity();
107 secureRouting = new SecureRoutingConfig();
108 secureTunnel = new SecureTunnelConfig();
109 commandExtensionData = new CommandExtensionData(new TreeMap<>());
112 protected abstract KNXClient getClient();
114 public CommandExtensionData getCommandExtensionData() {
115 return commandExtensionData;
119 * Initialize KNX secure if configured (simple interface)
121 * @param cKeyringFile keyring file, exported from ETS tool
122 * @param cKeyringPassword keyring password, set during export from ETS tool
125 protected boolean initializeSecurity(String cKeyringFile, String cKeyringPassword) throws KnxSecureException {
126 return initializeSecurity(cKeyringFile, cKeyringPassword, "", "", "", "", "");
130 * Initialize KNX secure if configured (full interface)
132 * @param cKeyringFile keyring file, exported from ETS tool
133 * @param cKeyringPassword keyring password, set during export from ETS tool
134 * @param cRouterBackboneGroupKey shared key for secure router mode. If not given, it will be read from keyring.
135 * @param cTunnelDevAuth device password for IP interface in tunnel mode. If not given it will be read from keyring
136 * if cTunnelSourceAddr is configured.
137 * @param cTunnelUser user id for tunnel mode. Must be an integer >0. If not given it will be read from keyring if
138 * cTunnelSourceAddr is configured.
139 * @param cTunnelPassword user password for tunnel mode. If not given it will be read from keyring if
140 * cTunnelSourceAddr is configured.
141 * @param cTunnelSourceAddr specify the KNX address to uniquely identify a tunnel connection in secure tunneling
142 * mode. Not required if cTunnelDevAuth, cTunnelUser, and cTunnelPassword are given.
145 protected boolean initializeSecurity(String cKeyringFile, String cKeyringPassword, String cRouterBackboneGroupKey,
146 String cTunnelDevAuth, String cTunnelUser, String cTunnelPassword, String cTunnelSourceAddr)
147 throws KnxSecureException {
148 keyring = Optional.empty();
149 keyringPassword = "";
150 IndividualAddress secureTunnelSourceAddr = null;
151 secureRouting = new SecureRoutingConfig();
152 secureTunnel = new SecureTunnelConfig();
154 boolean securityInitialized = false;
156 // step 1: secure routing, backbone group key manually specified in OH config (typically it is read from
158 if (!cRouterBackboneGroupKey.isBlank()) {
159 // provided in config, this will override whatever is read from keyring
160 String key = cRouterBackboneGroupKey.trim().replaceFirst("^0x", "").trim().replace(" ", "");
161 if (!key.isEmpty()) {
162 // helper may throw KnxSecureException
163 secureRouting.backboneGroupKey = secHelperParseBackboneKey(key);
164 securityInitialized = true;
168 // step 2: check if valid tunnel parameters are specified in config
169 if (!cTunnelSourceAddr.isBlank()) {
171 secureTunnelSourceAddr = new IndividualAddress(cTunnelSourceAddr.trim());
172 securityInitialized = true;
173 } catch (KNXFormatException e) {
174 throw new KnxSecureException("tunnel source address cannot be parsed, valid format is x.y.z");
177 if (!cTunnelDevAuth.isBlank()) {
178 secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(cTunnelDevAuth.toCharArray());
179 securityInitialized = true;
181 if (!cTunnelPassword.isBlank()) {
182 secureTunnel.userKey = SecureConnection.hashUserPassword(cTunnelPassword.toCharArray());
183 securityInitialized = true;
185 if (!cTunnelUser.isBlank()) {
186 String user = cTunnelUser.trim();
188 secureTunnel.user = Integer.decode(user);
189 } catch (NumberFormatException e) {
190 throw new KnxSecureException("tunnelUser must be a number >0");
192 if (secureTunnel.user <= 0) {
193 throw new KnxSecureException("tunnelUser must be a number >0");
195 securityInitialized = true;
199 if (!cKeyringFile.isBlank()) {
200 // filename defined in config, start parsing
202 // load keyring file from config dir, folder misc
203 String keyringUri = OpenHAB.getConfigFolder() + File.separator + "misc" + File.separator + cKeyringFile;
205 keyring = Optional.ofNullable(Keyring.load(keyringUri));
206 } catch (KNXMLException e) {
207 throw new KnxSecureException("keyring file configured, but loading failed: ", e);
209 if (!keyring.isPresent()) {
210 throw new KnxSecureException("keyring file configured, but loading failed: " + keyringUri);
213 // loading was successful, check signatures
214 // -> disabled, as Calimero v2.5 does this within the load() function
215 // if (!keyring.verifySignature(cKeyringPassword.toCharArray()))
216 // throw new KnxSecureException(
217 // "signature verification failed, please check keyring file: " + keyringUri);
218 keyringPassword = cKeyringPassword;
220 // We use a specific Security instance instead of default Calimero static instance
221 // Security.defaultInstallation().
222 // This necessary as it seems there is no possibility to clear the global instance on config changes.
223 openhabSecurity.useKeyring(keyring.get(), keyringPassword.toCharArray());
225 securityInitialized = true;
226 } catch (KnxSecureException e) {
227 keyring = Optional.empty();
228 keyringPassword = "";
230 } catch (Exception e) {
231 // load() may throw KnxSecureException or other undeclared exceptions, e.g. UncheckedIOException when
233 keyring = Optional.empty();
234 keyringPassword = "";
235 throw new KnxSecureException("keyring file configured, but loading failed", e);
239 // step 4: router: load backboneGroupKey from keyring if not manually specified
240 if ((secureRouting.backboneGroupKey.length == 0) && (keyring.isPresent())) {
241 // backbone group key is only available if secure routers are present
242 final Optional<byte[]> key = secHelperReadBackboneKey(keyring, keyringPassword);
243 if (key.isPresent()) {
244 secureRouting.backboneGroupKey = key.get();
245 securityInitialized = true;
249 // step 5: router: load latencyTolerance
251 // this parameter is currently not exposed in config, in case it must be set by using the keyring
252 secureRouting.latencyToleranceMs = 2000;
253 if (keyring.isPresent()) {
254 // backbone latency is only relevant if secure routers are present
255 final Optional<Backbone> bb = keyring.get().backbone();
256 if (bb.isPresent()) {
257 final long toleranceMs = bb.get().latencyTolerance().toMillis();
258 secureRouting.latencyToleranceMs = toleranceMs;
262 // step 6: tunnel: load data from keyring
263 if (secureTunnelSourceAddr != null) {
264 // requires a valid keyring
265 if (!keyring.isPresent()) {
266 throw new KnxSecureException("valid keyring specification required for secure tunnel mode");
268 // other parameters will not be accepted, since all is read from keyring in this case
269 if ((secureTunnel.userKey.length > 0) || secureTunnel.user != 0 || (secureTunnel.devKey.length > 0)) {
270 throw new KnxSecureException(
271 "tunnelSourceAddr is configured, please do not specify other parameters of secure tunnel");
274 Optional<SecureTunnelConfig> config = secHelperReadTunnelConfig(keyring, keyringPassword,
275 secureTunnelSourceAddr);
276 if (config.isEmpty()) {
277 throw new KnxSecureException("tunnel definition cannot be read from keyring");
279 secureTunnel = config.get();
281 return securityInitialized;
285 * converts hex string (32 characters) to byte[16]
287 * @param hexString 32 characters hex
288 * @return key in byte array format
290 public static byte[] secHelperParseBackboneKey(String hexString) throws KnxSecureException {
291 if (hexString.length() != 32) {
292 throw new KnxSecureException("backbone key must be 32 characters (16 byte hex notation)");
295 byte[] parsed = new byte[16];
297 for (byte i = 0; i < 16; i++) {
298 parsed[i] = (byte) Integer.parseInt(hexString.substring(2 * i, 2 * i + 2), 16);
300 } catch (NumberFormatException e) {
301 throw new KnxSecureException("backbone key configured, cannot parse hex string, illegal character", e);
306 public static Optional<byte[]> secHelperReadBackboneKey(Optional<Keyring> keyring, String keyringPassword) {
307 if (keyring.isEmpty()) {
308 throw new KnxSecureException("keyring not available, cannot read backbone key");
310 final Optional<Backbone> bb = keyring.get().backbone();
311 if (bb.isPresent()) {
312 final Optional<byte[]> gk = bb.get().groupKey();
313 if (gk.isPresent()) {
314 byte[] secureRoutingBackboneGroupKey = keyring.get().decryptKey(gk.get(),
315 keyringPassword.toCharArray());
316 if (secureRoutingBackboneGroupKey.length != 16) {
317 throw new KnxSecureException("backbone key found, unexpected length != 16");
319 return Optional.of(secureRoutingBackboneGroupKey);
322 return Optional.empty();
325 public static Optional<SecureTunnelConfig> secHelperReadTunnelConfig(Optional<Keyring> keyring,
326 String keyringPassword, IndividualAddress secureTunnelSourceAddr) {
327 if (keyring.isEmpty()) {
328 throw new KnxSecureException("keyring not available, cannot read tunnel config");
330 // iterate all interfaces to find matching secureTunnelSourceAddr
331 SecureTunnelConfig secureTunnel = new SecureTunnelConfig();
332 Iterator<List<Interface>> itInterface = keyring.get().interfaces().values().iterator();
333 boolean complete = false;
334 while (!complete && itInterface.hasNext()) {
335 List<Interface> eInterface = itInterface.next();
336 // tunnels are nested
337 Iterator<Interface> itTunnel = eInterface.iterator();
338 while (!complete && itTunnel.hasNext()) {
339 Interface eTunnel = itTunnel.next();
341 if (secureTunnelSourceAddr.equals(eTunnel.address())) {
343 final Optional<byte[]> pwBytes = eTunnel.password();
344 if (pwBytes.isPresent()) {
345 pw = new String(keyring.get().decryptPassword(pwBytes.get(), keyringPassword.toCharArray()));
346 secureTunnel.userKey = SecureConnection.hashUserPassword(pw.toCharArray());
350 final Optional<byte[]> auBytes = eTunnel.authentication();
351 if (auBytes.isPresent()) {
352 au = new String(keyring.get().decryptPassword(auBytes.get(), keyringPassword.toCharArray()));
353 secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(au.toCharArray())
358 secureTunnel.user = eTunnel.user();
360 return Optional.of(secureTunnel);
364 return Optional.empty();
368 * Show all secure group addresses and surrogates. A surrogate is the device which is asked to carry out an indirect
369 * read/write request.
370 * Simpler approach w/o surrogates: Security.defaultInstallation().groupSenders().toString());
372 public static String secHelperGetSecureGroupAddresses(final Security openhabSecurity) {
373 Map<GroupAddress, Set<String>> groupSendersWithSurrogate = new HashMap<GroupAddress, Set<String>>();
374 final Map<GroupAddress, Set<IndividualAddress>> senders = openhabSecurity.groupSenders();
375 for (var entry : senders.entrySet()) {
376 final GroupAddress ga = entry.getKey();
377 // the following approach is uses by Calimero to deduce the surrogate for GA diagnostics
378 // see calimero-core security/SecureApplicationLayer.java, surrogate(...)
379 IndividualAddress surrogate = null;
381 surrogate = senders.getOrDefault(ga, Set.of()).stream().findAny().get();
382 } catch (NoSuchElementException e) {
384 Set<String> devices = new HashSet<String>();
385 for (var device : entry.getValue()) {
386 if (device.equals(surrogate)) {
387 devices.add(device.toString() + " (S)");
389 devices.add(device.toString());
392 groupSendersWithSurrogate.put(ga, devices);
394 return groupSendersWithSurrogate.toString();
398 public void handleCommand(ChannelUID channelUID, Command command) {
399 // Nothing to do here
402 public ScheduledExecutorService getScheduler() {
406 public ScheduledExecutorService getBackgroundScheduler() {
407 return backgroundScheduler;
411 public void updateStatus(ThingStatus status) {
412 super.updateStatus(status);
416 public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
417 super.updateStatus(status, statusDetail, description);