[gree] Add support for ASC/GCM encryption (#16950)

* [gree] support for ASC/GCM encryption

Signed-off-by: Zhivka Dimova <zhivka.dimova@myforest.net>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Zhivka Dimova 2024-08-01 11:36:52 +02:00 committed by Ciprian Pascu
parent ed9afe8fbe
commit a6022133c4
19 changed files with 224 additions and 89 deletions

View File

@ -13,18 +13,22 @@
package org.openhab.binding.gree.internal;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HexFormat;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.gree.internal.gson.GreeBaseDTO;
/**
* The CryptoUtil class provides functionality for encrypting and decrypting
@ -36,11 +40,68 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
@NonNullByDefault
public class GreeCryptoUtil {
private static final String AES_KEY = "a3K8Bx%2r8Y7#xDh";
private static final String GCM_KEY = "{yxAHAY_Lm6pbC/<";
private static final String GCM_IV = "5440784449675a516c5e6313";
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);
}
public static byte[] getGCMGeneralKeyByteArray() {
return GCM_KEY.getBytes(StandardCharsets.UTF_8);
}
public static byte[] getGeneralKeyByteArray(EncryptionTypes encType) {
if (encType == EncryptionTypes.GCM) {
return getGCMGeneralKeyByteArray();
}
return getAESGeneralKeyByteArray();
}
public static byte[] getGCMIVByteArray() {
return HexFormat.of().parseHex(GCM_IV);
}
public static byte[] getGCMADDByteArray() {
return GCM_ADD.getBytes(StandardCharsets.UTF_8);
}
public static <T extends GreeBaseDTO> EncryptionTypes getEncryptionType(T response) {
return response.tag != null ? EncryptionTypes.GCM : EncryptionTypes.ECB;
}
public static <T extends GreeBaseDTO> String decrypt(T response) throws GreeException {
return decrypt(response, getEncryptionType(response));
}
public static <T extends GreeBaseDTO> String decrypt(byte[] keyarray, T response) throws GreeException {
return decrypt(keyarray, response, getEncryptionType(response));
}
public static <T extends GreeBaseDTO> String decrypt(T response, EncryptionTypes encType) throws GreeException {
if (encType == EncryptionTypes.GCM) {
return decrypt(getGCMGeneralKeyByteArray(), response, encType);
} else {
return decrypt(getAESGeneralKeyByteArray(), response, encType);
}
}
public static <T extends GreeBaseDTO> String decrypt(byte[] keyarray, T response, EncryptionTypes encType)
throws GreeException {
if (encType == EncryptionTypes.GCM) {
return decryptGCMPack(keyarray, response.pack, response.tag);
} else {
return decryptPack(keyarray, response.pack);
}
}
public static String decryptPack(byte[] keyarray, String message) throws GreeException {
try {
Key key = new SecretKeySpec(keyarray, "AES");
@ -58,6 +119,41 @@ public class GreeCryptoUtil {
}
}
public static String decryptGCMPack(byte[] keyBytes, String pack, String tag) throws GreeException {
try {
Key key = new SecretKeySpec(keyBytes, "AES");
Base64.Decoder decoder = Base64.getDecoder();
byte[] packBytes = decoder.decode(pack);
byte[] tagBytes = decoder.decode(tag);
byte[] messageBytes = new byte[packBytes.length + tagBytes.length];
System.arraycopy(packBytes, 0, messageBytes, 0, packBytes.length);
System.arraycopy(tagBytes, 0, messageBytes, packBytes.length, tagBytes.length);
Cipher gcmCipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, getGCMIVByteArray());
gcmCipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
gcmCipher.updateAAD(getGCMADDByteArray());
byte[] bytePlainText = gcmCipher.doFinal(messageBytes);
return new String(bytePlainText, StandardCharsets.UTF_8);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | InvalidKeyException
| IllegalBlockSizeException | InvalidAlgorithmParameterException ex) {
throw new GreeException("GCM decryption of recieved data failed", ex);
}
}
public static String[] encrypt(byte[] keyarray, String message, EncryptionTypes encType) throws GreeException {
if (encType == EncryptionTypes.GCM) {
return encryptGCMPack(keyarray, message);
} else {
String[] res = new String[1];
res[0] = encryptPack(keyarray, message);
return res;
}
}
public static String encryptPack(byte[] keyarray, String message) throws GreeException {
try {
Key key = new SecretKeySpec(keyarray, "AES");
@ -72,4 +168,32 @@ public class GreeCryptoUtil {
throw new GreeException("Unable to encrypt outbound data", ex);
}
}
public static String[] encryptGCMPack(byte[] keyarray, String message) throws GreeException {
try {
Key key = new SecretKeySpec(keyarray, "AES");
Cipher gcmCipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, getGCMIVByteArray());
gcmCipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
gcmCipher.updateAAD(getGCMADDByteArray());
byte[] encrypted = gcmCipher.doFinal(message.getBytes(StandardCharsets.UTF_8));
int packLength = encrypted.length - TAG_LENGTH;
byte[] pack = new byte[packLength];
byte[] tag = new byte[TAG_LENGTH];
System.arraycopy(encrypted, 0, pack, 0, packLength);
System.arraycopy(encrypted, packLength, tag, 0, TAG_LENGTH);
Base64.Encoder encoder = Base64.getEncoder();
String[] encryptedData = new String[2];
encryptedData[0] = new String(encoder.encode(pack), StandardCharsets.UTF_8);
encryptedData[1] = new String(encoder.encode(tag), StandardCharsets.UTF_8);
return encryptedData;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | InvalidKeyException
| IllegalBlockSizeException | InvalidAlgorithmParameterException ex) {
throw new GreeException("Unable to encrypt (gcm) outbound data", ex);
}
}
}

View File

@ -28,9 +28,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.gree.internal.GreeCryptoUtil;
import org.openhab.binding.gree.internal.GreeException;
import org.openhab.binding.gree.internal.gson.GreeScanReponsePackDTO;
import org.openhab.binding.gree.internal.gson.GreeScanRequestDTO;
import org.openhab.binding.gree.internal.gson.GreeScanResponseDTO;
import org.openhab.binding.gree.internal.gson.GreeScanResponsePackDTO;
import org.openhab.binding.gree.internal.handler.GreeAirDevice;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@ -103,12 +103,12 @@ public class GreeDeviceFinder {
}
// Decrypt message - a GreeException is thrown when something went wrong
String decryptedMsg = scanResponseGson.decryptedPack = GreeCryptoUtil
.decryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(), scanResponseGson.pack);
String decryptedMsg = scanResponseGson.decryptedPack = GreeCryptoUtil.decrypt(scanResponseGson);
logger.debug("Response received from address {}: {}", remoteAddress.getHostAddress(), decryptedMsg);
// Create the JSON to hold the response values
scanResponseGson.packJson = GSON.fromJson(decryptedMsg, GreeScanReponsePackDTO.class);
scanResponseGson.packJson = GSON.fromJson(decryptedMsg, GreeScanResponsePackDTO.class);
// Now make sure the device is reported as a Gree device
if ("gree".equalsIgnoreCase(scanResponseGson.packJson.brand)) {

View File

@ -0,0 +1,29 @@
/**
* 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.gree.internal.gson;
/**
*
* The GreeBaseDTO class is used as a base class for request and response classes
*
* @author Zhivka Dimvoa - Initial contribution
*/
public class GreeBaseDTO {
public String t = null;
public int i = 0;
public int uid = 0;
public String cid = null;
public String tcid = null;
public String tag = null;
public String pack = null;
}

View File

@ -14,7 +14,7 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeBindRequestPack4Gson class is used by Gson to hold values to be send to
* The GreeBindRequestPackDTO class is used by Gson to hold values to be send to
* the Air Conditioner during Binding
*
* @author John Cunha - Initial contribution

View File

@ -14,20 +14,11 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeBindResponse4Gson class is used by Gson to hold values returned from
* The GreeBindResponseDTO class is used by Gson to hold values returned from
* the Air Conditioner during Binding
*
* @author John Cunha - Initial contribution
*/
public class GreeBindResponseDTO {
public String t = null;
public int i = 0;
public int uid = 0;
public String cid = null;
public String tcid = null;
public String pack = null;
public transient String decryptedPack = null;
public class GreeBindResponseDTO extends GreeResponseBaseDTO {
public transient GreeBindResponsePackDTO packJson = null;
}

View File

@ -14,7 +14,7 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeBindResponsePack4Gson class is used by Gson to hold values returned from
* The GreeBindResponsePackDTO class is used by Gson to hold values returned from
* the Air Conditioner during Binding
*
* @author John Cunha - Initial contribution

View File

@ -14,21 +14,12 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeExecResponse4Gson class is used by Gson to hold values returned from
* The GreeExecResponseDTO class is used by Gson to hold values returned from
* the Air Conditioner during requests for Execution of Commands to the
* Air Conditioner.
*
* @author John Cunha - Initial contribution
*/
public class GreeExecResponseDTO {
public String t = null;
public int i = 0;
public int uid = 0;
public String cid = null;
public String tcid = null;
public String pack = null;
public transient String decryptedPack = null;
public class GreeExecResponseDTO extends GreeResponseBaseDTO {
public transient GreeExecResponsePackDTO packJson = null;
}

View File

@ -14,7 +14,7 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeExecResponsePack4Gson class is used by Gson to hold values returned from
* The GreeExecResponsePackDTO class is used by Gson to hold values returned from
* the Air Conditioner during requests for Execution of Commands to the
* Air Conditioner.
*

View File

@ -14,7 +14,7 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeExecuteCommandPack4Gson class is used by Gson to hold values to be send to
* The GreeExecuteCommandPackDTO class is used by Gson to hold values to be send to
* the Air Conditioner during requests for Execution of Commands to the
* Air Conditioner.
*

View File

@ -14,17 +14,11 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeReqStatus4Gson class is used by Gson to hold values to be send to
* The GreeReqStatusDTO class is used by Gson to hold values to be send to
* the Air Conditioner during requests for Status Updates to the
* Air Conditioner.
*
* @author John Cunha - Initial contribution
*/
public class GreeReqStatusDTO {
public String cid = null;
public int i = 0;
public String t = null;
public int uid = 0;
public String pack = null;
public String tcid = null;
public class GreeReqStatusDTO extends GreeRequestDTO {
}

View File

@ -14,14 +14,13 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeReqStatusPack4Gson class is used by Gson to hold values to be send to
* The GreeReqStatusPackDTO class is used by Gson to hold values to be send to
* the Air Conditioner during requests for Status Updates to the
* Air Conditioner.
*
* @author John Cunha - Initial contribution
*/
public class GreeReqStatusPackDTO {
public String t = null;
public String[] cols = null;
public String mac = null;

View File

@ -14,17 +14,10 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeBindRequest4Gson class is used by Gson to hold values to be send to
* the Air Conditioner during Binding
* The GreeRequestDTO class is used by Gson to hold values to be send to
* the Air Conditioner during Binding and as a base class for other request classes
*
* @author John Cunha - Initial contribution
*/
public class GreeRequestDTO {
public int uid = 0;
public String t = null;
public int i = 0;
public String pack = null;
public String cid = null;
public String tcid = null;
public class GreeRequestDTO extends GreeBaseDTO {
}

View File

@ -0,0 +1,23 @@
/**
* 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.gree.internal.gson;
/**
*
* The GreeResponseBaseDTO class is used as a base class for response classes
*
* @author Zhivka Dimvoa - Initial contribution
*/
public class GreeResponseBaseDTO extends GreeBaseDTO {
public transient String decryptedPack = null;
}

View File

@ -14,7 +14,7 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeScanRequest4Gson class is used by Gson to hold values sent to
* The GreeScanRequestDTO class is used by Gson to hold values sent to
* the Air Conditioner during Scan Requests to the Air Conditioner.
*
* @author John Cunha - Initial contribution

View File

@ -14,18 +14,11 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeScanResponse4Gson class is used by Gson to hold values returned by
* The GreeScanResponseDTO class is used by Gson to hold values returned by
* the Air Conditioner during Scan Requests to the Air Conditioner.
*
* @author John Cunha - Initial contribution
*/
public class GreeScanResponseDTO {
public String t = null;
public int i = 0;
public int uid = 0;
public String cid = null;
public String tcid = null;
public String pack = null;
public transient String decryptedPack = null;
public transient GreeScanReponsePackDTO packJson = null;
public class GreeScanResponseDTO extends GreeResponseBaseDTO {
public transient GreeScanResponsePackDTO packJson = null;
}

View File

@ -14,13 +14,12 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeScanReponsePack4Gson class is used by Gson to hold values returned by
* The GreeScanReponsePackDTO class is used by Gson to hold values returned by
* the Air Conditioner during Scan Requests to the Air Conditioner.
*
* @author John Cunha - Initial contribution
*/
public class GreeScanReponsePackDTO {
public class GreeScanResponsePackDTO {
public String t = null;
public String cid = null;
public String bc = null;

View File

@ -14,21 +14,12 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeStatusResponse4Gson class is used by Gson to hold values returned from
* The GreeStatusResponseDTO class is used by Gson to hold values returned from
* the Air Conditioner during requests for Status Updates to the
* Air Conditioner.
*
* @author John Cunha - Initial contribution
*/
public class GreeStatusResponseDTO {
public String t = null;
public int i = 0;
public int uid = 0;
public String cid = null;
public String tcid = null;
public String pack = null;
public transient String decryptedPack = null;
public class GreeStatusResponseDTO extends GreeResponseBaseDTO {
public transient GreeStatusResponsePackDTO packJson = null;
}

View File

@ -14,7 +14,7 @@ package org.openhab.binding.gree.internal.gson;
/**
*
* The GreeStatusResponsePack4Gson class is used by Gson to hold values returned from
* The GreeStatusResponsePackDTO class is used by Gson to hold values returned from
* the Air Conditioner during requests for Status Updates to the
* Air Conditioner.
*

View File

@ -64,6 +64,7 @@ public class GreeAirDevice {
private final InetAddress ipAddress;
private int port = 0;
private String encKey = "";
private GreeCryptoUtil.EncryptionTypes encType = GreeCryptoUtil.EncryptionTypes.ECB;
private Optional<GreeScanResponseDTO> scanResponseGson = Optional.empty();
private Optional<GreeStatusResponseDTO> statusResponseGson = Optional.empty();
private Optional<GreeStatusResponsePackDTO> prevStatusResponsePackGson = Optional.empty();
@ -76,6 +77,7 @@ public class GreeAirDevice {
this.ipAddress = ipAddress;
this.port = port;
this.scanResponseGson = Optional.of(scanResponse);
this.encType = GreeCryptoUtil.getEncryptionType(scanResponse);
}
public void getDeviceStatus(DatagramSocket clientSocket) throws GreeException {
@ -117,9 +119,8 @@ public class GreeAirDevice {
String reqStatusPackStr = GSON.toJson(reqStatusPackGson);
// Encrypt and send the Status Request pack
String encryptedStatusReqPacket = GreeCryptoUtil.encryptPack(getKey(), reqStatusPackStr);
DatagramPacket sendPacket = createPackRequest(0,
new String(encryptedStatusReqPacket.getBytes(), StandardCharsets.UTF_8));
String[] encryptedStatusReqData = GreeCryptoUtil.encrypt(getKey(), reqStatusPackStr, encType);
DatagramPacket sendPacket = createPackRequest(0, encryptedStatusReqData);
clientSocket.send(sendPacket);
// Keep a copy of the old response to be used to check if values have changed
@ -131,7 +132,7 @@ public class GreeAirDevice {
// Read the response, create the JSON to hold the response values
GreeStatusResponseDTO resp = receiveResponse(clientSocket, GreeStatusResponseDTO.class);
resp.decryptedPack = GreeCryptoUtil.decryptPack(getKey(), resp.pack);
resp.decryptedPack = GreeCryptoUtil.decrypt(getKey(), resp, encType);
logger.debug("Response from device: {}", resp.decryptedPack);
resp.packJson = GSON.fromJson(resp.decryptedPack, GreeStatusResponsePackDTO.class);
@ -157,14 +158,14 @@ public class GreeAirDevice {
String bindReqPackStr = GSON.toJson(bindReqPackGson);
// Encrypt and send the Binding Request pack
String encryptedBindReqPacket = GreeCryptoUtil.encryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(),
bindReqPackStr);
DatagramPacket sendPacket = createPackRequest(1, encryptedBindReqPacket);
String[] encryptedBindReqData = GreeCryptoUtil.encrypt(GreeCryptoUtil.getGeneralKeyByteArray(encType),
bindReqPackStr, encType);
DatagramPacket sendPacket = createPackRequest(1, encryptedBindReqData);
clientSocket.send(sendPacket);
// Recieve a response, create the JSON to hold the response values
GreeBindResponseDTO resp = receiveResponse(clientSocket, GreeBindResponseDTO.class);
resp.decryptedPack = GreeCryptoUtil.decryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(), resp.pack);
resp.decryptedPack = GreeCryptoUtil.decrypt(resp, encType);
resp.packJson = GSON.fromJson(resp.decryptedPack, GreeBindResponsePackDTO.class);
// Now set the key and flag to indicate the bind was successful
@ -424,13 +425,13 @@ public class GreeAirDevice {
String execCmdPackStr = GSON.toJson(execCmdPackGson);
// Now encrypt and send the Command Request pack
String encryptedCommandReqPacket = GreeCryptoUtil.encryptPack(getKey(), execCmdPackStr);
DatagramPacket sendPacket = createPackRequest(0, encryptedCommandReqPacket);
String[] encryptedCommandReqData = GreeCryptoUtil.encrypt(getKey(), execCmdPackStr, encType);
DatagramPacket sendPacket = createPackRequest(0, encryptedCommandReqData);
clientSocket.send(sendPacket);
// Receive and decode result
GreeExecResponseDTO execResponseGson = receiveResponse(clientSocket, GreeExecResponseDTO.class);
execResponseGson.decryptedPack = GreeCryptoUtil.decryptPack(getKey(), execResponseGson.pack);
execResponseGson.decryptedPack = GreeCryptoUtil.decrypt(getKey(), execResponseGson, encType);
// Create the JSON to hold the response values
execResponseGson.packJson = GSON.fromJson(execResponseGson.decryptedPack, GreeExecResponsePackDTO.class);
@ -451,14 +452,21 @@ public class GreeAirDevice {
executeCommand(clientSocket, Map.of(command, value));
}
private DatagramPacket createPackRequest(int i, String pack) {
private DatagramPacket createPackRequest(int i, String[] data) {
GreeRequestDTO request = new GreeRequestDTO();
request.cid = GREE_CID;
request.i = i;
request.t = GREE_CMDT_PACK;
request.uid = 0;
request.tcid = getId();
request.pack = pack;
request.pack = data[0];
if (encType == GreeCryptoUtil.EncryptionTypes.GCM) {
if (data.length > 1) {
request.tag = data[1];
} else {
logger.warn("Missing string for tag property for GCM encryption data");
}
}
byte[] sendData = GSON.toJson(request).getBytes(StandardCharsets.UTF_8);
return new DatagramPacket(sendData, sendData.length, ipAddress, port);
}