@@ -5,16 +5,17 @@ import { DSMRDecodeError, DSMRDecryptionError } from './errors.js';
5
5
* For now this is specific to the luxembourg's smart metering system. (E-Meter P1 Specification)
6
6
* They wrap a DSMR telegram in a custom frame with the following format:
7
7
*
8
- * | Byte | Description | Example |
9
- * | ------ | ------------------- | ----------------------------------- |
10
- * | 0 | SOF | DB (fixed) |
11
- * | 1 | System Title Length | 08 (fixed) |
12
- * | 2-9 | System Title | 00 11 22 33 44 55 66 77 |
13
- * | 10-11 | Length of the frame | 00 11 |
14
- * | 12 | SOF Frame Counter | 30 |
15
- * | 13-16 | Frame Counter | 00 11 22 33 |
16
- * | 17-n | Frame | <Encrypted DSMR frame> |
17
- * | n-n+12 | GCM Tag | 00 11 22 33 44 55 66 77 88 99 AA BB |
8
+ * | Byte | Description | Example |
9
+ * | ------ | -------------------- | ----------------------------------- |
10
+ * | 0 | SOF | DB (fixed) |
11
+ * | 1 | System Title Length | 08 (fixed) |
12
+ * | 2-9 | System Title | 00 11 22 33 44 55 66 77 |
13
+ * | 10 | Content Length Start | 82 (fixed) |
14
+ * | 11-12 | Length of the frame | 00 11 |
15
+ * | 13 | SOF Frame Counter | 30 (fixed) |
16
+ * | 14-17 | Frame Counter | 00 11 22 33 |
17
+ * | 18-n | Frame | <Encrypted DSMR frame> |
18
+ * | n-n+12 | GCM Tag | 00 11 22 33 44 55 66 77 88 99 AA BB |
18
19
*
19
20
* The encrypted DSMR frame is encrypted using AES-128-GCM, and the user can request the encryption
20
21
* key from the utility company. The IV is the concatenation of the system title and the frame
@@ -24,10 +25,13 @@ import { DSMRDecodeError, DSMRDecryptionError } from './errors.js';
24
25
* excluded.
25
26
*/
26
27
27
- export const ENCRYPTED_DSMR_TELEGRAM_SOF = 0xdb ;
28
+ export const ENCRYPTED_DSMR_TELEGRAM_SOF = 0xdb ; // DLMS_COMMAND_GENERAL_GLO_CIPHERING
29
+ export const ENCRYPTED_DSMR_CONTENT_LENGTH_START = 0x82 ; // DLMS type for uint16_t (Big Endian)
30
+ export const ENCRYPTED_DSMR_SECURITY_TYPE = 0x30 ; // DLMS_SECURITY_AUTHENTICATION_ENCRYPTION
28
31
export const ENCRYPTED_DSMR_SYSTEM_TITLE_LEN = 8 ;
29
32
export const ENCRYPTED_DSMR_GCM_TAG_LEN = 12 ;
30
- export const ENCRYPTED_DSMR_HEADER_LEN = 17 ;
33
+ export const ENCRYPTED_DSMR_HEADER_LEN = 18 ;
34
+ export const ENCRYPTION_DEFAULT_AAD = Buffer . from ( '00112233445566778899aabbccddeeff' , 'hex' ) ;
31
35
32
36
/**
33
37
* @param data A buffer that starts with the header (bytes 0-16) of the E-Meter P1 frame
@@ -40,29 +44,43 @@ export const decodeHeader = (data: Buffer) => {
40
44
41
45
let index = 0 ;
42
46
43
- if ( data [ index ++ ] !== ENCRYPTED_DSMR_TELEGRAM_SOF ) {
44
- throw new DSMRDecodeError ( 'Invalid telegram sof' ) ;
47
+ const sof = data [ index ++ ] ;
48
+ if ( sof !== ENCRYPTED_DSMR_TELEGRAM_SOF ) {
49
+ throw new DSMRDecodeError ( `Invalid telegram sof 0x${ sof . toString ( 16 ) } ` ) ;
45
50
}
46
51
47
- if ( data [ index ++ ] !== ENCRYPTED_DSMR_SYSTEM_TITLE_LEN ) {
48
- throw new DSMRDecodeError ( 'Invalid system title length' ) ;
52
+ const systemTitleLen = data [ index ++ ] ;
53
+ if ( systemTitleLen !== ENCRYPTED_DSMR_SYSTEM_TITLE_LEN ) {
54
+ throw new DSMRDecodeError ( `Invalid system title length 0x${ systemTitleLen . toString ( 16 ) } ` ) ;
49
55
}
50
56
51
57
const systemTitle = data . subarray ( index , index + ENCRYPTED_DSMR_SYSTEM_TITLE_LEN ) ;
52
58
index += ENCRYPTED_DSMR_SYSTEM_TITLE_LEN ;
53
- const contentLength = data . readUInt16LE ( index ) ;
59
+
60
+ const contentLengthStart = data [ index ++ ] ;
61
+ if ( contentLengthStart !== ENCRYPTED_DSMR_CONTENT_LENGTH_START ) {
62
+ throw new DSMRDecodeError (
63
+ `Invalid content length start byte 0x${ contentLengthStart . toString ( 16 ) } ` ,
64
+ ) ;
65
+ }
66
+
67
+ // The entire header is 18 bytes long, but for some reason the content length uses 17 as
68
+ // length for the header. Maybe they don't include the SOF byte?
69
+ const contentLength = data . readUInt16BE ( index ) + 1 - ENCRYPTED_DSMR_HEADER_LEN ;
54
70
index += 2 ;
55
71
56
- // According to the documentation, this should be 0x30, but it often is not.
57
- const sofFrameCounter = data [ index ++ ] ;
72
+ const securityType = data [ index ++ ] ;
73
+ if ( securityType !== ENCRYPTED_DSMR_SECURITY_TYPE ) {
74
+ throw new DSMRDecodeError ( `Invalid frame counter 0x${ securityType . toString ( 16 ) } ` ) ;
75
+ }
58
76
59
77
const frameCounter = data . subarray ( index , index + 4 ) ;
60
78
index += 4 ;
61
79
62
80
return {
63
81
systemTitle,
64
82
frameCounter,
65
- sofFrameCounter ,
83
+ securityType ,
66
84
contentLength,
67
85
} ;
68
86
} ;
@@ -77,7 +95,10 @@ export const decodeFooter = (data: Buffer, header: ReturnType<typeof decodeHeade
77
95
}
78
96
79
97
return {
80
- gcmTag : data . subarray ( header . contentLength , header . contentLength + ENCRYPTED_DSMR_GCM_TAG_LEN ) ,
98
+ gcmTag : data . subarray (
99
+ ENCRYPTED_DSMR_HEADER_LEN + header . contentLength ,
100
+ ENCRYPTED_DSMR_HEADER_LEN + header . contentLength + ENCRYPTED_DSMR_GCM_TAG_LEN ,
101
+ ) ,
81
102
} ;
82
103
} ;
83
104
@@ -88,6 +109,7 @@ export const decryptFrameContents = ({
88
109
footer,
89
110
key,
90
111
encoding,
112
+ additionalAuthenticatedData,
91
113
} : {
92
114
/** The encrypted DSMR frame */
93
115
data : Buffer ;
@@ -96,42 +118,93 @@ export const decryptFrameContents = ({
96
118
/** The decoded footer (use {@link decodeFooter}) */
97
119
footer : ReturnType < typeof decodeFooter > ;
98
120
/** The encryption key */
99
- key : string ;
121
+ key : Buffer ;
100
122
encoding : BufferEncoding ;
123
+ /** Optional additional authenticated data (AAD) to be used in the decryption. */
124
+ additionalAuthenticatedData ?: Buffer ;
101
125
} ) => {
102
- if ( data . length !== header . contentLength - ENCRYPTED_DSMR_HEADER_LEN ) {
103
- throw new Error (
104
- `Invalid frame length got ${ data . length } expected ${ header . contentLength - ENCRYPTED_DSMR_HEADER_LEN } ` ,
105
- ) ;
126
+ if ( data . length !== header . contentLength ) {
127
+ throw new Error ( `Invalid frame length got ${ data . length } expected ${ header . contentLength } ` ) ;
128
+ }
129
+
130
+ if ( additionalAuthenticatedData ?. length == 16 ) {
131
+ additionalAuthenticatedData = Buffer . concat ( [ Buffer . from ( [ 0x30 ] ) , additionalAuthenticatedData ] ) ;
106
132
}
107
133
108
134
const iv = Buffer . concat ( [ header . systemTitle , header . frameCounter ] ) ;
135
+ let cipher : crypto . DecipherGCM ;
136
+ let content = '' ;
109
137
110
- // Wrap in try-catch to throw a DSMRDecryptionError instead of a generic error.
138
+ // 1: decrypt the frame, this will only throw if the key, iv or AAD are not
139
+ // correct due to their format. `cipher.update` will never throw, but if the key/iv/aad
140
+ // are not valid it may return gibberish.
111
141
try {
112
- const cipher = crypto . createDecipheriv ( 'aes-128-gcm' , key , iv , {
142
+ cipher = crypto . createDecipheriv ( 'aes-128-gcm' , key , iv , {
113
143
authTagLength : ENCRYPTED_DSMR_GCM_TAG_LEN ,
114
144
} ) ;
145
+ cipher . setAutoPadding ( false ) ;
115
146
cipher . setAuthTag ( footer . gcmTag ) ;
116
147
117
- return cipher . update ( data , undefined , encoding ) + cipher . final ( encoding ) ;
148
+ if ( additionalAuthenticatedData ) {
149
+ cipher . setAAD ( additionalAuthenticatedData ) ;
150
+ }
151
+
152
+ content += cipher . update ( data , undefined , encoding ) ;
118
153
} catch ( error ) {
119
- throw new DSMRDecryptionError ( error ) ;
154
+ return {
155
+ content,
156
+ error : new DSMRDecryptionError ( error ) ,
157
+ } ;
120
158
}
159
+
160
+ // 2: call final on the frame. This will check the AAD/iv/key.
161
+ // When either of these are invalid, it will throw an "Unsupported state or unable to authenticate data" error.
162
+ // If the AAD is invalid, but the key/iv are valid the content can still be a valid DSMR frame!
163
+ try {
164
+ content += cipher . final ( encoding ) ;
165
+ } catch ( error ) {
166
+ return {
167
+ content,
168
+ error : new DSMRDecryptionError ( error ) ,
169
+ } ;
170
+ }
171
+
172
+ return {
173
+ content,
174
+ } ;
121
175
} ;
122
176
123
177
/** Decrypts a full encrypted DSMR frame */
124
178
export const decryptFrame = ( {
125
179
data,
126
180
key,
127
181
encoding,
182
+ additionalAuthenticatedData,
128
183
} : {
129
184
data : Buffer ;
130
- key : string ;
185
+ key : Buffer ;
186
+ additionalAuthenticatedData ?: Buffer ;
131
187
encoding : BufferEncoding ;
132
188
} ) => {
133
189
const header = decodeHeader ( data ) ;
134
190
const footer = decodeFooter ( data , header ) ;
135
- const content = data . subarray ( ENCRYPTED_DSMR_HEADER_LEN , header . contentLength ) ;
136
- return decryptFrameContents ( { data : content , header, footer, key, encoding } ) ;
191
+ const encryptedContent = data . subarray (
192
+ ENCRYPTED_DSMR_HEADER_LEN ,
193
+ ENCRYPTED_DSMR_HEADER_LEN + header . contentLength ,
194
+ ) ;
195
+ const { content, error } = decryptFrameContents ( {
196
+ data : encryptedContent ,
197
+ header,
198
+ footer,
199
+ key,
200
+ additionalAuthenticatedData,
201
+ encoding,
202
+ } ) ;
203
+
204
+ return {
205
+ header,
206
+ footer,
207
+ content,
208
+ error,
209
+ } ;
137
210
} ;
0 commit comments