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.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.Collections;
21 import javax.crypto.BadPaddingException;
22 import javax.crypto.Cipher;
23 import javax.crypto.IllegalBlockSizeException;
24 import javax.crypto.NoSuchPaddingException;
25 import javax.crypto.spec.GCMParameterSpec;
26 import javax.crypto.spec.SecretKeySpec;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.dsmr.internal.DSMRBindingConstants;
31 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram;
32 import org.openhab.binding.dsmr.internal.device.p1telegram.P1Telegram.TelegramState;
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,
60 private static final byte START_BYTE = (byte) 0xDB;
61 private static final byte SEPARATOR_82 = (byte) 0x82;
62 private static final byte SEPARATOR_30 = 0x30;
63 private static final int ADD_LENGTH = 17;
64 private static final int IV_BUFFER_LENGTH = 40;
65 private static final int GCM_TAG_LENGTH = 12;
66 private static final int GCM_BITS = GCM_TAG_LENGTH * Byte.SIZE;
67 private static final int MESSAGES_BUFFER_SIZE = 4096;
68 private static final String ADDITIONAL_ADD_PREFIX = "30";
70 private final Logger logger = LoggerFactory.getLogger(SmartyDecrypter.class);
71 private final ByteBuffer iv = ByteBuffer.allocate(IV_BUFFER_LENGTH);
72 private final ByteBuffer cipherText = ByteBuffer.allocate(MESSAGES_BUFFER_SIZE);
73 private final TelegramParser parser;
74 private @Nullable final SecretKeySpec secretKeySpec;
76 private State state = State.WAITING_FOR_START_BYTE;
77 private int currentBytePosition;
78 private int changeToNextStateAt;
80 private int dataLength;
81 private boolean lenientMode;
82 private final P1TelegramListener telegramListener;
83 private final byte[] addKey;
88 * @param parser parser of the Cosem messages
89 * @param telegramListener
90 * @param decryptionKey The key to decrypt the messages
91 * @param additionalKey Additional optional key to decrypt the message
93 public SmartyDecrypter(final TelegramParser parser, final P1TelegramListener telegramListener,
94 final String decryptionKey, final String additionalKey) {
96 this.telegramListener = telegramListener;
97 secretKeySpec = decryptionKey.isEmpty() ? null : new SecretKeySpec(HexUtils.hexToBytes(decryptionKey), "AES");
98 addKey = HexUtils.hexToBytes(additionalKey.isBlank() ? DSMRBindingConstants.ADDITIONAL_KEY_DEFAULT
99 : ((additionalKey.length() == 32 ? (ADDITIONAL_ADD_PREFIX) : "") + additionalKey));
103 public void parse(final byte[] data, final int length) {
104 for (int i = 0; i < length; i++) {
105 currentBytePosition++;
106 if (processStateActions(data[i])) {
110 if (lenientMode && secretKeySpec == null) {
111 parser.parse(data, length);
115 private boolean processStateActions(final byte rawInput) {
117 case WAITING_FOR_START_BYTE:
118 if (rawInput == START_BYTE) {
120 state = State.READ_SYSTEM_TITLE_LENGTH;
123 case READ_SYSTEM_TITLE_LENGTH:
124 state = State.READ_SYSTEM_TITLE;
125 // 2 start bytes (position 0 and 1) + system title length
126 changeToNextStateAt = 1 + rawInput;
128 case READ_SYSTEM_TITLE:
131 if (currentBytePosition >= changeToNextStateAt) {
132 state = State.READ_SEPARATOR_82;
133 changeToNextStateAt++;
136 case READ_SEPARATOR_82:
137 if (rawInput == SEPARATOR_82) {
138 state = State.READ_PAYLOAD_LENGTH; // Ignore separator byte
139 changeToNextStateAt += 2;
141 logger.debug("Missing separator (0x82). Dropping telegram.");
142 state = State.WAITING_FOR_START_BYTE;
145 case READ_PAYLOAD_LENGTH:
147 dataLength |= rawInput & 0xFF;
148 if (currentBytePosition >= changeToNextStateAt) {
149 state = State.READ_SEPARATOR_30;
150 changeToNextStateAt++;
153 case READ_SEPARATOR_30:
154 if (rawInput == SEPARATOR_30) {
155 state = State.READ_FRAME_COUNTER;
156 // 4 bytes for frame counter
157 changeToNextStateAt += 4;
159 logger.debug("Missing separator (0x30). Dropping telegram.");
160 state = State.WAITING_FOR_START_BYTE;
163 case READ_FRAME_COUNTER:
166 if (currentBytePosition >= changeToNextStateAt) {
167 state = State.READ_PAYLOAD;
168 changeToNextStateAt += dataLength - ADD_LENGTH;
172 cipherText.put(rawInput);
173 if (currentBytePosition >= changeToNextStateAt) {
174 state = State.READ_GCM_TAG;
175 changeToNextStateAt += GCM_TAG_LENGTH;
179 // All input has been read.
180 cipherText.put(rawInput);
181 if (currentBytePosition >= changeToNextStateAt) {
182 state = State.DONE_READING_TELEGRAM;
186 if (state == State.DONE_READING_TELEGRAM) {
187 state = State.WAITING_FOR_START_BYTE;
193 private void processCompleted() {
194 final byte[] plainText = decrypt();
197 if (plainText == null) {
199 .telegramReceived(new P1Telegram(Collections.emptyList(), TelegramState.INVALID_ENCRYPTION_KEY));
201 parser.parse(plainText, plainText.length);
206 * Decrypts the collected message.
208 * @return the decrypted message
210 private byte @Nullable [] decrypt() {
212 if (secretKeySpec != null) {
213 final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
214 cipher.init(Cipher.DECRYPT_MODE, secretKeySpec,
215 new GCMParameterSpec(GCM_BITS, iv.array(), 0, ivLength));
216 cipher.updateAAD(addKey);
217 return cipher.doFinal(cipherText.array(), 0, cipherText.position());
219 } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
220 | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
221 logger.warn("Decrypting smarty telegram failed: ", e);
227 public void reset() {
229 state = State.WAITING_FOR_START_BYTE;
232 currentBytePosition = 0;
233 changeToNextStateAt = 0;
239 public void setLenientMode(final boolean lenientMode) {
240 this.lenientMode = lenientMode;
241 parser.setLenientMode(lenientMode);