]> git.basschouten.com Git - openhab-addons.git/blob
399928d1be80ae7012c279921e9664add4f60086
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.silvercrestwifisocket.internal.utils;
14
15 import java.net.DatagramPacket;
16 import java.nio.charset.StandardCharsets;
17 import java.util.regex.Pattern;
18
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;
24
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;
33
34 /**
35  * Transforms the the received datagram packet to one
36  *
37  * @author Jaime Vaz - Initial contribution
38  * @author Christian Heimerl - for integration of EasyHome
39  *
40  */
41 public class WifiSocketPacketConverter {
42
43     private final Logger logger = LoggerFactory.getLogger(WifiSocketPacketConverter.class);
44
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";
52
53     private static final String ENCRIPTION_KEY = "0123456789abcdef";
54
55     private Cipher silvercrestEncryptCipher;
56     private Cipher silvercrestDecryptCipher;
57
58     /**
59      * START_OF_RECEIVED_PACKET.
60      * STX - pkt nbr - CompanyCode - device - authCode
61      *
62      * 00 -- 0029 -- C1 -- 11 -- 7150 (SilverCrest)
63      * 00 -- 0029 -- C2 -- 11 -- 92DD (EasyHome)
64      */
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})*";
67
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;
78
79     /**
80      * Default constructor of the packet converter.
81      */
82     public WifiSocketPacketConverter() {
83         // init cipher
84         byte[] encriptionKeyBytes;
85         try {
86             encriptionKeyBytes = ENCRIPTION_KEY.getBytes(StandardCharsets.UTF_8);
87             SecretKeySpec secretKey = new SecretKeySpec(encriptionKeyBytes, "AES");
88             IvParameterSpec ivKey = new IvParameterSpec(encriptionKeyBytes);
89
90             this.silvercrestEncryptCipher = Cipher.getInstance("AES/CBC/NoPadding");
91             this.silvercrestEncryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, ivKey);
92
93             this.silvercrestDecryptCipher = Cipher.getInstance("AES/CBC/NoPadding");
94             this.silvercrestDecryptCipher.init(Cipher.DECRYPT_MODE, secretKey, ivKey);
95         } catch (Exception exception) {
96             logger.debug(
97                     "Failure on WifiSocketPacketConverter creation. There was a problem creating ciphers. Error: {}",
98                     exception.getLocalizedMessage());
99         }
100     }
101
102     /**
103      * Method that transforms one {@link SilvercrestWifiSocketRequest} to one byte array to be ready to be transmitted.
104      *
105      * @param requestPacket the {@link SilvercrestWifiSocketRequest}.
106      * @return the byte array with the message.
107      */
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());
113
114         byte[] inputByte = hexStringToByteArray(fullCommand);
115         byte[] bEncrypted;
116         try {
117             bEncrypted = this.silvercrestEncryptCipher.doFinal(inputByte);
118             int encryptDataLength = bEncrypted.length;
119
120             logger.trace("Encrypted data={{}}", byteArrayToHexString(inputByte));
121             logger.trace("Decrypted data={{}}", byteArrayToHexString(bEncrypted));
122             String cryptedCommand = byteArrayToHexString(bEncrypted);
123
124             String packetString = REQUEST_PREFIX + LOCK_STATUS + requestPacket.getMacAddress()
125                     + Integer.toHexString(encryptDataLength) + cryptedCommand;
126
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());
133         }
134         return requestDatagram;
135     }
136
137     /**
138      * Decrypts one response {@link DatagramPacket}.
139      *
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.
144      */
145     public SilvercrestWifiSocketResponse decryptResponsePacket(final DatagramPacket packet)
146             throws PacketIntegrityErrorException, NotOneResponsePacketException {
147         SilvercrestWifiSocketResponse responsePacket = this.decryptResponsePacket(
148                 WifiSocketPacketConverter.byteArrayToHexString(packet.getData(), packet.getLength()));
149
150         responsePacket.setHostAddress(packet.getAddress().getHostAddress());
151
152         return responsePacket;
153     }
154
155     /**
156      * STX - pkt nbr - CompanyCode - device - authCode
157      *
158      * 00 -- 0029 -- C1 -- 11 -- 7150 (Silvercrest)
159      * 00 -- 0029 -- C2 -- 11 -- 92DD (EasyHome)
160      *
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.
165      */
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.");
171         }
172
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()));
177
178         logger.trace("Response packet decrypted data: [{}] with lenght: {}", decryptedData, decryptedData.length());
179
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);
206         } else {
207             throw new PacketIntegrityErrorException("The packet decrypted is with wrong format. \nPacket:[" + hexPacket
208                     + "]  \nDecryptedPacket:[" + decryptedData + "]");
209         }
210
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 + "]");
215         }
216
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);
220     }
221
222     /**
223      * Decrypts one received message with the correct cypher.
224      *
225      * @param inputData the cyphered message
226      * @return the decrypted message.
227      */
228     private String decrypt(final String inputData) {
229         byte[] inputByte = hexStringToByteArray(inputData);
230         byte[] bDecrypted;
231         try {
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?");
238         }
239         return null;
240     }
241
242     // String/Array/Hex manipulation
243     /**
244      * Converts one hexadecimal string to one byte array.
245      *
246      * @param str the string to convert.
247      * @return the byte array.
248      */
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++) {
252             int index = i * 2;
253             int v = Integer.parseInt(str.substring(index, index + 2), 16);
254             b[i] = (byte) v;
255         }
256         return b;
257     }
258
259     /**
260      * Converts one full byte array to one hexadecimal string.
261      *
262      * @param array the byte array to convert.
263      * @return the hexadecimal string.
264      */
265     private static String byteArrayToHexString(final byte[] array) {
266         return byteArrayToHexString(array, array.length);
267     }
268
269     /**
270      * Converts one partial byte array to one hexadecimal string.
271      *
272      * @param array the byte array to convert.
273      * @param length the length to convert.
274      * @return the hexadecimal string.
275      */
276     private static String byteArrayToHexString(final byte[] array, final int length) {
277         if ((array == null) || (array.length == 0)) {
278             return null;
279         }
280         StringBuilder builder = new StringBuilder();
281         String hex = "";
282
283         for (int i = 0; i < length; i++) {
284             hex = Integer.toHexString(0xFF & array[i]).toUpperCase();
285             if (hex.length() < 2) {
286                 hex = "0" + hex;
287             }
288             builder.append(hex);
289         }
290         return builder.toString();
291     }
292 }