|
5 | 5 |
|
6 | 6 | namespace dlms_parser { |
7 | 7 |
|
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; |
32 | 13 | } |
33 | | - |
34 | | - Logger::log(LogLevel::WARNING, "No supported APDU tag found in buffer"); |
35 | 14 | return false; |
36 | 15 | } |
37 | 16 |
|
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 | | - |
171 | 17 | // --------------------------------------------------------------------------- |
172 | 18 | // In-place unwrap: sequential pipeline replacing recursive parse(). |
173 | 19 | // Scans for known APDU tag, transforms in-place, loops until AXDR is exposed. |
|
0 commit comments