From 90442a3864d80cd609a80ccf57e778db86b7322d Mon Sep 17 00:00:00 2001 From: Zhivka Dimova Date: Mon, 7 Oct 2024 20:22:14 +0200 Subject: [PATCH] [gree] Use GCM encryption when bind fails (#17398) * [gree]: use GCM encryption when binding fails Signed-off-by: Zhivka Dimova --- bundles/org.openhab.binding.gree/README.md | 17 ++++---- .../gree/internal/GreeBindingConstants.java | 19 +++++++++ .../gree/internal/GreeConfiguration.java | 5 ++- .../binding/gree/internal/GreeCryptoUtil.java | 15 ++++--- .../binding/gree/internal/GreeException.java | 2 +- .../internal/discovery/GreeDeviceFinder.java | 6 ++- .../discovery/GreeDiscoveryService.java | 3 +- .../gree/internal/handler/GreeAirDevice.java | 39 ++++++++++++++++--- .../gree/internal/handler/GreeHandler.java | 11 +++--- .../resources/OH-INF/i18n/gree.properties | 21 +++++++--- .../resources/OH-INF/thing/thing-types.xml | 7 ++++ 11 files changed, 112 insertions(+), 33 deletions(-) diff --git a/bundles/org.openhab.binding.gree/README.md b/bundles/org.openhab.binding.gree/README.md index aa000c7365b..58b120334dd 100644 --- a/bundles/org.openhab.binding.gree/README.md +++ b/bundles/org.openhab.binding.gree/README.md @@ -19,15 +19,18 @@ No binding configuration is required. ## Thing Configuration -| Channel Name | Type | Description | -|--------------------------|------------|-----------------------------------------------------------------------------------------------| -| ipAddress | IP Address | IP address of the unit. | -| broadcastAddress | IP Address | Broadcast address being used for discovery, usually derived from the IP interface address. | -| refresh | Integer | Refresh interval in seconds for polling the device status. | -| currentTemperatureOffset | Decimal | Offset in Celsius for the current temperature value received from the device. | +| Channel Name | Type | Description | +|--------------------------|-----------------|-----------------------------------------------------------------------------------------------| +| ipAddress | IP Address | IP address of the unit. | +| broadcastAddress | IP Address | Broadcast address being used for discovery, usually derived from the IP interface address. | +| refresh | Integer | Refresh interval in seconds for polling the device status. | +| currentTemperatureOffset | Decimal | Offset in Celsius for the current temperature value received from the device. | +| encryptionType | EncryptionTypes | Encryption type (ECB or GCM) used for communicating with the AC device | The Air Conditioner's IP address is mandatory, all other parameters are optional. If the broadcast is not set (default) it will be derived from openHAB's network setting (Check Network Settings in the openHAB UI). +The binding tries to automatically detect the encryption type when communicating with the AC. +If this fails, you might need need to set the encryption type manually. Only change this if you have a good reason to. ## Channels @@ -64,7 +67,7 @@ When changing mode, the air conditioner will be turned on unless "off" is select ### Things ```java -Thing gree:airconditioner:a1234561 [ ipAddress="192.168.1.111", refresh=2 ] +Thing gree:airconditioner:a1234561 [ ipAddress="192.168.1.111", refresh=2, encryptionType="ECB" ] ``` ### Items diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java index 94e47edee08..7538abd4e1a 100644 --- a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java @@ -12,7 +12,11 @@ */ package org.openhab.binding.gree.internal; +import java.util.Map; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.thing.ThingTypeUID; @@ -39,6 +43,8 @@ public class GreeBindingConstants { public static final String PROPERTY_IP = "ipAddress"; public static final String PROPERTY_BROADCAST = "broadcastAddress"; + public static final String PROPERTY_ENCRYPTION_TYPE = "encryptionType"; + // List of all Channel ids public static final String POWER_CHANNEL = "power"; public static final String MODE_CHANNEL = "mode"; @@ -174,4 +180,17 @@ public class GreeBindingConstants { * for more details. */ public static final double INTERNAL_TEMP_SENSOR_OFFSET = -40.0; + + public enum EncryptionTypes { + UNKNOWN, + ECB, + GCM; + + private static final Map MAP = Stream.of(EncryptionTypes.values()) + .collect(Collectors.toMap(Enum::name, Function.identity())); + + public static EncryptionTypes of(final String name) { + return MAP.getOrDefault(name, UNKNOWN); + } + }; } diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java index 1e8f397aae8..05afdd15895 100644 --- a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.gree.internal; +import static org.openhab.binding.gree.internal.GreeBindingConstants.*; + import java.math.BigDecimal; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -32,10 +34,11 @@ public class GreeConfiguration { * of the temperature sensor. */ public BigDecimal currentTemperatureOffset = new BigDecimal(0.0); + public EncryptionTypes encryptionType = EncryptionTypes.UNKNOWN; @Override public String toString() { return "Config: ipAddress=" + ipAddress + ", broadcastAddress=" + broadcastAddress + ", refresh=" + refresh - + ", currentTemperatureOffset=" + currentTemperatureOffset; + + ", currentTemperatureOffset=" + currentTemperatureOffset + ", encryptionType=" + encryptionType; } } diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java index f51678788bc..aebc2fa5fca 100644 --- a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.gree.internal; +import static org.openhab.binding.gree.internal.GreeBindingConstants.EncryptionTypes; + import java.nio.charset.StandardCharsets; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; @@ -45,11 +47,6 @@ public class GreeCryptoUtil { private static final String GCM_ADD = "qualcomm-test"; private static final int TAG_LENGTH = 16; - public enum EncryptionTypes { - ECB, - GCM - }; - public static byte[] getAESGeneralKeyByteArray() { return AES_KEY.getBytes(StandardCharsets.UTF_8); } @@ -86,6 +83,10 @@ public class GreeCryptoUtil { } public static String decrypt(T response, EncryptionTypes encType) throws GreeException { + if (encType == EncryptionTypes.UNKNOWN) { + encType = getEncryptionType(response); + } + if (encType == EncryptionTypes.GCM) { return decrypt(getGCMGeneralKeyByteArray(), response, encType); } else { @@ -95,6 +96,10 @@ public class GreeCryptoUtil { public static String decrypt(byte[] keyarray, T response, EncryptionTypes encType) throws GreeException { + if (encType == EncryptionTypes.UNKNOWN) { + encType = getEncryptionType(response); + } + if (encType == EncryptionTypes.GCM) { return decryptGCMPack(keyarray, response.pack, response.tag); } else { diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java index cd824dc735e..94441be8aa0 100644 --- a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java @@ -106,7 +106,7 @@ public class GreeException extends Exception { private Class getCauseClass() { Throwable cause = getCause(); - if (getCause() != null) { + if (cause != null) { return cause.getClass(); } return GreeException.class; diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java index bc277d7034e..a0b50f72a9a 100644 --- a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java @@ -61,7 +61,8 @@ public class GreeDeviceFinder { public GreeDeviceFinder() { } - public void scan(DatagramSocket clientSocket, String broadcastAddress, boolean scanNetwork) throws GreeException { + public void scan(DatagramSocket clientSocket, String broadcastAddress, boolean scanNetwork, + EncryptionTypes encryptionTypeConfig) throws GreeException { InetAddress ipAddress; try { ipAddress = InetAddress.getByName(broadcastAddress); @@ -103,7 +104,8 @@ public class GreeDeviceFinder { } // Decrypt message - a GreeException is thrown when something went wrong - String decryptedMsg = scanResponseGson.decryptedPack = GreeCryptoUtil.decrypt(scanResponseGson); + String decryptedMsg = scanResponseGson.decryptedPack = GreeCryptoUtil.decrypt(scanResponseGson, + encryptionTypeConfig); logger.debug("Response received from address {}: {}", remoteAddress.getHostAddress(), decryptedMsg); diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java index 7551154d938..eff9b2d360b 100644 --- a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java @@ -87,7 +87,7 @@ public class GreeDiscoveryService extends AbstractDiscoveryService { @Override protected void startScan() { try (DatagramSocket clientSocket = new DatagramSocket()) { - deviceFinder.scan(clientSocket, broadcastAddress, true); + deviceFinder.scan(clientSocket, broadcastAddress, true, EncryptionTypes.UNKNOWN); int count = deviceFinder.getScannedDeviceCount(); logger.debug("{}", messages.get("discovery.result", count)); @@ -112,6 +112,7 @@ public class GreeDiscoveryService extends AbstractDiscoveryService { properties.put(Thing.PROPERTY_MAC_ADDRESS, device.getId()); properties.put(PROPERTY_IP, ipAddress); properties.put(PROPERTY_BROADCAST, broadcastAddress); + properties.put(PROPERTY_ENCRYPTION_TYPE, device.getEncryptionType()); ThingUID thingUID = new ThingUID(THING_TYPE_GREEAIRCON, device.getId()); DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties) .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).withLabel(device.getName()).build(); diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java index 2a7db2a5c59..950b682b5c8 100644 --- a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java @@ -64,7 +64,7 @@ public class GreeAirDevice { private final InetAddress ipAddress; private int port = 0; private String encKey = ""; - private GreeCryptoUtil.EncryptionTypes encType = GreeCryptoUtil.EncryptionTypes.ECB; + private EncryptionTypes encType = EncryptionTypes.UNKNOWN; private Optional scanResponseGson = Optional.empty(); private Optional statusResponseGson = Optional.empty(); private Optional prevStatusResponsePackGson = Optional.empty(); @@ -148,7 +148,7 @@ public class GreeAirDevice { } } - public void bindWithDevice(DatagramSocket clientSocket) throws GreeException { + public void bindWithDevice(DatagramSocket clientSocket, EncryptionTypes encryptionTypeConfig) throws GreeException { try { // Prep the Binding Request pack GreeBindRequestPackDTO bindReqPackGson = new GreeBindRequestPackDTO(); @@ -158,6 +158,7 @@ public class GreeAirDevice { String bindReqPackStr = GSON.toJson(bindReqPackGson); // Encrypt and send the Binding Request pack + setEncryptionType(encryptionTypeConfig); String[] encryptedBindReqData = GreeCryptoUtil.encrypt(GreeCryptoUtil.getGeneralKeyByteArray(encType), bindReqPackStr, encType); DatagramPacket sendPacket = createPackRequest(1, encryptedBindReqData); @@ -174,7 +175,12 @@ public class GreeAirDevice { // save the outcome isBound = true; } catch (IOException | JsonSyntaxException e) { - throw new GreeException("Unable to bind to device", e); + if (encType != EncryptionTypes.GCM) { + logger.debug("Unable to bind to device - changing the encryption mode to GCM and trying again", e); + bindWithDevice(clientSocket, EncryptionTypes.GCM); + } else { + throw new GreeException("Unable to bind to device", e); + } } } @@ -460,7 +466,7 @@ public class GreeAirDevice { request.uid = 0; request.tcid = getId(); request.pack = data[0]; - if (encType == GreeCryptoUtil.EncryptionTypes.GCM) { + if (encType == EncryptionTypes.GCM) { if (data.length > 1) { request.tag = data[1]; } else { @@ -519,6 +525,24 @@ public class GreeAirDevice { return isBound; } + public void setEncryptionType(EncryptionTypes value) { + if (value == EncryptionTypes.UNKNOWN) { + logger.debug("Trying to set encryption type to 'UNKNOWN' for device: {}, current value: {}", getName(), + encType); + if (encType == EncryptionTypes.UNKNOWN) { + logger.debug("Falling back to 'ECB' for device: {}", getName()); + encType = EncryptionTypes.ECB; + } + } else { + logger.debug("Change encryption type for device: {}, from : {}, to: {}", getName(), encType, value); + encType = value; + } + } + + public EncryptionTypes getEncryptionType() { + return encType; + } + public byte[] getKey() { return encKey.getBytes(StandardCharsets.UTF_8); } @@ -528,7 +552,12 @@ public class GreeAirDevice { } public String getName() { - return scanResponseGson.isPresent() ? scanResponseGson.get().packJson.name : ""; + if (scanResponseGson.isPresent()) { + String name = scanResponseGson.get().packJson.name; + return name.trim().isEmpty() ? getId() : name; + } + + return ""; } public String getVendor() { diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java index 9b0203b9b88..b0428fa775e 100644 --- a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java +++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java @@ -108,12 +108,12 @@ public class GreeHandler extends BaseThingHandler { clientSocket.get().setSoTimeout(DATAGRAM_SOCKET_TIMEOUT); } // Find the GREE device - deviceFinder.scan(clientSocket.get(), config.ipAddress, false); + deviceFinder.scan(clientSocket.get(), config.ipAddress, false, config.encryptionType); GreeAirDevice newDevice = deviceFinder.getDeviceByIPAddress(config.ipAddress); if (newDevice != null) { // Ok, our device responded, now let's Bind with it device = newDevice; - device.bindWithDevice(clientSocket.get()); + device.bindWithDevice(clientSocket.get(), config.encryptionType); if (device.getIsBound()) { updateStatus(ThingStatus.ONLINE); return; @@ -138,7 +138,7 @@ public class GreeHandler extends BaseThingHandler { @Override public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { - // The thing is updated by the scheduled automatic refresh so do nothing here. + initializeThing(); } else { logger.debug("{}: Issue command {} to channe {}", thingId, command, channelUID.getIdWithoutGroup()); String channelId = channelUID.getIdWithoutGroup(); @@ -377,8 +377,9 @@ public class GreeHandler extends BaseThingHandler { } } catch (GreeException e) { String subcode = ""; - if (e.getCause() != null) { - subcode = " (" + e.getCause().getMessage() + ")"; + Throwable cause = e.getCause(); + if (cause != null) { + subcode = " (" + cause.getMessage() + ")"; } String message = messages.get("update.exception", e.getMessageString() + subcode); if (getThing().getStatus() == ThingStatus.OFFLINE) { diff --git a/bundles/org.openhab.binding.gree/src/main/resources/OH-INF/i18n/gree.properties b/bundles/org.openhab.binding.gree/src/main/resources/OH-INF/i18n/gree.properties index cdc2bcab1e9..ccae40ffa34 100644 --- a/bundles/org.openhab.binding.gree/src/main/resources/OH-INF/i18n/gree.properties +++ b/bundles/org.openhab.binding.gree/src/main/resources/OH-INF/i18n/gree.properties @@ -1,12 +1,15 @@ -# GREE Binding +# add-on + addon.gree.name = GREE Binding -addon.gree.description = This binding integrates the GREE series of air conditioners +addon.gree.description = This is the binding for GREE air conditioners. # thing types + thing-type.gree.airconditioner.label = Air Conditioner thing-type.gree.airconditioner.description = A GREE Air Conditioner with WiFi Module # thing type config description + thing-type.config.gree.airconditioner.ipAddress.label = IP Address thing-type.config.gree.airconditioner.ipAddress.description = IP Address of the GREE unit. thing-type.config.gree.airconditioner.broadcastAddress.label = Subnet Broadcast Address @@ -15,8 +18,13 @@ thing-type.config.gree.airconditioner.refresh.label = Refresh Interval thing-type.config.gree.airconditioner.refresh.description = Interval to query an update from the device. thing-type.config.gree.airconditioner.currentTemperatureOffset.label = Offset for Current Temperature thing-type.config.gree.airconditioner.currentTemperatureOffset.description = The offset in Celsius for the current temperature value received from the device. +thing-type.config.gree.airconditioner.encryptionType.label = Encryption type +thing-type.config.gree.airconditioner.encryptionType.description = The encryption type used for encrypting the data send to the AC device. +thing-type.config.gree.airconditioner.encryptionType.state.option.ECB = ECB +thing-type.config.gree.airconditioner.encryptionType.state.option.GCM = GCM # channel types + channel-type.gree.power.label = Power channel-type.gree.power.description = Turn power on/off channel-type.gree.mode.label = Unit Mode @@ -70,7 +78,7 @@ channel-type.gree.swingupdown.option.11 = Swing Upmost channel-type.gree.swingleftright.label = Horizontal Swing Mode channel-type.gree.swingleftright.description = Sets the horizontal swing action on the Air Conditioner: 0=OFF, 1=Full Swing, 2=Left, 3=Mid-Left, 4=Mid, 5=Mid-Right, 6=Right channel-type.gree.swingleftright.option.0 = OFF -channel-type.gree.swingleftright.option.1 = Full Swing +channel-type.gree.swingleftright.option.1 = Full Swing channel-type.gree.swingleftright.option.2 = Left channel-type.gree.swingleftright.option.3 = Mid-Left channel-type.gree.swingleftright.option.4 = Mid @@ -83,7 +91,8 @@ channel-type.gree.light.description = Enable/disable the front display on the Ai channel-type.gree.health.label = Health Mode channel-type.gree.health.description = Set on/off the Air Conditioner's Health function if applicable to the Air Conditioner model. -# User Messages +# user messages + message.thinginit.failed = Unable to connect to air conditioner message.thinginit.invconf = Invalid configuration data message.thinginit.exception = Thing initialization failed: {0} @@ -92,5 +101,5 @@ message.command.exception = Unable to execute command {0} for channel {1} message.update.exception = Unable to perform auto-update: {0} message.channel.exception = Unable to update channel {0} with {1} message.discovery.result = {0} units discovered. -message.discovery.newunit = Device {0} discovered at {1}, MAC={2} -message.discovery.exception = Device Discovery failed: {0} +message.discovery.newunit = Device {0} discovered at {1}, MAC={2} +message.discovery.exception = Device Discovery failed: {0} diff --git a/bundles/org.openhab.binding.gree/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.gree/src/main/resources/OH-INF/thing/thing-types.xml index 51cf508f7f4..8fc6b30be9b 100644 --- a/bundles/org.openhab.binding.gree/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.gree/src/main/resources/OH-INF/thing/thing-types.xml @@ -41,6 +41,13 @@ Degrees Celsius true + + + + + + true +