[doorbird] Add support for version 2 encryption scheme (#16297)

* Add support for version 2 encryption scheme

Signed-off-by: Mark Hilbush <mark@hilbush.com>
This commit is contained in:
Mark Hilbush 2024-01-17 15:07:18 -05:00 committed by Leo Siepel
parent 555664693c
commit babb5ba9d4
6 changed files with 199 additions and 12 deletions

View 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);
}
}

View File

@ -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;
}
}

View 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());

View 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);

View 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);

View File

@ -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;
}
}