]> git.basschouten.com Git - openhab-addons.git/blob
ed2e8111ac33f97f46d04dd478962103c902f099
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.knx.internal.handler;
14
15 import java.io.File;
16 import java.util.HashMap;
17 import java.util.HashSet;
18 import java.util.Iterator;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.NoSuchElementException;
22 import java.util.Optional;
23 import java.util.Set;
24 import java.util.TreeMap;
25 import java.util.concurrent.Executors;
26 import java.util.concurrent.ScheduledExecutorService;
27
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;
40
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;
51
52 /**
53  * The {@link KNXBridgeBaseThingHandler} is responsible for handling commands, which are
54  * sent to one of the channels.
55  *
56  * @author Simon Kaufmann - Initial contribution and API
57  * @author Holger Friedrich - KNX Secure configuration
58  */
59 @NonNullByDefault
60 public abstract class KNXBridgeBaseThingHandler extends BaseBridgeHandler implements StatusUpdateCallback {
61
62     public static class SecureTunnelConfig {
63         public SecureTunnelConfig() {
64             devKey = new byte[0];
65             userKey = new byte[0];
66             user = 0;
67         }
68
69         public byte[] devKey;
70         public byte[] userKey;
71         public int user = 0;
72     }
73
74     public static class SecureRoutingConfig {
75         public SecureRoutingConfig() {
76             backboneGroupKey = new byte[0];
77             latencyToleranceMs = 0;
78         }
79
80         public byte[] backboneGroupKey;
81         public long latencyToleranceMs = 0;
82     }
83
84     /**
85      * Helper class to carry information which can be used by the
86      * command line extension (openHAB console).
87      */
88     public record CommandExtensionData(Map<String, Long> unknownGA) {
89     }
90
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)
97
98     protected Security openhabSecurity;
99     protected SecureRoutingConfig secureRouting;
100     protected SecureTunnelConfig secureTunnel;
101     private CommandExtensionData commandExtensionData;
102
103     public KNXBridgeBaseThingHandler(Bridge bridge) {
104         super(bridge);
105         keyring = Optional.empty();
106         openhabSecurity = Security.newSecurity();
107         secureRouting = new SecureRoutingConfig();
108         secureTunnel = new SecureTunnelConfig();
109         commandExtensionData = new CommandExtensionData(new TreeMap<>());
110     }
111
112     protected abstract KNXClient getClient();
113
114     public CommandExtensionData getCommandExtensionData() {
115         return commandExtensionData;
116     }
117
118     /***
119      * Initialize KNX secure if configured (simple interface)
120      *
121      * @param cKeyringFile keyring file, exported from ETS tool
122      * @param cKeyringPassword keyring password, set during export from ETS tool
123      * @return
124      */
125     protected boolean initializeSecurity(String cKeyringFile, String cKeyringPassword) throws KnxSecureException {
126         return initializeSecurity(cKeyringFile, cKeyringPassword, "", "", "", "", "");
127     }
128
129     /***
130      * Initialize KNX secure if configured (full interface)
131      *
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.
143      * @return
144      */
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();
153
154         boolean securityInitialized = false;
155
156         // step 1: secure routing, backbone group key manually specified in OH config (typically it is read from
157         // keyring)
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;
165             }
166         }
167
168         // step 2: check if valid tunnel parameters are specified in config
169         if (!cTunnelSourceAddr.isBlank()) {
170             try {
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");
175             }
176         }
177         if (!cTunnelDevAuth.isBlank()) {
178             secureTunnel.devKey = SecureConnection.hashDeviceAuthenticationPassword(cTunnelDevAuth.toCharArray());
179             securityInitialized = true;
180         }
181         if (!cTunnelPassword.isBlank()) {
182             secureTunnel.userKey = SecureConnection.hashUserPassword(cTunnelPassword.toCharArray());
183             securityInitialized = true;
184         }
185         if (!cTunnelUser.isBlank()) {
186             String user = cTunnelUser.trim();
187             try {
188                 secureTunnel.user = Integer.decode(user);
189             } catch (NumberFormatException e) {
190                 throw new KnxSecureException("tunnelUser must be a number >0");
191             }
192             if (secureTunnel.user <= 0) {
193                 throw new KnxSecureException("tunnelUser must be a number >0");
194             }
195             securityInitialized = true;
196         }
197
198         // step 3: keyring
199         if (!cKeyringFile.isBlank()) {
200             // filename defined in config, start parsing
201             try {
202                 // load keyring file from config dir, folder misc
203                 String keyringUri = OpenHAB.getConfigFolder() + File.separator + "misc" + File.separator + cKeyringFile;
204                 try {
205                     keyring = Optional.ofNullable(Keyring.load(keyringUri));
206                 } catch (KNXMLException e) {
207                     throw new KnxSecureException("keyring file configured, but loading failed: ", e);
208                 }
209                 if (!keyring.isPresent()) {
210                     throw new KnxSecureException("keyring file configured, but loading failed: " + keyringUri);
211                 }
212
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;
219
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());
224
225                 securityInitialized = true;
226             } catch (KnxSecureException e) {
227                 keyring = Optional.empty();
228                 keyringPassword = "";
229                 throw e;
230             } catch (Exception e) {
231                 // load() may throw KnxSecureException or other undeclared exceptions, e.g. UncheckedIOException when
232                 // file is not found
233                 keyring = Optional.empty();
234                 keyringPassword = "";
235                 throw new KnxSecureException("keyring file configured, but loading failed", e);
236             }
237         }
238
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;
246             }
247         }
248
249         // step 5: router: load latencyTolerance
250         // default to 2000ms
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;
259             }
260         }
261
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");
267             }
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");
272             }
273
274             Optional<SecureTunnelConfig> config = secHelperReadTunnelConfig(keyring, keyringPassword,
275                     secureTunnelSourceAddr);
276             if (config.isEmpty()) {
277                 throw new KnxSecureException("tunnel definition cannot be read from keyring");
278             }
279             secureTunnel = config.get();
280         }
281         return securityInitialized;
282     }
283
284     /***
285      * converts hex string (32 characters) to byte[16]
286      *
287      * @param hexString 32 characters hex
288      * @return key in byte array format
289      */
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)");
293         }
294
295         byte[] parsed = new byte[16];
296         try {
297             for (byte i = 0; i < 16; i++) {
298                 parsed[i] = (byte) Integer.parseInt(hexString.substring(2 * i, 2 * i + 2), 16);
299             }
300         } catch (NumberFormatException e) {
301             throw new KnxSecureException("backbone key configured, cannot parse hex string, illegal character", e);
302         }
303         return parsed;
304     }
305
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");
309         }
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");
318                 }
319                 return Optional.of(secureRoutingBackboneGroupKey);
320             }
321         }
322         return Optional.empty();
323     }
324
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");
329         }
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();
340
341                 if (secureTunnelSourceAddr.equals(eTunnel.address())) {
342                     String pw = "";
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());
347                     }
348
349                     String au = "";
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())
354                                 .clone();
355                     }
356
357                     // set user, 0=fail
358                     secureTunnel.user = eTunnel.user();
359
360                     return Optional.of(secureTunnel);
361                 }
362             }
363         }
364         return Optional.empty();
365     }
366
367     /***
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());
371      */
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;
380             try {
381                 surrogate = senders.getOrDefault(ga, Set.of()).stream().findAny().get();
382             } catch (NoSuchElementException e) {
383             }
384             Set<String> devices = new HashSet<String>();
385             for (var device : entry.getValue()) {
386                 if (device.equals(surrogate)) {
387                     devices.add(device.toString() + " (S)");
388                 } else {
389                     devices.add(device.toString());
390                 }
391             }
392             groupSendersWithSurrogate.put(ga, devices);
393         }
394         return groupSendersWithSurrogate.toString();
395     }
396
397     @Override
398     public void handleCommand(ChannelUID channelUID, Command command) {
399         // Nothing to do here
400     }
401
402     public ScheduledExecutorService getScheduler() {
403         return knxScheduler;
404     }
405
406     public ScheduledExecutorService getBackgroundScheduler() {
407         return backgroundScheduler;
408     }
409
410     @Override
411     public void updateStatus(ThingStatus status) {
412         super.updateStatus(status);
413     }
414
415     @Override
416     public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
417         super.updateStatus(status, statusDetail, description);
418     }
419 }