]> git.basschouten.com Git - openhab-addons.git/blob
a14e0e4b3c771c935c4e6be05afb651f825f08d8
[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.doorbird.internal.listener;
14
15 import java.net.DatagramPacket;
16 import java.nio.BufferUnderflowException;
17 import java.nio.ByteBuffer;
18 import java.nio.charset.StandardCharsets;
19 import java.util.Arrays;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.core.util.HexUtils;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26
27 import com.goterl.lazycode.lazysodium.LazySodiumJava;
28 import com.goterl.lazycode.lazysodium.SodiumJava;
29 import com.goterl.lazycode.lazysodium.exceptions.SodiumException;
30 import com.goterl.lazycode.lazysodium.interfaces.PwHash;
31 import com.sun.jna.NativeLong;
32
33 /**
34  * The {@link DoorbirdEvent} is responsible for decoding event packets received
35  * from the Doorbird.
36  *
37  * @author Mark Hilbush - Initial contribution
38  */
39 @NonNullByDefault
40 public class DoorbirdEvent {
41     private final Logger logger = LoggerFactory.getLogger(DoorbirdEvent.class);
42
43     // These values are extracted from the UDP packet
44     private byte version;
45     private int opslimit;
46     private long memlimit;
47     private byte[] salt = new byte[16];
48     private byte[] nonce = new byte[8];
49     private byte[] ciphertext = new byte[34];
50
51     // Starting 6 characters from the user name
52     private @Nullable String eventIntercomId;
53
54     // Doorbell number for doorbell event, or "motion" for motion events
55     private @Nullable String eventId;
56
57     // Timestamp of event
58     private long eventTimestamp;
59
60     private boolean isDoorbellEvent;
61
62     /*
63      * We want a single instance of LazySodium. Also, try to load the libsodium library
64      * from multiple sources.
65      *
66      * To load the libsodium library,
67      * - first try to load the library from the resources that are bundled with
68      * the LazySodium jar. (i.e. new SodiumJava())
69      * - if that fails with an UnsatisfiedLinkError, then try to load the library
70      * from the operating system (i.e. new SodiumJava("sodium").
71      * - if both of these attempts fail, the binding will be functional, except for
72      * its ability to decrypt the UDP events.
73      */
74     @NonNullByDefault
75     private static class LazySodiumJavaHolder {
76         private static final Logger LOGGER = LoggerFactory.getLogger(LazySodiumJavaHolder.class);
77
78         static final @Nullable LazySodiumJava LAZY_SODIUM_JAVA_INSTANCE = loadLazySodiumJava();
79
80         private static @Nullable LazySodiumJava loadLazySodiumJava() {
81             LOGGER.debug("LazySodium has not been loaded yet. Try to load it now.");
82             LazySodiumJava lazySodiumJava = null;
83             try {
84                 lazySodiumJava = new LazySodiumJava(new SodiumJava());
85                 LOGGER.debug("Successfully loaded bundled libsodium crypto library!!");
86             } catch (UnsatisfiedLinkError e1) {
87                 try {
88                     LOGGER.debug("Unable to load bundled libsodium crypto library!! Try to load OS version.", e1);
89                     lazySodiumJava = new LazySodiumJava(new SodiumJava("sodium"));
90                     LOGGER.debug("Successfully loaded libsodium crypto library from operating system!!");
91                 } catch (UnsatisfiedLinkError e2) {
92                     LOGGER.info("Failed to load libsodium crypto library!!", e2);
93                     LOGGER.info("Try manually installing libsodium on your OS if libsodium supports your architecture");
94                 }
95             }
96             return lazySodiumJava;
97         }
98     }
99
100     public static @Nullable LazySodiumJava getLazySodiumJavaInstance() {
101         return LazySodiumJavaHolder.LAZY_SODIUM_JAVA_INSTANCE;
102     }
103
104     // Will be true if this is a valid Doorbird event
105     public boolean isDoorbellEvent() {
106         return isDoorbellEvent;
107     }
108
109     // Contains the intercomId for valid Doorbird events
110     public @Nullable String getIntercomId() {
111         return eventIntercomId;
112     }
113
114     // Contains the eventId for valid Doorbird events
115     public @Nullable String getEventId() {
116         return eventId;
117     }
118
119     // Contains the timestamp for valid Doorbird events
120     public long getTimestamp() {
121         return eventTimestamp;
122     }
123
124     /*
125      * The following functions support the decryption of the doorbell event
126      * using the LazySodium wrapper for the libsodium crypto library
127      */
128     public void decrypt(DatagramPacket p, String password) {
129         isDoorbellEvent = false;
130
131         int length = p.getLength();
132         byte[] data = Arrays.copyOf(p.getData(), length);
133
134         // A valid event contains a 3 byte signature followed by the decryption version
135         if (length < 4) {
136             return;
137         }
138
139         // Only the first 5 characters of the password are used to generate the decryption key
140         if (password.length() < 5) {
141             logger.info("Invalid password length, must be at least 5 characters");
142             return;
143         }
144         String passwordFirstFive = password.substring(0, 5);
145
146         try {
147             // Load the message into the ByteBuffer
148             ByteBuffer bb = ByteBuffer.allocate(length);
149             bb.put(data, 0, length);
150             bb.rewind();
151             // Check for proper event signature
152             if (!isValidSignature(bb)) {
153                 logger.trace("Received event not a doorbell event: {}", new String(data, StandardCharsets.US_ASCII));
154                 return;
155             }
156             // Get the decryption version
157             version = getVersion(bb);
158             if (version == 1) {
159                 // Decrypt using version 1 decryption scheme
160                 decryptV1(bb, passwordFirstFive);
161             } else {
162                 logger.info("Don't know how to decrypt version {} doorbell event", version);
163             }
164         } catch (IndexOutOfBoundsException e) {
165             logger.info("IndexOutOfBoundsException decrypting doorbell event", e);
166         } catch (BufferUnderflowException e) {
167             logger.info("BufferUnderflowException decrypting doorbell event", e);
168         }
169     }
170
171     private boolean isValidSignature(ByteBuffer bb) throws IndexOutOfBoundsException, BufferUnderflowException {
172         // Check the first three bytes for the proper signature
173         return (bb.get() & 0xFF) == 0xDE && (bb.get() & 0xFF) == 0xAD && (bb.get() & 0xFF) == 0xBE;
174     }
175
176     private byte getVersion(ByteBuffer bb) throws IndexOutOfBoundsException, BufferUnderflowException {
177         // Extract the decryption version from the packet
178         return bb.get();
179     }
180
181     private void decryptV1(ByteBuffer bb, String password5) throws IndexOutOfBoundsException, BufferUnderflowException {
182         LazySodiumJava sodium = getLazySodiumJavaInstance();
183         if (sodium == null) {
184             logger.debug("Unable to decrypt event because libsodium is not loaded");
185             return;
186         }
187         if (bb.capacity() != 70) {
188             logger.info("Received malformed version 1 doorbell event, length not 70 bytes");
189             return;
190         }
191         // opslimit and memlimit are 4 bytes each
192         opslimit = bb.getInt();
193         memlimit = bb.getInt();
194         // Get salt, nonce, and ciphertext arrays
195         bb.get(salt, 0, salt.length);
196         bb.get(nonce, 0, nonce.length);
197         bb.get(ciphertext, 0, ciphertext.length);
198
199         // Create the hash, which will be used to decrypt the ciphertext
200         byte[] hash;
201         try {
202             logger.trace("Calling cryptoPwHash with passwordFirstFive='{}', opslimit={}, memlimit={}, salt='{}'",
203                     password5, opslimit, memlimit, HexUtils.bytesToHex(salt, " "));
204             String hashAsString = sodium.cryptoPwHash(password5, 32, salt, opslimit, new NativeLong(memlimit),
205                     PwHash.Alg.PWHASH_ALG_ARGON2I13);
206             hash = HexUtils.hexToBytes(hashAsString);
207         } catch (SodiumException e) {
208             logger.info("Got SodiumException", e);
209             return;
210         }
211
212         // Set up the variables for the decryption algorithm
213         byte[] m = new byte[30];
214         long[] mLen = new long[30];
215         byte[] nSec = null;
216         byte[] c = ciphertext;
217         long cLen = ciphertext.length;
218         byte[] ad = null;
219         long adLen = 0;
220         byte[] nPub = nonce;
221         byte[] k = hash;
222
223         // Decrypt the ciphertext
224         logger.trace("Call cryptoAeadChaCha20Poly1305Decrypt with ciphertext='{}', nonce='{}', key='{}'",
225                 HexUtils.bytesToHex(ciphertext, " "), HexUtils.bytesToHex(nonce, " "), HexUtils.bytesToHex(k, " "));
226         boolean success = sodium.cryptoAeadChaCha20Poly1305Decrypt(m, mLen, nSec, c, cLen, ad, adLen, nPub, k);
227         if (!success) {
228             /*
229              * Don't log at debug level since the decryption will fail for events encrypted with
230              * passwords other than the password contained in the thing configuration (reference API
231              * documentation for details)
232              */
233             logger.trace("Decryption FAILED");
234             return;
235         }
236         int decryptedTextLength = (int) mLen[0];
237         if (decryptedTextLength != 18L) {
238             logger.info("Length of decrypted text is invalid, must be 18 bytes");
239             return;
240         }
241         // Get event fields from decrypted text
242         logger.debug("Received and successfully decrypted a Doorbird event!!");
243         ByteBuffer b = ByteBuffer.allocate(decryptedTextLength);
244         b.put(m, 0, decryptedTextLength);
245         b.rewind();
246         byte[] buf = new byte[8];
247         b.get(buf, 0, 6);
248         eventIntercomId = new String(buf, 0, 6).trim();
249         b.get(buf, 0, 8);
250         eventId = new String(buf, 0, 8).trim();
251         eventTimestamp = b.getInt();
252
253         logger.debug("Event is eventId='{}', intercomId='{}', timestamp={}", eventId, eventIntercomId, eventTimestamp);
254         isDoorbellEvent = true;
255     }
256 }