From 41743e882a7ad9e14962f6ac924fd1df818f7e5f Mon Sep 17 00:00:00 2001 From: Mark Hilbush Date: Wed, 17 Jan 2024 15:07:18 -0500 Subject: [PATCH] [doorbird] Add support for version 2 encryption scheme (#16297) * Add support for version 2 encryption scheme Signed-off-by: Mark Hilbush --- .../doorbird/internal/api/DoorbirdAPI.java | 31 +++++++--- .../internal/api/DoorbirdSession.java | 55 ++++++++++++++++ .../internal/handler/DoorbellHandler.java | 9 +++ .../internal/listener/DoorbirdEvent.java | 62 ++++++++++++++++++- .../listener/DoorbirdUdpListener.java | 5 +- .../internal/model/GetsessionDTO.java | 49 +++++++++++++++ 6 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdSession.java create mode 100644 bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/model/GetsessionDTO.java diff --git a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdAPI.java b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdAPI.java index 6f2e2eddef..81cf117eba 100644 --- a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdAPI.java +++ b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdAPI.java @@ -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 index 0000000000..452d980581 --- /dev/null +++ b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/api/DoorbirdSession.java @@ -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; + } +} diff --git a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/handler/DoorbellHandler.java b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/handler/DoorbellHandler.java index 0cb0e0955f..71a0fd2310 100644 --- a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/handler/DoorbellHandler.java +++ b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/handler/DoorbellHandler.java @@ -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 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()); diff --git a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdEvent.java b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdEvent.java index a14e0e4b3c..9479b05f97 100644 --- a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdEvent.java +++ b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdEvent.java @@ -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); diff --git a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdUdpListener.java b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdUdpListener.java index dcc918d620..5fb5a9266d 100644 --- a/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdUdpListener.java +++ b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/listener/DoorbirdUdpListener.java @@ -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 index 0000000000..85b32e9086 --- /dev/null +++ b/bundles/org.openhab.binding.doorbird/src/main/java/org/openhab/binding/doorbird/internal/model/GetsessionDTO.java @@ -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; + } +} -- 2.47.3