2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.silvercrestwifisocket.internal.utils;
15 import java.net.DatagramPacket;
16 import java.nio.charset.StandardCharsets;
17 import java.util.regex.Pattern;
19 import javax.crypto.BadPaddingException;
20 import javax.crypto.Cipher;
21 import javax.crypto.IllegalBlockSizeException;
22 import javax.crypto.spec.IvParameterSpec;
23 import javax.crypto.spec.SecretKeySpec;
25 import org.openhab.binding.silvercrestwifisocket.internal.entities.SilvercrestWifiSocketRequest;
26 import org.openhab.binding.silvercrestwifisocket.internal.entities.SilvercrestWifiSocketResponse;
27 import org.openhab.binding.silvercrestwifisocket.internal.enums.SilvercrestWifiSocketResponseType;
28 import org.openhab.binding.silvercrestwifisocket.internal.enums.SilvercrestWifiSocketVendor;
29 import org.openhab.binding.silvercrestwifisocket.internal.exceptions.NotOneResponsePacketException;
30 import org.openhab.binding.silvercrestwifisocket.internal.exceptions.PacketIntegrityErrorException;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * Transforms the the received datagram packet to one
37 * @author Jaime Vaz - Initial contribution
38 * @author Christian Heimerl - for integration of EasyHome
41 public class WifiSocketPacketConverter {
43 private final Logger logger = LoggerFactory.getLogger(WifiSocketPacketConverter.class);
45 private static final String REQUEST_PREFIX = "01";
46 private static final String RESPONSE_PREFIX = "0142";
47 private static final String LOCK_STATUS = "40";
48 /* encryptDataLength */
49 private static final String ENCRYPT_PREFIX = "00";
50 private static final String PACKET_NUMBER = "FFFF";
51 private static final String DEVICE_TYPE = "11";
53 private static final String ENCRIPTION_KEY = "0123456789abcdef";
55 private Cipher silvercrestEncryptCipher;
56 private Cipher silvercrestDecryptCipher;
59 * START_OF_RECEIVED_PACKET.
60 * STX - pkt nbr - CompanyCode - device - authCode
62 * 00 -- 0029 -- C1 -- 11 -- 7150 (SilverCrest)
63 * 00 -- 0029 -- C2 -- 11 -- 92DD (EasyHome)
65 private static final String REGEX_START_OF_RECEIVED_PACKET = "00([A-F0-9]{4})(?:C21192DD|C1117150)";
66 private static final String REGEX_HEXADECIMAL_PAIRS = "([A-F0-9]{2})*";
68 private static final String REGEX_START_OF_RECEIVED_PACKET_SEARCH_MAC_ADDRESS = REGEX_START_OF_RECEIVED_PACKET
69 + "23" + REGEX_HEXADECIMAL_PAIRS;
70 private static final String REGEX_START_OF_RECEIVED_PACKET_HEART_BEAT = REGEX_START_OF_RECEIVED_PACKET + "61"
71 + REGEX_HEXADECIMAL_PAIRS;
72 private static final String REGEX_START_OF_RECEIVED_PACKET_CMD_GPIO_EVENT = REGEX_START_OF_RECEIVED_PACKET + "06"
73 + REGEX_HEXADECIMAL_PAIRS;
74 private static final String REGEX_START_OF_RECEIVED_PACKET_QUERY_STATUS = REGEX_START_OF_RECEIVED_PACKET + "02"
75 + REGEX_HEXADECIMAL_PAIRS;
76 private static final String REGEX_START_OF_RECEIVED_PACKET_RESPONSE_GPIO_CHANGE_REQUEST = REGEX_START_OF_RECEIVED_PACKET
77 + "01" + REGEX_HEXADECIMAL_PAIRS;
80 * Default constructor of the packet converter.
82 public WifiSocketPacketConverter() {
84 byte[] encriptionKeyBytes;
86 encriptionKeyBytes = ENCRIPTION_KEY.getBytes(StandardCharsets.UTF_8);
87 SecretKeySpec secretKey = new SecretKeySpec(encriptionKeyBytes, "AES");
88 IvParameterSpec ivKey = new IvParameterSpec(encriptionKeyBytes);
90 this.silvercrestEncryptCipher = Cipher.getInstance("AES/CBC/NoPadding");
91 this.silvercrestEncryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, ivKey);
93 this.silvercrestDecryptCipher = Cipher.getInstance("AES/CBC/NoPadding");
94 this.silvercrestDecryptCipher.init(Cipher.DECRYPT_MODE, secretKey, ivKey);
95 } catch (Exception exception) {
97 "Failure on WifiSocketPacketConverter creation. There was a problem creating ciphers. Error: {}",
98 exception.getLocalizedMessage());
103 * Method that transforms one {@link SilvercrestWifiSocketRequest} to one byte array to be ready to be transmitted.
105 * @param requestPacket the {@link SilvercrestWifiSocketRequest}.
106 * @return the byte array with the message.
108 public byte[] transformToByteMessage(final SilvercrestWifiSocketRequest requestPacket) {
109 byte[] requestDatagram = null;
110 String fullCommand = ENCRYPT_PREFIX + PACKET_NUMBER + requestPacket.getVendor().getCompanyCode() + DEVICE_TYPE
111 + requestPacket.getVendor().getAuthenticationCode()
112 + String.format(requestPacket.getType().getCommand(), requestPacket.getMacAddress());
114 byte[] inputByte = hexStringToByteArray(fullCommand);
117 bEncrypted = this.silvercrestEncryptCipher.doFinal(inputByte);
118 int encryptDataLength = bEncrypted.length;
120 logger.trace("Encrypted data={{}}", byteArrayToHexString(inputByte));
121 logger.trace("Decrypted data={{}}", byteArrayToHexString(bEncrypted));
122 String cryptedCommand = byteArrayToHexString(bEncrypted);
124 String packetString = REQUEST_PREFIX + LOCK_STATUS + requestPacket.getMacAddress()
125 + Integer.toHexString(encryptDataLength) + cryptedCommand;
127 logger.trace("Request Packet: {}", packetString);
128 logger.trace("Request packet decrypted data: [{}] with lenght: {}", fullCommand, fullCommand.length());
129 requestDatagram = hexStringToByteArray(packetString);
130 } catch (BadPaddingException | IllegalBlockSizeException e) {
131 logger.debug("Failure processing the build of the request packet for mac '{}' and type '{}'",
132 requestPacket.getMacAddress(), requestPacket.getType());
134 return requestDatagram;
138 * Decrypts one response {@link DatagramPacket}.
140 * @param packet the {@link DatagramPacket}
141 * @return the {@link SilvercrestWifiSocketResponse} is successfully decrypted.
142 * @throws PacketIntegrityErrorException if the message has some integrity error.
143 * @throws NotOneResponsePacketException if the message received is not one response packet.
145 public SilvercrestWifiSocketResponse decryptResponsePacket(final DatagramPacket packet)
146 throws PacketIntegrityErrorException, NotOneResponsePacketException {
147 SilvercrestWifiSocketResponse responsePacket = this.decryptResponsePacket(
148 WifiSocketPacketConverter.byteArrayToHexString(packet.getData(), packet.getLength()));
150 responsePacket.setHostAddress(packet.getAddress().getHostAddress());
152 return responsePacket;
156 * STX - pkt nbr - CompanyCode - device - authCode
158 * 00 -- 0029 -- C1 -- 11 -- 7150 (Silvercrest)
159 * 00 -- 0029 -- C2 -- 11 -- 92DD (EasyHome)
161 * @param hexPacket the hex packet to convert
162 * @return the converted response.
163 * @throws PacketIntegrityErrorException the packet passed is not recognized.
164 * @throws NotOneResponsePacketException the packet passed is not one response.
166 private SilvercrestWifiSocketResponse decryptResponsePacket(final String hexPacket)
167 throws PacketIntegrityErrorException, NotOneResponsePacketException {
168 if (!Pattern.matches(RESPONSE_PREFIX + REGEX_HEXADECIMAL_PAIRS, hexPacket)) {
169 logger.trace("The packet received is not one response! \nPacket:[{}]", hexPacket);
170 throw new NotOneResponsePacketException("The packet received is not one response.");
173 logger.trace("Response packet: {}", hexPacket);
174 String macAddress = hexPacket.substring(4, 16);
175 logger.trace("The mac address of the sender of the packet is: {}", macAddress);
176 String decryptedData = this.decrypt(hexPacket.substring(18, hexPacket.length()));
178 logger.trace("Response packet decrypted data: [{}] with lenght: {}", decryptedData, decryptedData.length());
180 SilvercrestWifiSocketResponseType responseType;
181 // check packet integrity
182 if (Pattern.matches(REGEX_START_OF_RECEIVED_PACKET_SEARCH_MAC_ADDRESS, decryptedData)) {
183 responseType = SilvercrestWifiSocketResponseType.DISCOVERY;
184 logger.trace("Received answer of mac address search! lenght:{}", decryptedData.length());
185 } else if (Pattern.matches(REGEX_START_OF_RECEIVED_PACKET_HEART_BEAT, decryptedData)) {
186 responseType = SilvercrestWifiSocketResponseType.ACK;
187 logger.trace("Received heart beat!");
188 } else if (Pattern.matches(REGEX_START_OF_RECEIVED_PACKET_CMD_GPIO_EVENT, decryptedData)) {
189 logger.trace("Received gpio event!");
190 String status = decryptedData.substring(20, 22);
191 responseType = "FF".equalsIgnoreCase(status) ? SilvercrestWifiSocketResponseType.ON
192 : SilvercrestWifiSocketResponseType.OFF;
193 logger.trace("Socket status: {}", responseType);
194 } else if (Pattern.matches(REGEX_START_OF_RECEIVED_PACKET_RESPONSE_GPIO_CHANGE_REQUEST, decryptedData)) {
195 logger.trace("Received response from a gpio change request!");
196 String status = decryptedData.substring(20, 22);
197 responseType = "FF".equalsIgnoreCase(status) ? SilvercrestWifiSocketResponseType.ON
198 : SilvercrestWifiSocketResponseType.OFF;
199 logger.trace("Socket status: {}", responseType);
200 } else if (Pattern.matches(REGEX_START_OF_RECEIVED_PACKET_QUERY_STATUS, decryptedData)) {
201 logger.trace("Received response from status query!");
202 String status = decryptedData.substring(20, 22);
203 responseType = "FF".equalsIgnoreCase(status) ? SilvercrestWifiSocketResponseType.ON
204 : SilvercrestWifiSocketResponseType.OFF;
205 logger.trace("Socket status: {}", responseType);
207 throw new PacketIntegrityErrorException("The packet decrypted is with wrong format. \nPacket:[" + hexPacket
208 + "] \nDecryptedPacket:[" + decryptedData + "]");
211 SilvercrestWifiSocketVendor vendor = SilvercrestWifiSocketVendor.fromCode(decryptedData.substring(6, 8));
212 if (vendor == null) {
213 throw new PacketIntegrityErrorException("Could not extract vendor from the decrypted packet. \nPacket:["
214 + hexPacket + "] \nDecryptedPacket:[" + decryptedData + "]");
217 logger.trace("Decrypt success. Packet is from socket with mac address [{}] and type is [{}] and vendor is [{}]",
218 macAddress, responseType, vendor);
219 return new SilvercrestWifiSocketResponse(macAddress, responseType, vendor);
223 * Decrypts one received message with the correct cypher.
225 * @param inputData the cyphered message
226 * @return the decrypted message.
228 private String decrypt(final String inputData) {
229 byte[] inputByte = hexStringToByteArray(inputData);
232 bDecrypted = this.silvercrestDecryptCipher.doFinal(inputByte);
233 logger.trace("Encrypted data={{}}", byteArrayToHexString(inputByte));
234 logger.trace("Decrypted data={{}}", byteArrayToHexString(bDecrypted));
235 return byteArrayToHexString(bDecrypted);
236 } catch (Exception e) {
237 logger.trace("Problem decrypting the input data. Bad reception?");
242 // String/Array/Hex manipulation
244 * Converts one hexadecimal string to one byte array.
246 * @param str the string to convert.
247 * @return the byte array.
249 private static byte[] hexStringToByteArray(final String str) {
250 byte[] b = new byte[str.length() / 2];
251 for (int i = 0; i < b.length; i++) {
253 int v = Integer.parseInt(str.substring(index, index + 2), 16);
260 * Converts one full byte array to one hexadecimal string.
262 * @param array the byte array to convert.
263 * @return the hexadecimal string.
265 private static String byteArrayToHexString(final byte[] array) {
266 return byteArrayToHexString(array, array.length);
270 * Converts one partial byte array to one hexadecimal string.
272 * @param array the byte array to convert.
273 * @param length the length to convert.
274 * @return the hexadecimal string.
276 private static String byteArrayToHexString(final byte[] array, final int length) {
277 if ((array == null) || (array.length == 0)) {
280 StringBuilder builder = new StringBuilder();
283 for (int i = 0; i < length; i++) {
284 hex = Integer.toHexString(0xFF & array[i]).toUpperCase();
285 if (hex.length() < 2) {
290 return builder.toString();