[gree] Use GCM encryption when bind fails (#17398)

* [gree]: use GCM encryption when binding fails

Signed-off-by: Zhivka Dimova <zhivka.dimova@myforest.net>
This commit is contained in:
Zhivka Dimova 2024-10-07 20:22:14 +02:00 committed by GitHub
parent e6b372c053
commit 90442a3864
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 112 additions and 33 deletions

View File

@ -19,15 +19,18 @@ No binding configuration is required.
## Thing Configuration ## Thing Configuration
| Channel Name | Type | Description | | Channel Name | Type | Description |
|--------------------------|------------|-----------------------------------------------------------------------------------------------| |--------------------------|-----------------|-----------------------------------------------------------------------------------------------|
| ipAddress | IP Address | IP address of the unit. | | ipAddress | IP Address | IP address of the unit. |
| broadcastAddress | IP Address | Broadcast address being used for discovery, usually derived from the IP interface address. | | 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. | | 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. | | 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. 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). 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. Only change this if you have a good reason to.
## Channels ## Channels
@ -64,7 +67,7 @@ When changing mode, the air conditioner will be turned on unless "off" is select
### Things ### Things
```java ```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 ### Items

View File

@ -12,7 +12,11 @@
*/ */
package org.openhab.binding.gree.internal; package org.openhab.binding.gree.internal;
import java.util.Map;
import java.util.Set; 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.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID; 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_IP = "ipAddress";
public static final String PROPERTY_BROADCAST = "broadcastAddress"; public static final String PROPERTY_BROADCAST = "broadcastAddress";
public static final String PROPERTY_ENCRYPTION_TYPE = "encryptionType";
// List of all Channel ids // List of all Channel ids
public static final String POWER_CHANNEL = "power"; public static final String POWER_CHANNEL = "power";
public static final String MODE_CHANNEL = "mode"; public static final String MODE_CHANNEL = "mode";
@ -174,4 +180,17 @@ public class GreeBindingConstants {
* for more details. * for more details.
*/ */
public static final double INTERNAL_TEMP_SENSOR_OFFSET = -40.0; public static final double INTERNAL_TEMP_SENSOR_OFFSET = -40.0;
public enum EncryptionTypes {
UNKNOWN,
ECB,
GCM;
private static final Map<String, EncryptionTypes> MAP = Stream.of(EncryptionTypes.values())
.collect(Collectors.toMap(Enum::name, Function.identity()));
public static EncryptionTypes of(final String name) {
return MAP.getOrDefault(name, UNKNOWN);
}
};
} }

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.gree.internal; package org.openhab.binding.gree.internal;
import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -32,10 +34,11 @@ public class GreeConfiguration {
* of the temperature sensor. * of the temperature sensor.
*/ */
public BigDecimal currentTemperatureOffset = new BigDecimal(0.0); public BigDecimal currentTemperatureOffset = new BigDecimal(0.0);
public EncryptionTypes encryptionType = EncryptionTypes.UNKNOWN;
@Override @Override
public String toString() { public String toString() {
return "Config: ipAddress=" + ipAddress + ", broadcastAddress=" + broadcastAddress + ", refresh=" + refresh return "Config: ipAddress=" + ipAddress + ", broadcastAddress=" + broadcastAddress + ", refresh=" + refresh
+ ", currentTemperatureOffset=" + currentTemperatureOffset; + ", currentTemperatureOffset=" + currentTemperatureOffset + ", encryptionType=" + encryptionType;
} }
} }

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.gree.internal; package org.openhab.binding.gree.internal;
import static org.openhab.binding.gree.internal.GreeBindingConstants.EncryptionTypes;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
@ -45,11 +47,6 @@ public class GreeCryptoUtil {
private static final String GCM_ADD = "qualcomm-test"; private static final String GCM_ADD = "qualcomm-test";
private static final int TAG_LENGTH = 16; private static final int TAG_LENGTH = 16;
public enum EncryptionTypes {
ECB,
GCM
};
public static byte[] getAESGeneralKeyByteArray() { public static byte[] getAESGeneralKeyByteArray() {
return AES_KEY.getBytes(StandardCharsets.UTF_8); return AES_KEY.getBytes(StandardCharsets.UTF_8);
} }
@ -86,6 +83,10 @@ public class GreeCryptoUtil {
} }
public static <T extends GreeBaseDTO> String decrypt(T response, EncryptionTypes encType) throws GreeException { public static <T extends GreeBaseDTO> String decrypt(T response, EncryptionTypes encType) throws GreeException {
if (encType == EncryptionTypes.UNKNOWN) {
encType = getEncryptionType(response);
}
if (encType == EncryptionTypes.GCM) { if (encType == EncryptionTypes.GCM) {
return decrypt(getGCMGeneralKeyByteArray(), response, encType); return decrypt(getGCMGeneralKeyByteArray(), response, encType);
} else { } else {
@ -95,6 +96,10 @@ public class GreeCryptoUtil {
public static <T extends GreeBaseDTO> String decrypt(byte[] keyarray, T response, EncryptionTypes encType) public static <T extends GreeBaseDTO> String decrypt(byte[] keyarray, T response, EncryptionTypes encType)
throws GreeException { throws GreeException {
if (encType == EncryptionTypes.UNKNOWN) {
encType = getEncryptionType(response);
}
if (encType == EncryptionTypes.GCM) { if (encType == EncryptionTypes.GCM) {
return decryptGCMPack(keyarray, response.pack, response.tag); return decryptGCMPack(keyarray, response.pack, response.tag);
} else { } else {

View File

@ -106,7 +106,7 @@ public class GreeException extends Exception {
private Class<?> getCauseClass() { private Class<?> getCauseClass() {
Throwable cause = getCause(); Throwable cause = getCause();
if (getCause() != null) { if (cause != null) {
return cause.getClass(); return cause.getClass();
} }
return GreeException.class; return GreeException.class;

View File

@ -61,7 +61,8 @@ public class GreeDeviceFinder {
public 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; InetAddress ipAddress;
try { try {
ipAddress = InetAddress.getByName(broadcastAddress); ipAddress = InetAddress.getByName(broadcastAddress);
@ -103,7 +104,8 @@ public class GreeDeviceFinder {
} }
// Decrypt message - a GreeException is thrown when something went wrong // 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); logger.debug("Response received from address {}: {}", remoteAddress.getHostAddress(), decryptedMsg);

View File

@ -87,7 +87,7 @@ public class GreeDiscoveryService extends AbstractDiscoveryService {
@Override @Override
protected void startScan() { protected void startScan() {
try (DatagramSocket clientSocket = new DatagramSocket()) { try (DatagramSocket clientSocket = new DatagramSocket()) {
deviceFinder.scan(clientSocket, broadcastAddress, true); deviceFinder.scan(clientSocket, broadcastAddress, true, EncryptionTypes.UNKNOWN);
int count = deviceFinder.getScannedDeviceCount(); int count = deviceFinder.getScannedDeviceCount();
logger.debug("{}", messages.get("discovery.result", count)); 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(Thing.PROPERTY_MAC_ADDRESS, device.getId());
properties.put(PROPERTY_IP, ipAddress); properties.put(PROPERTY_IP, ipAddress);
properties.put(PROPERTY_BROADCAST, broadcastAddress); properties.put(PROPERTY_BROADCAST, broadcastAddress);
properties.put(PROPERTY_ENCRYPTION_TYPE, device.getEncryptionType());
ThingUID thingUID = new ThingUID(THING_TYPE_GREEAIRCON, device.getId()); ThingUID thingUID = new ThingUID(THING_TYPE_GREEAIRCON, device.getId());
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties) DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).withLabel(device.getName()).build(); .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).withLabel(device.getName()).build();

View File

@ -64,7 +64,7 @@ public class GreeAirDevice {
private final InetAddress ipAddress; private final InetAddress ipAddress;
private int port = 0; private int port = 0;
private String encKey = ""; private String encKey = "";
private GreeCryptoUtil.EncryptionTypes encType = GreeCryptoUtil.EncryptionTypes.ECB; private EncryptionTypes encType = EncryptionTypes.UNKNOWN;
private Optional<GreeScanResponseDTO> scanResponseGson = Optional.empty(); private Optional<GreeScanResponseDTO> scanResponseGson = Optional.empty();
private Optional<GreeStatusResponseDTO> statusResponseGson = Optional.empty(); private Optional<GreeStatusResponseDTO> statusResponseGson = Optional.empty();
private Optional<GreeStatusResponsePackDTO> prevStatusResponsePackGson = Optional.empty(); private Optional<GreeStatusResponsePackDTO> 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 { try {
// Prep the Binding Request pack // Prep the Binding Request pack
GreeBindRequestPackDTO bindReqPackGson = new GreeBindRequestPackDTO(); GreeBindRequestPackDTO bindReqPackGson = new GreeBindRequestPackDTO();
@ -158,6 +158,7 @@ public class GreeAirDevice {
String bindReqPackStr = GSON.toJson(bindReqPackGson); String bindReqPackStr = GSON.toJson(bindReqPackGson);
// Encrypt and send the Binding Request pack // Encrypt and send the Binding Request pack
setEncryptionType(encryptionTypeConfig);
String[] encryptedBindReqData = GreeCryptoUtil.encrypt(GreeCryptoUtil.getGeneralKeyByteArray(encType), String[] encryptedBindReqData = GreeCryptoUtil.encrypt(GreeCryptoUtil.getGeneralKeyByteArray(encType),
bindReqPackStr, encType); bindReqPackStr, encType);
DatagramPacket sendPacket = createPackRequest(1, encryptedBindReqData); DatagramPacket sendPacket = createPackRequest(1, encryptedBindReqData);
@ -174,7 +175,12 @@ public class GreeAirDevice {
// save the outcome // save the outcome
isBound = true; isBound = true;
} catch (IOException | JsonSyntaxException e) { } 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.uid = 0;
request.tcid = getId(); request.tcid = getId();
request.pack = data[0]; request.pack = data[0];
if (encType == GreeCryptoUtil.EncryptionTypes.GCM) { if (encType == EncryptionTypes.GCM) {
if (data.length > 1) { if (data.length > 1) {
request.tag = data[1]; request.tag = data[1];
} else { } else {
@ -519,6 +525,24 @@ public class GreeAirDevice {
return isBound; 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() { public byte[] getKey() {
return encKey.getBytes(StandardCharsets.UTF_8); return encKey.getBytes(StandardCharsets.UTF_8);
} }
@ -528,7 +552,12 @@ public class GreeAirDevice {
} }
public String getName() { 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() { public String getVendor() {

View File

@ -108,12 +108,12 @@ public class GreeHandler extends BaseThingHandler {
clientSocket.get().setSoTimeout(DATAGRAM_SOCKET_TIMEOUT); clientSocket.get().setSoTimeout(DATAGRAM_SOCKET_TIMEOUT);
} }
// Find the GREE device // 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); GreeAirDevice newDevice = deviceFinder.getDeviceByIPAddress(config.ipAddress);
if (newDevice != null) { if (newDevice != null) {
// Ok, our device responded, now let's Bind with it // Ok, our device responded, now let's Bind with it
device = newDevice; device = newDevice;
device.bindWithDevice(clientSocket.get()); device.bindWithDevice(clientSocket.get(), config.encryptionType);
if (device.getIsBound()) { if (device.getIsBound()) {
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
return; return;
@ -138,7 +138,7 @@ public class GreeHandler extends BaseThingHandler {
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
// The thing is updated by the scheduled automatic refresh so do nothing here. initializeThing();
} else { } else {
logger.debug("{}: Issue command {} to channe {}", thingId, command, channelUID.getIdWithoutGroup()); logger.debug("{}: Issue command {} to channe {}", thingId, command, channelUID.getIdWithoutGroup());
String channelId = channelUID.getIdWithoutGroup(); String channelId = channelUID.getIdWithoutGroup();
@ -377,8 +377,9 @@ public class GreeHandler extends BaseThingHandler {
} }
} catch (GreeException e) { } catch (GreeException e) {
String subcode = ""; String subcode = "";
if (e.getCause() != null) { Throwable cause = e.getCause();
subcode = " (" + e.getCause().getMessage() + ")"; if (cause != null) {
subcode = " (" + cause.getMessage() + ")";
} }
String message = messages.get("update.exception", e.getMessageString() + subcode); String message = messages.get("update.exception", e.getMessageString() + subcode);
if (getThing().getStatus() == ThingStatus.OFFLINE) { if (getThing().getStatus() == ThingStatus.OFFLINE) {

View File

@ -1,12 +1,15 @@
# GREE Binding # add-on
addon.gree.name = GREE Binding 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 types
thing-type.gree.airconditioner.label = Air Conditioner thing-type.gree.airconditioner.label = Air Conditioner
thing-type.gree.airconditioner.description = A GREE Air Conditioner with WiFi Module thing-type.gree.airconditioner.description = A GREE Air Conditioner with WiFi Module
# thing type config description # thing type config description
thing-type.config.gree.airconditioner.ipAddress.label = IP Address 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.ipAddress.description = IP Address of the GREE unit.
thing-type.config.gree.airconditioner.broadcastAddress.label = Subnet Broadcast Address 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.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.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.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 types
channel-type.gree.power.label = Power channel-type.gree.power.label = Power
channel-type.gree.power.description = Turn power on/off channel-type.gree.power.description = Turn power on/off
channel-type.gree.mode.label = Unit Mode 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.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.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.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.2 = Left
channel-type.gree.swingleftright.option.3 = Mid-Left channel-type.gree.swingleftright.option.3 = Mid-Left
channel-type.gree.swingleftright.option.4 = Mid 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.label = Health Mode
channel-type.gree.health.description = Set on/off the Air Conditioner's Health function if applicable to the Air Conditioner model. 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.failed = Unable to connect to air conditioner
message.thinginit.invconf = Invalid configuration data message.thinginit.invconf = Invalid configuration data
message.thinginit.exception = Thing initialization failed: {0} 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.update.exception = Unable to perform auto-update: {0}
message.channel.exception = Unable to update channel {0} with {1} message.channel.exception = Unable to update channel {0} with {1}
message.discovery.result = {0} units discovered. message.discovery.result = {0} units discovered.
message.discovery.newunit = Device {0} discovered at {1}, MAC={2} message.discovery.newunit = Device {0} discovered at {1}, MAC={2}
message.discovery.exception = Device Discovery failed: {0} message.discovery.exception = Device Discovery failed: {0}

View File

@ -41,6 +41,13 @@
<unitLabel>Degrees Celsius</unitLabel> <unitLabel>Degrees Celsius</unitLabel>
<advanced>true</advanced> <advanced>true</advanced>
</parameter> </parameter>
<parameter name="encryptionType" type="text">
<options>
<option value="ECB">@text/thing-type.config.gree.airconditioner.encryptionType.state.option.ECB</option>
<option value="GCM">@text/thing-type.config.gree.airconditioner.encryptionType.state.option.GCM</option>
</options>
<advanced>true</advanced>
</parameter>
</config-description> </config-description>
</thing-type> </thing-type>