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.km200.internal;
15 import java.io.UnsupportedEncodingException;
16 import java.nio.charset.StandardCharsets;
17 import java.security.GeneralSecurityException;
18 import java.security.MessageDigest;
19 import java.security.NoSuchAlgorithmException;
20 import java.util.Arrays;
21 import java.util.Base64;
23 import javax.crypto.Cipher;
24 import javax.crypto.spec.SecretKeySpec;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
32 * The KM200Cryption is managing the en- and decription of the communication to the device
34 * @author Markus Eckhardt - Initial contribution
37 public class KM200Cryption {
39 private final Logger logger = LoggerFactory.getLogger(KM200Cryption.class);
41 private final KM200Device remoteDevice;
43 public KM200Cryption(KM200Device remoteDevice) {
44 this.remoteDevice = remoteDevice;
48 * This function removes zero padding from a byte array.
51 private byte[] removeZeroPadding(byte[] bytes) {
52 int i = bytes.length - 1;
53 while (i >= 0 && bytes[i] == 0) {
56 return Arrays.copyOf(bytes, i + 1);
60 * This function adds zero padding to a byte array.
63 private byte[] addZeroPadding(byte[] bdata, int bSize, String cSet) throws UnsupportedEncodingException {
64 int encryptPadchar = bSize - (bdata.length % bSize);
65 byte[] padchars = new String(new char[encryptPadchar]).getBytes(cSet);
66 byte[] paddedData = new byte[bdata.length + padchars.length];
67 System.arraycopy(bdata, 0, paddedData, 0, bdata.length);
68 System.arraycopy(padchars, 0, paddedData, bdata.length, padchars.length);
73 * This function does the decoding for a new message from the device
76 public @Nullable String decodeMessage(byte[] encoded) {
77 String retString = null;
78 byte[] decodedB64 = null;
80 // MimeDecoder was the only working decoder.
81 decodedB64 = Base64.getMimeDecoder().decode(encoded);
84 /* Check whether the length of the decryptData is NOT multiplies of 16 */
85 if ((decodedB64.length & 0xF) != 0) {
86 logger.debug("Length of message is {}.", decodedB64.length);
88 retString = new String(decodedB64, remoteDevice.getCharSet());
89 logger.debug("Did NOT decrypt message, returning {}.", retString);
93 final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
94 cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(remoteDevice.getCryptKeyPriv(), "AES"));
95 byte[] decryptedData = cipher.doFinal(decodedB64);
96 byte[] decryptedDataWOZP = removeZeroPadding(decryptedData);
97 return (new String(decryptedDataWOZP, remoteDevice.getCharSet()));
98 } catch (UnsupportedEncodingException | GeneralSecurityException e) {
99 logger.warn("Exception on encoding ({})", e.getMessage());
105 * This function does the encoding for a new message to the device
108 public byte @Nullable [] encodeMessage(String data) {
111 byte[] bdata = data.getBytes(remoteDevice.getCharSet());
112 final Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
113 cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(remoteDevice.getCryptKeyPriv(), "AES"));
114 int bsize = cipher.getBlockSize();
115 /* Add Padding, encrypt AES and B64 */
116 byte[] encryptedData = cipher.doFinal(addZeroPadding(bdata, bsize, remoteDevice.getCharSet()));
118 return (Base64.getMimeEncoder().encode(encryptedData));
119 } catch (IllegalArgumentException e) {
120 logger.debug("Base64encoding not possible: {}", e.getMessage());
122 } catch (UnsupportedEncodingException | GeneralSecurityException e) {
123 logger.warn("Exception on encoding ({})", e.getMessage());
129 * This function creates the private key from the MD5Salt, the device and the private password
131 * @author Markus Eckhardt
133 public void recreateKeys() {
134 if (!remoteDevice.getGatewayPassword().isBlank() && !remoteDevice.getPrivatePassword().isBlank()
135 && remoteDevice.getMD5Salt().length > 0) {
137 byte[] md5K2Init = null;
138 byte[] md5K2Private = null;
139 byte[] bytesOfGatewayPassword = null;
140 byte[] bytesOfPrivatePassword = null;
142 /* Needed keys for the communication */
145 MessageDigest md = null;
147 md = MessageDigest.getInstance("MD5");
148 } catch (NoSuchAlgorithmException e) {
149 logger.warn("No such algorithm, MD5: {}", e.getMessage());
153 /* First half of the key: MD5 of (GatewayPassword . Salt) */
154 bytesOfGatewayPassword = remoteDevice.getGatewayPassword().getBytes(StandardCharsets.UTF_8);
155 byte[] combParts1 = new byte[bytesOfGatewayPassword.length + remoteDevice.getMD5Salt().length];
156 System.arraycopy(bytesOfGatewayPassword, 0, combParts1, 0, bytesOfGatewayPassword.length);
157 System.arraycopy(remoteDevice.getMD5Salt(), 0, combParts1, bytesOfGatewayPassword.length,
158 remoteDevice.getMD5Salt().length);
159 md5K1 = md.digest(combParts1);
161 /* Second half of the key: - Initial: MD5 of ( Salt) */
162 md5K2Init = md.digest(remoteDevice.getMD5Salt());
164 /* Second half of the key: - private: MD5 of ( Salt . PrivatePassword) */
165 bytesOfPrivatePassword = remoteDevice.getPrivatePassword().getBytes(StandardCharsets.UTF_8);
166 byte[] combParts2 = new byte[bytesOfPrivatePassword.length + remoteDevice.getMD5Salt().length];
167 System.arraycopy(remoteDevice.getMD5Salt(), 0, combParts2, 0, remoteDevice.getMD5Salt().length);
168 System.arraycopy(bytesOfPrivatePassword, 0, combParts2, remoteDevice.getMD5Salt().length,
169 bytesOfPrivatePassword.length);
170 md5K2Private = md.digest(combParts2);
173 cryptKeyInit = new byte[md5K1.length + md5K2Init.length];
174 System.arraycopy(md5K1, 0, cryptKeyInit, 0, md5K1.length);
175 System.arraycopy(md5K2Init, 0, cryptKeyInit, md5K1.length, md5K2Init.length);
176 remoteDevice.setCryptKeyInit(cryptKeyInit);
178 cryptKeyPriv = new byte[md5K1.length + md5K2Private.length];
179 System.arraycopy(md5K1, 0, cryptKeyPriv, 0, md5K1.length);
180 System.arraycopy(md5K2Private, 0, cryptKeyPriv, md5K1.length, md5K2Private.length);
181 remoteDevice.setCryptKeyPriv(cryptKeyPriv);