Skip to content

Commit 849cc94

Browse files
committed
refactor parsing to in-place unwrap
replace recursive logic with in-place unwrap ApduHandler::parse now requires mutable buffer and calls unwrap_in_place reducing allocations simplifying reassembly/decryption
1 parent c855a25 commit 849cc94

File tree

4 files changed

+11
-238
lines changed

4 files changed

+11
-238
lines changed

src/dlms_parser/apdu_handler.cpp

Lines changed: 5 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -5,169 +5,15 @@
55

66
namespace dlms_parser {
77

8-
bool ApduHandler::parse(const uint8_t* buf, size_t len, AxdrPayloadCallback cb) const {
9-
for (size_t pos = 0; pos < len; pos++) {
10-
const uint8_t tag = buf[pos];
11-
const uint8_t* rest = buf + pos + 1;
12-
const size_t rest_len = len - pos - 1;
13-
14-
if (tag == DLMS_APDU_GENERAL_BLOCK_TRANSFER) {
15-
Logger::log(LogLevel::DEBUG, "Found General-Block-Transfer (0xE0) at offset %zu", pos);
16-
return parse_general_block_transfer_(buf + pos, len - pos, cb);
17-
}
18-
if (tag == DLMS_APDU_DATA_NOTIFICATION) {
19-
Logger::log(LogLevel::DEBUG, "Found DATA-NOTIFICATION (0x0F) at offset %zu", pos);
20-
return parse_data_notification_(rest, rest_len, cb);
21-
}
22-
if (tag == DLMS_APDU_GENERAL_GLO_CIPHERING || tag == DLMS_APDU_GENERAL_DED_CIPHERING) {
23-
Logger::log(LogLevel::DEBUG, "Found ciphered APDU (0x%02X) at offset %zu", tag, pos);
24-
return parse_ciphered_apdu_(rest, rest_len, tag, cb);
25-
}
26-
if (tag == DLMS_DATA_TYPE_ARRAY || tag == DLMS_DATA_TYPE_STRUCTURE) {
27-
Logger::log(LogLevel::DEBUG, "Found raw AXDR %s (0x%02X) at offset %zu - no APDU wrapper",
28-
tag == DLMS_DATA_TYPE_ARRAY ? "ARRAY" : "STRUCTURE", tag, pos);
29-
cb(buf + pos, len - pos);
30-
return true;
31-
}
8+
bool ApduHandler::parse(uint8_t* buf, size_t len, AxdrPayloadCallback cb) const {
9+
UnwrapResult result = unwrap_in_place(buf, len);
10+
if (result.length > 0) {
11+
cb(buf + result.offset, result.length);
12+
return true;
3213
}
33-
34-
Logger::log(LogLevel::WARNING, "No supported APDU tag found in buffer");
3514
return false;
3615
}
3716

38-
bool ApduHandler::parse_data_notification_(const uint8_t* buf, size_t len,
39-
AxdrPayloadCallback cb) const {
40-
if (len < 5) {
41-
Logger::log(LogLevel::WARNING, "DATA-NOTIFICATION payload too short (%zu bytes)", len);
42-
return false;
43-
}
44-
45-
size_t pos = 0;
46-
47-
// Long-Invoke-ID-And-Priority (4 bytes)
48-
pos += 4;
49-
50-
// Date-Time presence flag (1 byte): 0x00 = absent, anything else = 12-byte datetime follows
51-
const uint8_t has_datetime = buf[pos++];
52-
if (has_datetime != 0x00) {
53-
Logger::log(LogLevel::VERBOSE, "Datetime flag 0x%02X - skipping 12-byte datetime", has_datetime);
54-
if (pos + 12 > len) {
55-
Logger::log(LogLevel::WARNING, "Buffer too short to skip datetime object");
56-
return false;
57-
}
58-
pos += 12;
59-
}
60-
61-
if (pos >= len) {
62-
Logger::log(LogLevel::WARNING, "No AXDR payload after DATA-NOTIFICATION header");
63-
return false;
64-
}
65-
66-
cb(buf + pos, len - pos);
67-
return true;
68-
}
69-
70-
bool ApduHandler::parse_ciphered_apdu_(const uint8_t* buf, size_t len, uint8_t /*tag*/,
71-
AxdrPayloadCallback cb) const {
72-
if (!decryptor_ || !decryptor_->has_key()) {
73-
Logger::log(LogLevel::WARNING, "Encrypted APDU received but no decryption key is set");
74-
return false;
75-
}
76-
77-
size_t pos = 0;
78-
79-
// System title length byte (must be 8)
80-
if (pos >= len || buf[pos] != DLMS_SYSTITLE_LENGTH) {
81-
Logger::log(LogLevel::WARNING, "Unexpected system title length byte: 0x%02X",
82-
pos < len ? buf[pos] : 0xFF);
83-
return false;
84-
}
85-
pos++;
86-
87-
// System title (8 bytes) → first 8 bytes of the 12-byte IV
88-
if (pos + DLMS_SYSTITLE_LENGTH > len) return false;
89-
uint8_t iv[DLMS_IV_LENGTH];
90-
std::memcpy(iv, buf + pos, DLMS_SYSTITLE_LENGTH);
91-
pos += DLMS_SYSTITLE_LENGTH;
92-
93-
// BER length
94-
const uint32_t cipher_len = utils::read_ber_length(buf, pos, len);
95-
if (cipher_len == 0) return false;
96-
97-
// Security control byte (1 byte, informational — skip)
98-
if (pos >= len) return false;
99-
pos++;
100-
101-
// Frame counter (4 bytes) → last 4 bytes of IV
102-
if (pos + DLMS_FRAME_COUNTER_LENGTH > len) return false;
103-
std::memcpy(iv + DLMS_SYSTITLE_LENGTH, buf + pos, DLMS_FRAME_COUNTER_LENGTH);
104-
pos += DLMS_FRAME_COUNTER_LENGTH;
105-
106-
// The length field includes the security control byte and frame counter (5 bytes total)
107-
if (cipher_len < DLMS_LENGTH_CORRECTION) {
108-
Logger::log(LogLevel::WARNING, "Cipher length field too small: %u", cipher_len);
109-
return false;
110-
}
111-
const uint32_t payload_len = cipher_len - DLMS_LENGTH_CORRECTION;
112-
113-
if (pos + payload_len > len) {
114-
Logger::log(LogLevel::WARNING, "Buffer too short for ciphertext (need %u, have %zu)", payload_len,
115-
len - pos);
116-
return false;
117-
}
118-
119-
std::vector<uint8_t> plain;
120-
if (!decryptor_->decrypt(iv, buf + pos, payload_len, plain)) {
121-
Logger::log(LogLevel::ERROR, "Decryption failed");
122-
return false;
123-
}
124-
125-
// Decrypted payload begins with the inner APDU tag (typically 0x0F) — recurse
126-
return parse(plain.data(), plain.size(), cb);
127-
}
128-
129-
bool ApduHandler::parse_general_block_transfer_(const uint8_t* buf, size_t len,
130-
AxdrPayloadCallback cb) const {
131-
// GBT block format: E0 [ctrl:1] [block_num:2] [block_num_ack:2] [BER_len] [data...]
132-
// Reassemble all blocks, then recurse into parse().
133-
std::vector<uint8_t> reassembled;
134-
size_t pos = 0;
135-
136-
while (pos < len && buf[pos] == DLMS_APDU_GENERAL_BLOCK_TRANSFER) {
137-
if (pos + 6 > len) {
138-
Logger::log(LogLevel::WARNING, "GBT: truncated block header at offset %zu", pos);
139-
return false;
140-
}
141-
const uint8_t ctrl = buf[pos + 1];
142-
const bool is_last = (ctrl & 0x80U) != 0;
143-
const uint16_t block_num = static_cast<uint16_t>(buf[pos + 2] << 8 | buf[pos + 3]);
144-
// pos+4..pos+5: block_num_ack (skip)
145-
size_t ber_pos = pos + 6;
146-
const uint32_t block_len = utils::read_ber_length(buf, ber_pos, len);
147-
148-
if (ber_pos + block_len > len) {
149-
Logger::log(LogLevel::WARNING, "GBT: block %u truncated (need %u, have %zu)",
150-
block_num, block_len, len - ber_pos);
151-
return false;
152-
}
153-
154-
Logger::log(LogLevel::DEBUG, "GBT block %u: %u bytes%s", block_num, block_len,
155-
is_last ? " (last)" : "");
156-
reassembled.insert(reassembled.end(), buf + ber_pos, buf + ber_pos + block_len);
157-
pos = ber_pos + block_len;
158-
159-
if (is_last) break;
160-
}
161-
162-
if (reassembled.empty()) {
163-
Logger::log(LogLevel::WARNING, "GBT: no payload after reassembly");
164-
return false;
165-
}
166-
167-
Logger::log(LogLevel::DEBUG, "GBT: reassembled %zu bytes from blocks", reassembled.size());
168-
return parse(reassembled.data(), reassembled.size(), cb);
169-
}
170-
17117
// ---------------------------------------------------------------------------
17218
// In-place unwrap: sequential pipeline replacing recursive parse().
17319
// Scans for known APDU tag, transforms in-place, loops until AXDR is exposed.

src/dlms_parser/apdu_handler.h

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,18 @@ using AxdrPayloadCallback = std::function<void(const uint8_t* axdr, size_t len)>
1212

1313
// Scans a buffer byte-by-byte for the first recognized DLMS APDU tag.
1414
// Unknown leading bytes are skipped. Recognized tags:
15-
// 0xE0 General-Block-Transfer : reassembles numbered blocks, then recurses
15+
// 0xE0 General-Block-Transfer : reassembles numbered blocks
1616
// 0x0F DATA-NOTIFICATION : strips Long-Invoke-ID and optional datetime header
17-
// 0xDB General-Glo-Ciphering : decrypts with GcmDecryptor, then recurses
18-
// 0xDF General-Ded-Ciphering : decrypts with GcmDecryptor, then recurses
17+
// 0xDB General-Glo-Ciphering : decrypts with GcmDecryptor
18+
// 0xDF General-Ded-Ciphering : decrypts with GcmDecryptor
1919
// 0x01 / 0x02 raw ARRAY/STRUCT : no APDU wrapper (e.g. HDLC/Aidon)
2020
class ApduHandler {
2121
public:
2222
void set_decryptor(GcmDecryptor* d) { decryptor_ = d; }
2323

2424
// Fires cb exactly once on success with the raw AXDR payload span.
25-
bool parse(const uint8_t* buf, size_t len, AxdrPayloadCallback cb) const;
25+
// Requires a mutable buffer for in-place decryption and reassembly.
26+
bool parse(uint8_t* buf, size_t len, AxdrPayloadCallback cb) const;
2627

2728
// In-place unwrap: transforms buf in a loop (GBT→decrypt→strip header).
2829
// Returns the offset and length of the AXDR payload within buf.
@@ -31,10 +32,6 @@ class ApduHandler {
3132
UnwrapResult unwrap_in_place(uint8_t* buf, size_t len) const;
3233

3334
private:
34-
bool parse_data_notification_(const uint8_t* buf, size_t len, AxdrPayloadCallback cb) const;
35-
bool parse_ciphered_apdu_(const uint8_t* buf, size_t len, uint8_t tag, AxdrPayloadCallback cb) const;
36-
bool parse_general_block_transfer_(const uint8_t* buf, size_t len, AxdrPayloadCallback cb) const;
37-
3835
GcmDecryptor* decryptor_{nullptr};
3936
};
4037

src/dlms_parser/gcm_decryptor.cpp

Lines changed: 1 addition & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -26,73 +26,8 @@ void GcmDecryptor::set_key(const std::vector<uint8_t>& key) {
2626
}
2727
}
2828

29-
bool GcmDecryptor::decrypt(const uint8_t* iv, const uint8_t* cipher, size_t cipher_len,
30-
std::vector<uint8_t>& plain_out) const {
31-
plain_out.resize(cipher_len);
32-
33-
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO) || defined(USE_ESP8266) || defined(ESP8266)
34-
35-
std::memcpy(plain_out.data(), cipher, cipher_len);
36-
br_gcm_context gcm_ctx;
37-
br_aes_ct_ctr_keys bc;
38-
br_aes_ct_ctr_init(&bc, key_.data(), key_.size());
39-
br_gcm_init(&gcm_ctx, &bc.vtable, br_ghash_ctmul32);
40-
br_gcm_reset(&gcm_ctx, iv, 12);
41-
br_gcm_flip(&gcm_ctx);
42-
br_gcm_run(&gcm_ctx, 0, plain_out.data(), cipher_len);
43-
return true;
44-
45-
// ESP_IDF_VERSION_VAL(6,0,0) == 0x60000; written as literal to avoid expansion errors when the macro is undefined
46-
#elif defined(USE_ESP32) && defined(ESP_IDF_VERSION) && (ESP_IDF_VERSION >= 0x60000)
47-
48-
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
49-
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
50-
psa_set_key_bits(&attributes, static_cast<psa_key_bits_t>(key_.size() * 8));
51-
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
52-
psa_set_key_algorithm(&attributes, PSA_ALG_GCM);
53-
54-
mbedtls_svc_key_id_t key_id;
55-
bool ok = false;
56-
if (psa_import_key(&attributes, key_.data(), key_.size(), &key_id) == PSA_SUCCESS) {
57-
psa_aead_operation_t op = PSA_AEAD_OPERATION_INIT;
58-
if (psa_aead_decrypt_setup(&op, key_id, PSA_ALG_GCM) == PSA_SUCCESS &&
59-
psa_aead_set_nonce(&op, iv, 12) == PSA_SUCCESS) {
60-
size_t outlen = 0;
61-
if (psa_aead_update(&op, cipher, cipher_len, plain_out.data(), cipher_len, &outlen) == PSA_SUCCESS &&
62-
outlen == cipher_len) {
63-
ok = true;
64-
}
65-
}
66-
psa_aead_abort(&op);
67-
psa_destroy_key(key_id);
68-
}
69-
if (!ok) {
70-
Logger::log(LogLevel::ERROR, "PSA decryption failed");
71-
}
72-
return ok;
73-
74-
#else
75-
76-
mbedtls_gcm_context gcm_ctx;
77-
mbedtls_gcm_init(&gcm_ctx);
78-
mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, key_.data(),
79-
static_cast<unsigned int>(key_.size() * 8));
80-
mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, 12);
81-
size_t outlen = 0;
82-
const int ret = mbedtls_gcm_update(&gcm_ctx, cipher, cipher_len,
83-
plain_out.data(), cipher_len, &outlen);
84-
mbedtls_gcm_free(&gcm_ctx);
85-
if (ret != 0) {
86-
Logger::log(LogLevel::ERROR, "mbedTLS decryption failed: %d", ret);
87-
return false;
88-
}
89-
return true;
90-
91-
#endif
92-
}
93-
9429
size_t GcmDecryptor::decrypt_in_place(const uint8_t* iv, uint8_t* buf,
95-
size_t cipher_offset, size_t cipher_len) const {
30+
size_t cipher_offset, size_t cipher_len) const {
9631

9732
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO) || defined(USE_ESP8266) || defined(ESP8266)
9833

src/dlms_parser/gcm_decryptor.h

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ class GcmDecryptor {
1717

1818
bool has_key() const { return has_key_; }
1919

20-
// Decrypts AES-128-GCM ciphertext. iv must be exactly 12 bytes.
21-
// On success, plain_out is filled with cipher_len decrypted bytes and true is returned.
22-
bool decrypt(const uint8_t* iv, const uint8_t* cipher, size_t cipher_len,
23-
std::vector<uint8_t>& plain_out) const;
24-
2520
// In-place decrypt: reads ciphertext from buf[cipher_offset..], writes plaintext to buf[0..].
2621
// Returns plaintext length, or 0 on failure. iv must be exactly 12 bytes.
2722
size_t decrypt_in_place(const uint8_t* iv, uint8_t* buf, size_t cipher_offset, size_t cipher_len) const;

0 commit comments

Comments
 (0)