[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

@ -20,14 +20,17 @@ 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. |
| 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

View File

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

View File

@ -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 <T extends GreeBaseDTO> 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 <T extends GreeBaseDTO> 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 {

View File

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

View File

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

View File

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

View File

@ -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<GreeScanResponseDTO> scanResponseGson = Optional.empty();
private Optional<GreeStatusResponseDTO> statusResponseGson = 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 {
// 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,9 +175,14 @@ public class GreeAirDevice {
// save the outcome
isBound = true;
} catch (IOException | JsonSyntaxException 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);
}
}
}
public void setDevicePower(DatagramSocket clientSocket, int value) throws GreeException {
setCommandValue(clientSocket, GREE_PROP_POWER, value);
@ -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() {

View File

@ -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) {

View File

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

View File

@ -41,6 +41,13 @@
<unitLabel>Degrees Celsius</unitLabel>
<advanced>true</advanced>
</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>
</thing-type>