2 * Copyright (c) 2010-2024 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.dsmr.internal.device;
15 import java.nio.ByteBuffer;
16 import java.security.InvalidAlgorithmParameterException;
17 import java.security.InvalidKeyException;
18 import java.security.NoSuchAlgorithmException;
19 import java.util.Arrays;
20 import java.util.Optional;
22 import javax.crypto.BadPaddingException;
23 import javax.crypto.Cipher;
24 import javax.crypto.IllegalBlockSizeException;
25 import javax.crypto.NoSuchPaddingException;
26 import javax.crypto.spec.GCMParameterSpec;
27 import javax.crypto.spec.SecretKeySpec;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.dsmr.internal.DSMRBindingConstants;
32 import org.openhab.binding.dsmr.internal.device.connector.DSMRErrorStatus;
33 import org.openhab.binding.dsmr.internal.device.p1telegram.P1TelegramListener;
34 import org.openhab.binding.dsmr.internal.device.p1telegram.TelegramParser;
35 import org.openhab.core.util.HexUtils;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
40 * Decodes messages send by The Luxembourgian Smart Meter "Smarty".
42 * @author Hilbrand Bouwkamp - Initial contribution
45 public class SmartyDecrypter implements TelegramParser {
48 WAITING_FOR_START_BYTE,
49 READ_SYSTEM_TITLE_LENGTH,
59 private static final byte START_BYTE = (byte) 0xDB;
60 private static final byte SEPARATOR_82 = (byte) 0x82;
61 private static final byte SEPARATOR_30 = 0x30;
62 private static final int ADD_LENGTH = 17;
63 private static final int IV_BUFFER_LENGTH = 40;
64 private static final int GCM_TAG_LENGTH = 12;
65 private static final int GCM_BITS = GCM_TAG_LENGTH * Byte.SIZE;
66 private static final int MESSAGES_BUFFER_SIZE = 4096;
67 private static final String ADDITIONAL_ADD_PREFIX = "30";
69 private final Logger logger = LoggerFactory.getLogger(SmartyDecrypter.class);
70 private final ByteBuffer iv = ByteBuffer.allocate(IV_BUFFER_LENGTH);
71 private final ByteBuffer cipherText = ByteBuffer.allocate(MESSAGES_BUFFER_SIZE);
72 private final TelegramParser parser;
73 private @Nullable final SecretKeySpec secretKeySpec;
75 private State state = State.WAITING_FOR_START_BYTE;
76 private int currentBytePosition;
77 private int changeToNextStateAt;
79 private int dataLength;
80 private boolean lenientMode;
81 private final P1TelegramListener telegramListener;
82 private final byte[] addKey;
87 * @param parser parser of the Cosem messages
88 * @param telegramListener
89 * @param decryptionKey The key to decrypt the messages
90 * @param additionalKey Additional optional key to decrypt the message
92 public SmartyDecrypter(final TelegramParser parser, final P1TelegramListener telegramListener,
93 final String decryptionKey, final String additionalKey) {
95 this.telegramListener = telegramListener;
96 secretKeySpec = decryptionKey.isEmpty() ? null : new SecretKeySpec(HexUtils.hexToBytes(decryptionKey), "AES");
97 addKey = HexUtils.hexToBytes(additionalKey.isBlank() ? DSMRBindingConstants.CONFIGURATION_ADDITIONAL_KEY_DEFAULT
98 : ((additionalKey.length() == 32 ? (ADDITIONAL_ADD_PREFIX) : "") + additionalKey));
102 public void parse(final byte[] data, final int length) {
103 for (int i = 0; i < length; i++) {
104 currentBytePosition++;
105 if (processStateActions(data[i])) {
109 if (lenientMode && secretKeySpec == null) {
110 parser.parse(data, length);
114 private boolean processStateActions(final byte rawInput) {
115 // Safeguard against buffer overrun in case corrupt data is received.
116 if (ivLength == IV_BUFFER_LENGTH) {
121 case WAITING_FOR_START_BYTE:
122 if (rawInput == START_BYTE) {
124 state = State.READ_SYSTEM_TITLE_LENGTH;
127 case READ_SYSTEM_TITLE_LENGTH:
128 state = State.READ_SYSTEM_TITLE;
129 // 2 start bytes (position 0 and 1) + system title length
130 changeToNextStateAt = 1 + rawInput;
132 case READ_SYSTEM_TITLE:
135 if (currentBytePosition >= changeToNextStateAt) {
136 state = State.READ_SEPARATOR_82;
137 changeToNextStateAt++;
140 case READ_SEPARATOR_82:
141 if (rawInput == SEPARATOR_82) {
142 state = State.READ_PAYLOAD_LENGTH; // Ignore separator byte
143 changeToNextStateAt += 2;
145 logger.debug("Missing separator (0x82). Dropping telegram.");
146 state = State.WAITING_FOR_START_BYTE;
149 case READ_PAYLOAD_LENGTH:
151 dataLength |= rawInput & 0xFF;
152 if (currentBytePosition >= changeToNextStateAt) {
153 state = State.READ_SEPARATOR_30;
154 changeToNextStateAt++;
157 case READ_SEPARATOR_30:
158 if (rawInput == SEPARATOR_30) {
159 state = State.READ_FRAME_COUNTER;
160 // 4 bytes for frame counter
161 changeToNextStateAt += 4;
163 logger.debug("Missing separator (0x30). Dropping telegram.");
164 state = State.WAITING_FOR_START_BYTE;
167 case READ_FRAME_COUNTER:
170 if (currentBytePosition >= changeToNextStateAt) {
171 state = State.READ_PAYLOAD;
172 changeToNextStateAt += dataLength - ADD_LENGTH;
176 cipherText.put(rawInput);
177 if (currentBytePosition >= changeToNextStateAt) {
178 state = State.READ_GCM_TAG;
179 changeToNextStateAt += GCM_TAG_LENGTH;
183 // All input has been read.
184 cipherText.put(rawInput);
185 if (currentBytePosition >= changeToNextStateAt) {
186 state = State.WAITING_FOR_START_BYTE;
194 private void processCompleted() {
196 final byte[] plainText = decrypt();
198 if (plainText != null) {
199 parser.parse(plainText, plainText.length);
207 * Decrypts the collected message.
209 * @return the decrypted message
211 private byte @Nullable [] decrypt() {
213 if (secretKeySpec != null) {
214 final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
215 cipher.init(Cipher.DECRYPT_MODE, secretKeySpec,
216 new GCMParameterSpec(GCM_BITS, iv.array(), 0, ivLength));
217 cipher.updateAAD(addKey);
218 return cipher.doFinal(cipherText.array(), 0, cipherText.position());
220 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
221 | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
222 if (lenientMode || logger.isDebugEnabled()) {
223 // log in lenient mode or when debug is enabled. But log to warn to also work when lenientMode is
225 logger.warn("Failed encrypted telegram: {}",
226 HexUtils.bytesToHex(Arrays.copyOf(cipherText.array(), cipherText.position())));
227 logger.warn("Exception of failed decryption of telegram: ", e);
229 telegramListener.onError(DSMRErrorStatus.INVALID_DECRYPTION_KEY,
230 Optional.ofNullable(e.getMessage()).orElse(""));
236 public void reset() {
238 state = State.WAITING_FOR_START_BYTE;
241 currentBytePosition = 0;
242 changeToNextStateAt = 0;
248 public void setLenientMode(final boolean lenientMode) {
249 this.lenientMode = lenientMode;
250 parser.setLenientMode(lenientMode);