mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[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:
parent
555664693c
commit
babb5ba9d4
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user