2 * Copyright (c) 2010-2023 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.doorbird.internal.listener;
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;
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;
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;
34 * The {@link DoorbirdEvent} is responsible for decoding event packets received
37 * @author Mark Hilbush - Initial contribution
40 public class DoorbirdEvent {
41 private final Logger logger = LoggerFactory.getLogger(DoorbirdEvent.class);
43 // These values are extracted from the UDP packet
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];
51 // Starting 6 characters from the user name
52 private @Nullable String eventIntercomId;
54 // Doorbell number for doorbell event, or "motion" for motion events
55 private @Nullable String eventId;
58 private long eventTimestamp;
60 private boolean isDoorbellEvent;
63 * We want a single instance of LazySodium. Also, try to load the libsodium library
64 * from multiple sources.
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.
75 private static class LazySodiumJavaHolder {
76 private static final Logger LOGGER = LoggerFactory.getLogger(LazySodiumJavaHolder.class);
78 static final @Nullable LazySodiumJava LAZY_SODIUM_JAVA_INSTANCE = loadLazySodiumJava();
80 private static @Nullable LazySodiumJava loadLazySodiumJava() {
81 LOGGER.debug("LazySodium has not been loaded yet. Try to load it now.");
82 LazySodiumJava lazySodiumJava = null;
84 lazySodiumJava = new LazySodiumJava(new SodiumJava());
85 LOGGER.debug("Successfully loaded bundled libsodium crypto library!!");
86 } catch (UnsatisfiedLinkError e1) {
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");
96 return lazySodiumJava;
100 public static @Nullable LazySodiumJava getLazySodiumJavaInstance() {
101 return LazySodiumJavaHolder.LAZY_SODIUM_JAVA_INSTANCE;
104 // Will be true if this is a valid Doorbird event
105 public boolean isDoorbellEvent() {
106 return isDoorbellEvent;
109 // Contains the intercomId for valid Doorbird events
110 public @Nullable String getIntercomId() {
111 return eventIntercomId;
114 // Contains the eventId for valid Doorbird events
115 public @Nullable String getEventId() {
119 // Contains the timestamp for valid Doorbird events
120 public long getTimestamp() {
121 return eventTimestamp;
125 * The following functions support the decryption of the doorbell event
126 * using the LazySodium wrapper for the libsodium crypto library
128 public void decrypt(DatagramPacket p, String password) {
129 isDoorbellEvent = false;
131 int length = p.getLength();
132 byte[] data = Arrays.copyOf(p.getData(), length);
134 // A valid event contains a 3 byte signature followed by the decryption version
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");
144 String passwordFirstFive = password.substring(0, 5);
147 // Load the message into the ByteBuffer
148 ByteBuffer bb = ByteBuffer.allocate(length);
149 bb.put(data, 0, length);
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));
156 // Get the decryption version
157 version = getVersion(bb);
159 // Decrypt using version 1 decryption scheme
160 decryptV1(bb, passwordFirstFive);
162 logger.info("Don't know how to decrypt version {} doorbell event", version);
164 } catch (IndexOutOfBoundsException e) {
165 logger.info("IndexOutOfBoundsException decrypting doorbell event", e);
166 } catch (BufferUnderflowException e) {
167 logger.info("BufferUnderflowException decrypting doorbell event", e);
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;
176 private byte getVersion(ByteBuffer bb) throws IndexOutOfBoundsException, BufferUnderflowException {
177 // Extract the decryption version from the packet
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");
187 if (bb.capacity() != 70) {
188 logger.info("Received malformed version 1 doorbell event, length not 70 bytes");
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);
199 // Create the hash, which will be used to decrypt the ciphertext
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);
212 // Set up the variables for the decryption algorithm
213 byte[] m = new byte[30];
214 long[] mLen = new long[30];
216 byte[] c = ciphertext;
217 long cLen = ciphertext.length;
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);
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)
233 logger.trace("Decryption FAILED");
236 int decryptedTextLength = (int) mLen[0];
237 if (decryptedTextLength != 18L) {
238 logger.info("Length of decrypted text is invalid, must be 18 bytes");
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);
246 byte[] buf = new byte[8];
248 eventIntercomId = new String(buf, 0, 6).trim();
250 eventId = new String(buf, 0, 8).trim();
251 eventTimestamp = b.getInt();
253 logger.debug("Event is eventId='{}', intercomId='{}', timestamp={}", eventId, eventIntercomId, eventTimestamp);
254 isDoorbellEvent = true;