]> git.basschouten.com Git - openhab-addons.git/commitdiff
[doorbird] Add support for version 2 encryption scheme (#16297)
authorMark Hilbush <mark@hilbush.com>
Wed, 17 Jan 2024 20:07:18 +0000 (15:07 -0500)
committerGitHub <noreply@github.com>
Wed, 17 Jan 2024 20:07:18 +0000 (21:07 +0100)
* Add support for version 2 encryption scheme

Signed-off-by: Mark Hilbush <mark@hilbush.com>
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdAPI.java
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdSession.java [new file with mode: 0644]
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/handler/DoorbellHandler.java
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdEvent.java
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdUdpListener.java
bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/model/GetsessionDTO.java [new file with mode: 0644]

index 6f2e2eddef3fd8a9f2d4e08fd89bd74f8372c4c2..81cf117eba1976c3ddab29a1b505dcd66d690e5c 100644 (file)
@@ -93,15 +93,30 @@ public final class DoorbirdAPI {
             logger.debug("Doorbird returned json response: {}", infoResponse);
             doorbirdInfo = new DoorbirdInfo(infoResponse);
         } catch (IOException e) {
-            logger.info("Unable to communicate with Doorbird: {}", e.getMessage());
+            logger.warn("Unable to communicate with Doorbird: {}", e.getMessage());
         } catch (JsonSyntaxException e) {
-            logger.info("Unable to parse Doorbird response: {}", e.getMessage());
+            logger.warn("Unable to parse Doorbird response: {}", e.getMessage());
         } catch (DoorbirdUnauthorizedException e) {
             logAuthorizationError("getDoorbirdName");
         }
         return doorbirdInfo;
     }
 
+    public @Nullable DoorbirdSession getSession() {
+        DoorbirdSession doorbirdSession = null;
+        try {
+            String sessionResponse = executeGetRequest("/bha-api/getsession.cgi");
+            doorbirdSession = new DoorbirdSession(sessionResponse);
+        } catch (IOException e) {
+            logger.warn("Unable to communicate with Doorbird: {}", e.getMessage());
+        } catch (JsonSyntaxException e) {
+            logger.warn("Unable to parse Doorbird response: {}", e.getMessage());
+        } catch (DoorbirdUnauthorizedException e) {
+            logAuthorizationError("getDoorbirdName");
+        }
+        return doorbirdSession;
+    }
+
     public @Nullable SipStatus getSipStatus() {
         SipStatus sipStatus = null;
         try {
@@ -109,9 +124,9 @@ public final class DoorbirdAPI {
             logger.debug("Doorbird returned json response: {}", statusResponse);
             sipStatus = new SipStatus(statusResponse);
         } catch (IOException e) {
-            logger.info("Unable to communicate with Doorbird: {}", e.getMessage());
+            logger.warn("Unable to communicate with Doorbird: {}", e.getMessage());
         } catch (JsonSyntaxException e) {
-            logger.info("Unable to parse Doorbird response: {}", e.getMessage());
+            logger.warn("Unable to parse Doorbird response: {}", e.getMessage());
         } catch (DoorbirdUnauthorizedException e) {
             logAuthorizationError("getSipStatus");
         }
@@ -123,7 +138,7 @@ public final class DoorbirdAPI {
             String response = executeGetRequest("/bha-api/light-on.cgi");
             logger.debug("Response={}", response);
         } catch (IOException e) {
-            logger.debug("IOException turning on light: {}", e.getMessage());
+            logger.warn("IOException turning on light: {}", e.getMessage());
         } catch (DoorbirdUnauthorizedException e) {
             logAuthorizationError("lightOn");
         }
@@ -134,7 +149,7 @@ public final class DoorbirdAPI {
             String response = executeGetRequest("/bha-api/restart.cgi");
             logger.debug("Response={}", response);
         } catch (IOException e) {
-            logger.debug("IOException restarting device: {}", e.getMessage());
+            logger.warn("IOException restarting device: {}", e.getMessage());
         } catch (DoorbirdUnauthorizedException e) {
             logAuthorizationError("restart");
         }
@@ -145,7 +160,7 @@ public final class DoorbirdAPI {
             String response = executeGetRequest("/bha-api/sip.cgi?action=hangup");
             logger.debug("Response={}", response);
         } catch (IOException e) {
-            logger.debug("IOException hanging up SIP call: {}", e.getMessage());
+            logger.warn("IOException hanging up SIP call: {}", e.getMessage());
         } catch (DoorbirdUnauthorizedException e) {
             logAuthorizationError("sipHangup");
         }
@@ -320,6 +335,6 @@ public final class DoorbirdAPI {
     }
 
     private void logAuthorizationError(String operation) {
-        logger.info("Authorization info is not set or is incorrect on call to '{}' API", operation);
+        logger.warn("Authorization info is not set or is incorrect on call to '{}' API", operation);
     }
 }
diff --git a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdSession.java b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdSession.java
new file mode 100644 (file)
index 0000000..452d980
--- /dev/null
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2010-2024 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.doorbird.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.doorbird.internal.model.GetsessionDTO;
+import org.openhab.binding.doorbird.internal.model.GetsessionDTO.GetsessionBha;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The {@link DoorbirdSession} holds information about the Doorbird session,
+ * including the v2 decryption key for notification events.
+ *
+ * @author Mark Hilbush - Initial contribution
+ */
+@NonNullByDefault
+public class DoorbirdSession {
+    private @Nullable String returnCode;
+    private @Nullable String sessionId;
+    private @Nullable String decryptionKey;
+
+    public DoorbirdSession(String infoJson) throws JsonSyntaxException {
+        GetsessionDTO session = DoorbirdAPI.fromJson(infoJson, GetsessionDTO.class);
+        if (session != null) {
+            GetsessionBha bha = session.bha;
+            returnCode = bha.returnCode;
+            sessionId = bha.sessionId;
+            decryptionKey = bha.decryptionKey;
+        }
+    }
+
+    public @Nullable String getReturnCode() {
+        return returnCode;
+    }
+
+    public @Nullable String getSessionId() {
+        return sessionId;
+    }
+
+    public @Nullable String getDecryptionKey() {
+        return decryptionKey;
+    }
+}
index 0cb0e0955f266bddd3cef1212f6da17dea467859..71a0fd231080d7d097a149869737c69088704731 100644 (file)
@@ -38,6 +38,7 @@ import org.eclipse.jetty.client.HttpClient;
 import org.openhab.binding.doorbird.internal.action.DoorbirdActions;
 import org.openhab.binding.doorbird.internal.api.DoorbirdAPI;
 import org.openhab.binding.doorbird.internal.api.DoorbirdImage;
+import org.openhab.binding.doorbird.internal.api.DoorbirdSession;
 import org.openhab.binding.doorbird.internal.api.SipStatus;
 import org.openhab.binding.doorbird.internal.audio.DoorbirdAudioSink;
 import org.openhab.binding.doorbird.internal.config.DoorbellConfiguration;
@@ -94,6 +95,8 @@ public class DoorbellHandler extends BaseThingHandler {
 
     private DoorbirdAPI api = new DoorbirdAPI();
 
+    private @Nullable DoorbirdSession session;
+
     private BundleContext bundleContext;
 
     private @Nullable ServiceRegistration<AudioSink> audioSinkRegistration;
@@ -130,6 +133,7 @@ public class DoorbellHandler extends BaseThingHandler {
         }
         api.setAuthorization(host, user, password);
         api.setHttpClient(httpClient);
+        session = api.getSession();
         startImageRefreshJob();
         startUDPListenerJob();
         startAudioSink();
@@ -189,6 +193,11 @@ public class DoorbellHandler extends BaseThingHandler {
         updateMotionMontage();
     }
 
+    // Callback used by listener to get session object
+    public @Nullable DoorbirdSession getSession() {
+        return session;
+    }
+
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
         logger.debug("Got command {} for channel {} of thing {}", command, channelUID, getThing().getUID());
index a14e0e4b3c771c935c4e6be05afb651f825f08d8..9479b05f97d521b54564ed8ce35a2bac6d507346 100644 (file)
@@ -71,7 +71,6 @@ public class DoorbirdEvent {
      * - if both of these attempts fail, the binding will be functional, except for
      * its ability to decrypt the UDP events.
      */
-    @NonNullByDefault
     private static class LazySodiumJavaHolder {
         private static final Logger LOGGER = LoggerFactory.getLogger(LazySodiumJavaHolder.class);
 
@@ -125,7 +124,7 @@ public class DoorbirdEvent {
      * The following functions support the decryption of the doorbell event
      * using the LazySodium wrapper for the libsodium crypto library
      */
-    public void decrypt(DatagramPacket p, String password) {
+    public void decrypt(DatagramPacket p, String password, @Nullable String v2DecryptionKey) {
         isDoorbellEvent = false;
 
         int length = p.getLength();
@@ -158,6 +157,9 @@ public class DoorbirdEvent {
             if (version == 1) {
                 // Decrypt using version 1 decryption scheme
                 decryptV1(bb, passwordFirstFive);
+            } else if (version == 2) {
+                // Decrypt using version 2 decryption scheme
+                decryptV2(bb, v2DecryptionKey);
             } else {
                 logger.info("Don't know how to decrypt version {} doorbell event", version);
             }
@@ -181,13 +183,16 @@ public class DoorbirdEvent {
     private void decryptV1(ByteBuffer bb, String password5) throws IndexOutOfBoundsException, BufferUnderflowException {
         LazySodiumJava sodium = getLazySodiumJavaInstance();
         if (sodium == null) {
-            logger.debug("Unable to decrypt event because libsodium is not loaded");
+            logger.debug("Unable to decrypt v1 event because libsodium is not loaded");
             return;
         }
         if (bb.capacity() != 70) {
             logger.info("Received malformed version 1 doorbell event, length not 70 bytes");
             return;
         }
+
+        logger.debug("Decrypting v1 event, size of buffer: {}", bb.capacity());
+
         // opslimit and memlimit are 4 bytes each
         opslimit = bb.getInt();
         memlimit = bb.getInt();
@@ -233,11 +238,62 @@ public class DoorbirdEvent {
             logger.trace("Decryption FAILED");
             return;
         }
+        getFieldsFromDecryptedText(m, mLen);
+    }
+
+    private void decryptV2(ByteBuffer bb, @Nullable String v2DecryptionKey)
+            throws IndexOutOfBoundsException, BufferUnderflowException {
+        LazySodiumJava sodium = getLazySodiumJavaInstance();
+        if (sodium == null) {
+            logger.debug("Unable to decrypt v2 event because libsodium is not loaded");
+            return;
+        }
+
+        if (v2DecryptionKey == null) {
+            logger.debug("Unable to decrypt v2 event because decryption key is null");
+            return;
+        }
+
+        logger.debug("Decrypting v2 event, size of buffer: {}", bb.capacity());
+
+        // Get nonce and ciphertext arrays
+        bb.get(nonce, 0, nonce.length);
+        bb.get(ciphertext, 0, ciphertext.length);
+
+        // Set up the variables for the decryption algorithm
+        byte[] m = new byte[30];
+        long[] mLen = new long[30];
+        byte[] nSec = null;
+        byte[] c = ciphertext;
+        long cLen = ciphertext.length;
+        byte[] ad = null;
+        long adLen = 0;
+        byte[] nPub = nonce;
+        byte[] k = v2DecryptionKey.getBytes();
+
+        // Decrypt the ciphertext
+        logger.trace("Call cryptoAeadChaCha20Poly1305Decrypt with ciphertext='{}', nonce='{}', key='{}'",
+                HexUtils.bytesToHex(ciphertext, " "), HexUtils.bytesToHex(nonce, " "), HexUtils.bytesToHex(k, " "));
+        boolean success = sodium.cryptoAeadChaCha20Poly1305Decrypt(m, mLen, nSec, c, cLen, ad, adLen, nPub, k);
+        if (!success) {
+            /*
+             * Don't log at debug level since the decryption will fail for events encrypted with
+             * passwords other than the password contained in the thing configuration (reference API
+             * documentation for details)
+             */
+            logger.trace("Decryption FAILED");
+            return;
+        }
+        getFieldsFromDecryptedText(m, mLen);
+    }
+
+    private void getFieldsFromDecryptedText(byte[] m, long[] mLen) {
         int decryptedTextLength = (int) mLen[0];
         if (decryptedTextLength != 18L) {
             logger.info("Length of decrypted text is invalid, must be 18 bytes");
             return;
         }
+
         // Get event fields from decrypted text
         logger.debug("Received and successfully decrypted a Doorbird event!!");
         ByteBuffer b = ByteBuffer.allocate(decryptedTextLength);
index dcc918d6208834dfe7e3f2d579ba8357ceecd979..5fb5a9266d6f09d9ebeb979ac3f34512c3b9631b 100644 (file)
@@ -22,6 +22,7 @@ import java.util.Arrays;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.doorbird.internal.api.DoorbirdSession;
 import org.openhab.binding.doorbird.internal.handler.DoorbellHandler;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -113,6 +114,8 @@ public class DoorbirdUdpListener extends Thread {
             return;
         }
 
+        DoorbirdSession session = thingHandler.getSession();
+        String v2DecryptionKey = (session != null ? session.getDecryptionKey() : null);
         String userId = thingHandler.getUserId();
         String userPassword = thingHandler.getUserPassword();
         if (userId == null || userPassword == null) {
@@ -120,7 +123,7 @@ public class DoorbirdUdpListener extends Thread {
             return;
         }
         try {
-            event.decrypt(packet, userPassword);
+            event.decrypt(packet, userPassword, v2DecryptionKey);
         } catch (RuntimeException e) {
             // The libsodium library might generate a runtime exception if the packet is malformed
             logger.info("DoorbirdEvent got unhandled exception: {}", e.getMessage(), e);
diff --git a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/model/GetsessionDTO.java b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/model/GetsessionDTO.java
new file mode 100644 (file)
index 0000000..85b32e9
--- /dev/null
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2010-2024 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.doorbird.internal.model;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * The {@link GetsessionDTO} models the JSON response returned by the Doorbird in response
+ * to calling the getsession.cgi API.
+ *
+ * @author Mark Hilbush - Initial contribution
+ */
+public class GetsessionDTO {
+    /**
+     * Top level container of information about the Doorbird session
+     */
+    @SerializedName("BHA")
+    public GetsessionBha bha;
+
+    public class GetsessionBha {
+        /**
+         * Return code from the Doorbird
+         */
+        @SerializedName("RETURNCODE")
+        public String returnCode;
+
+        /**
+         * Contains information about the Doorbird session
+         */
+        @SerializedName("SESSIONID")
+        public String sessionId;
+
+        /**
+         * Contains the v2 decryption key for events
+         */
+        @SerializedName("NOTIFICATION_ENCRYPTION_KEY")
+        public String decryptionKey;
+    }
+}