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
@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");
}
}
+ 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");
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);
+ }
+ }
}
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;
}
// 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)) {
--- /dev/null
+/**
+ * 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;
+}
/**
*
- * 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
/**
*
- * 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;
}
/**
*
- * 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
/**
*
- * 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;
}
/**
*
- * 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.
*
/**
*
- * 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.
*
/**
*
- * 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 {
}
/**
*
- * 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;
/**
*
- * 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 {
}
--- /dev/null
+/**
+ * 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;
+}
+++ /dev/null
-/**
- * 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 GreeScanReponsePack4Gson 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 String t = null;
- public String cid = null;
- public String bc = null;
- public String brand = null;
- public String catalog = null;
- public String mac = null;
- public String mid = null;
- public String model = null;
- public String name = null;
- public String series = null;
- public String vender = null;
- public String ver = null;
- public int lock = 0;
-}
/**
*
- * 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
/**
*
- * 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;
}
--- /dev/null
+/**
+ * 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 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 GreeScanResponsePackDTO {
+ public String t = null;
+ public String cid = null;
+ public String bc = null;
+ public String brand = null;
+ public String catalog = null;
+ public String mac = null;
+ public String mid = null;
+ public String model = null;
+ public String name = null;
+ public String series = null;
+ public String vender = null;
+ public String ver = null;
+ public int lock = 0;
+}
/**
*
- * 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;
}
/**
*
- * 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.
*
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();
this.ipAddress = ipAddress;
this.port = port;
this.scanResponseGson = Optional.of(scanResponse);
+ this.encType = GreeCryptoUtil.getEncryptionType(scanResponse);
}
public void getDeviceStatus(DatagramSocket clientSocket) throws GreeException {
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
// 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);
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
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);
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);
}