@matter/general 0.14.1-alpha.0-20250607-a93593303 → 0.15.0-alpha.0-20250612-ddd428561

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/dist/cjs/codec/DerCodec.d.ts +12 -17
  2. package/dist/cjs/codec/DerCodec.d.ts.map +1 -1
  3. package/dist/cjs/codec/DerCodec.js +90 -51
  4. package/dist/cjs/codec/DerCodec.js.map +1 -1
  5. package/dist/cjs/codec/DerTypes.js +1 -1
  6. package/dist/cjs/codec/DnsCodec.d.ts +5 -5
  7. package/dist/cjs/crypto/Crypto.d.ts +111 -62
  8. package/dist/cjs/crypto/Crypto.d.ts.map +1 -1
  9. package/dist/cjs/crypto/Crypto.js +92 -31
  10. package/dist/cjs/crypto/Crypto.js.map +1 -1
  11. package/dist/cjs/crypto/CryptoError.d.ts +32 -0
  12. package/dist/cjs/crypto/CryptoError.d.ts.map +1 -0
  13. package/dist/cjs/crypto/CryptoError.js +44 -0
  14. package/dist/cjs/crypto/CryptoError.js.map +6 -0
  15. package/dist/cjs/crypto/Key.d.ts +2 -2
  16. package/dist/cjs/crypto/Key.d.ts.map +1 -1
  17. package/dist/cjs/crypto/Key.js +15 -16
  18. package/dist/cjs/crypto/Key.js.map +1 -1
  19. package/dist/cjs/crypto/Spake2p.js +5 -5
  20. package/dist/cjs/crypto/Spake2p.js.map +1 -1
  21. package/dist/cjs/crypto/StandardCrypto.d.ts +33 -0
  22. package/dist/cjs/crypto/StandardCrypto.d.ts.map +1 -0
  23. package/dist/cjs/crypto/StandardCrypto.js +208 -0
  24. package/dist/cjs/crypto/StandardCrypto.js.map +6 -0
  25. package/dist/cjs/crypto/aes/Aes.d.ts +21 -0
  26. package/dist/cjs/crypto/aes/Aes.d.ts.map +1 -0
  27. package/dist/cjs/crypto/aes/Aes.js +132 -0
  28. package/dist/cjs/crypto/aes/Aes.js.map +6 -0
  29. package/dist/cjs/crypto/aes/Ccm.d.ts +71 -0
  30. package/dist/cjs/crypto/aes/Ccm.d.ts.map +1 -0
  31. package/dist/cjs/crypto/aes/Ccm.js +194 -0
  32. package/dist/cjs/crypto/aes/Ccm.js.map +6 -0
  33. package/dist/cjs/crypto/aes/WordArray.d.ts +30 -0
  34. package/dist/cjs/crypto/aes/WordArray.d.ts.map +1 -0
  35. package/dist/cjs/crypto/aes/WordArray.js +91 -0
  36. package/dist/cjs/crypto/aes/WordArray.js.map +6 -0
  37. package/dist/cjs/crypto/index.d.ts +3 -0
  38. package/dist/cjs/crypto/index.d.ts.map +1 -1
  39. package/dist/cjs/crypto/index.js +3 -0
  40. package/dist/cjs/crypto/index.js.map +1 -1
  41. package/dist/cjs/crypto/nonentropic.d.ts +16 -0
  42. package/dist/cjs/crypto/nonentropic.d.ts.map +1 -0
  43. package/dist/cjs/crypto/nonentropic.js +70 -0
  44. package/dist/cjs/crypto/nonentropic.js.map +6 -0
  45. package/dist/cjs/environment/Environment.d.ts.map +1 -1
  46. package/dist/cjs/environment/Environment.js +1 -5
  47. package/dist/cjs/environment/Environment.js.map +1 -1
  48. package/dist/cjs/environment/RuntimeService.d.ts +2 -4
  49. package/dist/cjs/environment/RuntimeService.d.ts.map +1 -1
  50. package/dist/cjs/environment/RuntimeService.js +4 -4
  51. package/dist/cjs/environment/RuntimeService.js.map +1 -1
  52. package/dist/cjs/environment/VariableService.d.ts.map +1 -1
  53. package/dist/cjs/environment/VariableService.js +1 -0
  54. package/dist/cjs/environment/VariableService.js.map +1 -1
  55. package/dist/cjs/log/LogFormat.js +17 -11
  56. package/dist/cjs/log/LogFormat.js.map +1 -1
  57. package/dist/cjs/net/Network.d.ts +0 -1
  58. package/dist/cjs/net/Network.d.ts.map +1 -1
  59. package/dist/cjs/net/Network.js +0 -4
  60. package/dist/cjs/net/Network.js.map +1 -1
  61. package/dist/cjs/time/Time.d.ts.map +1 -1
  62. package/dist/cjs/time/Time.js +2 -2
  63. package/dist/cjs/time/Time.js.map +1 -1
  64. package/dist/cjs/util/Bytes.d.ts +6 -0
  65. package/dist/cjs/util/Bytes.d.ts.map +1 -1
  66. package/dist/cjs/util/Bytes.js +15 -1
  67. package/dist/cjs/util/Bytes.js.map +1 -1
  68. package/dist/cjs/util/DataWriter.d.ts +1 -1
  69. package/dist/cjs/util/DataWriter.js +2 -2
  70. package/dist/cjs/util/DataWriter.js.map +1 -1
  71. package/dist/cjs/util/DeepCopy.js +1 -1
  72. package/dist/cjs/util/DeepCopy.js.map +1 -1
  73. package/dist/cjs/util/GeneratedClass.d.ts +3 -3
  74. package/dist/cjs/util/GeneratedClass.d.ts.map +1 -1
  75. package/dist/cjs/util/GeneratedClass.js +99 -73
  76. package/dist/cjs/util/GeneratedClass.js.map +2 -2
  77. package/dist/cjs/util/Number.d.ts +0 -1
  78. package/dist/cjs/util/Number.d.ts.map +1 -1
  79. package/dist/cjs/util/Number.js +0 -4
  80. package/dist/cjs/util/Number.js.map +1 -1
  81. package/dist/esm/codec/DerCodec.d.ts +12 -17
  82. package/dist/esm/codec/DerCodec.d.ts.map +1 -1
  83. package/dist/esm/codec/DerCodec.js +90 -51
  84. package/dist/esm/codec/DerCodec.js.map +1 -1
  85. package/dist/esm/codec/DerTypes.js +2 -2
  86. package/dist/esm/codec/DnsCodec.d.ts +5 -5
  87. package/dist/esm/crypto/Crypto.d.ts +111 -62
  88. package/dist/esm/crypto/Crypto.d.ts.map +1 -1
  89. package/dist/esm/crypto/Crypto.js +93 -32
  90. package/dist/esm/crypto/Crypto.js.map +1 -1
  91. package/dist/esm/crypto/CryptoError.d.ts +32 -0
  92. package/dist/esm/crypto/CryptoError.d.ts.map +1 -0
  93. package/dist/esm/crypto/CryptoError.js +24 -0
  94. package/dist/esm/crypto/CryptoError.js.map +6 -0
  95. package/dist/esm/crypto/Key.d.ts +2 -2
  96. package/dist/esm/crypto/Key.d.ts.map +1 -1
  97. package/dist/esm/crypto/Key.js +15 -16
  98. package/dist/esm/crypto/Key.js.map +1 -1
  99. package/dist/esm/crypto/Spake2p.js +5 -5
  100. package/dist/esm/crypto/Spake2p.js.map +1 -1
  101. package/dist/esm/crypto/StandardCrypto.d.ts +33 -0
  102. package/dist/esm/crypto/StandardCrypto.d.ts.map +1 -0
  103. package/dist/esm/crypto/StandardCrypto.js +188 -0
  104. package/dist/esm/crypto/StandardCrypto.js.map +6 -0
  105. package/dist/esm/crypto/aes/Aes.d.ts +21 -0
  106. package/dist/esm/crypto/aes/Aes.d.ts.map +1 -0
  107. package/dist/esm/crypto/aes/Aes.js +112 -0
  108. package/dist/esm/crypto/aes/Aes.js.map +6 -0
  109. package/dist/esm/crypto/aes/Ccm.d.ts +71 -0
  110. package/dist/esm/crypto/aes/Ccm.d.ts.map +1 -0
  111. package/dist/esm/crypto/aes/Ccm.js +174 -0
  112. package/dist/esm/crypto/aes/Ccm.js.map +6 -0
  113. package/dist/esm/crypto/aes/WordArray.d.ts +30 -0
  114. package/dist/esm/crypto/aes/WordArray.d.ts.map +1 -0
  115. package/dist/esm/crypto/aes/WordArray.js +71 -0
  116. package/dist/esm/crypto/aes/WordArray.js.map +6 -0
  117. package/dist/esm/crypto/index.d.ts +3 -0
  118. package/dist/esm/crypto/index.d.ts.map +1 -1
  119. package/dist/esm/crypto/index.js +3 -0
  120. package/dist/esm/crypto/index.js.map +1 -1
  121. package/dist/esm/crypto/nonentropic.d.ts +16 -0
  122. package/dist/esm/crypto/nonentropic.d.ts.map +1 -0
  123. package/dist/esm/crypto/nonentropic.js +50 -0
  124. package/dist/esm/crypto/nonentropic.js.map +6 -0
  125. package/dist/esm/environment/Environment.d.ts.map +1 -1
  126. package/dist/esm/environment/Environment.js +1 -5
  127. package/dist/esm/environment/Environment.js.map +1 -1
  128. package/dist/esm/environment/RuntimeService.d.ts +2 -4
  129. package/dist/esm/environment/RuntimeService.d.ts.map +1 -1
  130. package/dist/esm/environment/RuntimeService.js +4 -4
  131. package/dist/esm/environment/RuntimeService.js.map +1 -1
  132. package/dist/esm/environment/VariableService.d.ts.map +1 -1
  133. package/dist/esm/environment/VariableService.js +1 -0
  134. package/dist/esm/environment/VariableService.js.map +1 -1
  135. package/dist/esm/log/LogFormat.js +17 -11
  136. package/dist/esm/log/LogFormat.js.map +1 -1
  137. package/dist/esm/net/Network.d.ts +0 -1
  138. package/dist/esm/net/Network.d.ts.map +1 -1
  139. package/dist/esm/net/Network.js +1 -5
  140. package/dist/esm/net/Network.js.map +1 -1
  141. package/dist/esm/time/Time.d.ts.map +1 -1
  142. package/dist/esm/time/Time.js +2 -2
  143. package/dist/esm/time/Time.js.map +1 -1
  144. package/dist/esm/util/Bytes.d.ts +6 -0
  145. package/dist/esm/util/Bytes.d.ts.map +1 -1
  146. package/dist/esm/util/Bytes.js +15 -1
  147. package/dist/esm/util/Bytes.js.map +1 -1
  148. package/dist/esm/util/DataWriter.d.ts +1 -1
  149. package/dist/esm/util/DataWriter.js +3 -3
  150. package/dist/esm/util/DataWriter.js.map +1 -1
  151. package/dist/esm/util/DeepCopy.js +1 -1
  152. package/dist/esm/util/DeepCopy.js.map +1 -1
  153. package/dist/esm/util/GeneratedClass.d.ts +3 -3
  154. package/dist/esm/util/GeneratedClass.d.ts.map +1 -1
  155. package/dist/esm/util/GeneratedClass.js +97 -71
  156. package/dist/esm/util/GeneratedClass.js.map +2 -2
  157. package/dist/esm/util/Number.d.ts +0 -1
  158. package/dist/esm/util/Number.d.ts.map +1 -1
  159. package/dist/esm/util/Number.js +0 -4
  160. package/dist/esm/util/Number.js.map +1 -1
  161. package/package.json +3 -3
  162. package/src/codec/DerCodec.ts +106 -52
  163. package/src/codec/DerTypes.ts +2 -2
  164. package/src/crypto/Crypto.ts +196 -76
  165. package/src/crypto/CryptoError.ts +32 -0
  166. package/src/crypto/Key.ts +17 -18
  167. package/src/crypto/Spake2p.ts +5 -5
  168. package/src/crypto/StandardCrypto.ts +252 -0
  169. package/src/crypto/aes/Aes.ts +210 -0
  170. package/src/crypto/aes/Ccm.ts +350 -0
  171. package/src/crypto/aes/README.md +4 -0
  172. package/src/crypto/aes/WordArray.ts +105 -0
  173. package/src/crypto/index.ts +3 -0
  174. package/src/crypto/nonentropic.ts +65 -0
  175. package/src/environment/Environment.ts +1 -6
  176. package/src/environment/RuntimeService.ts +5 -5
  177. package/src/environment/VariableService.ts +1 -0
  178. package/src/log/LogFormat.ts +19 -11
  179. package/src/net/Network.ts +1 -7
  180. package/src/time/Time.ts +4 -4
  181. package/src/util/Bytes.ts +19 -0
  182. package/src/util/DataWriter.ts +3 -3
  183. package/src/util/DeepCopy.ts +2 -2
  184. package/src/util/GeneratedClass.ts +161 -102
  185. package/src/util/Number.ts +0 -4
@@ -0,0 +1,350 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Project CHIP Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * NIST: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38c.pdf
9
+ *
10
+ * SJCL: https://github.com/bitwiseshiftleft/sjcl/blob/master/core/ccm.js
11
+ *
12
+ * OpenSSL: https://github.com/openssl/openssl/blob/master/crypto/modes/ccm128.c
13
+ */
14
+
15
+ import { CRYPTO_AEAD_MIC_LENGTH_BYTES, CRYPTO_AEAD_NONCE_LENGTH_BYTES } from "#crypto/CryptoConstants.js";
16
+ import { CryptoInputError } from "#crypto/CryptoError.js";
17
+ import { Aes } from "./Aes.js";
18
+ import { WordArray } from "./WordArray.js";
19
+
20
+ /**
21
+ * WARNING: Unaudited. Consider platform replacement if available.
22
+ *
23
+ * This AES-CCM implementation is tailored for Matter:
24
+ *
25
+ * * Only supports 2-byte length
26
+ *
27
+ * * Only supports 13-byte nonce
28
+ *
29
+ * * Stores the MIC in the ciphertext buffer following the ciphertext
30
+ *
31
+ * * Our AES implementation supports multiple key sizes but only 16 bytes are legal
32
+ *
33
+ * We take a few approaches to improve performance:
34
+ *
35
+ * * Uses singletons for temporary working buffers to avoid GC
36
+ *
37
+ * * Uses Uint8Array, Int32Array and DataView depending on which is most efficient while addressing platform byte order
38
+ *
39
+ * * Performs data conversion one block at a time rather than converting entire input/output buffer
40
+ *
41
+ * * Functions are monomorphic and should JIT well
42
+ *
43
+ * Implementation notes:
44
+ *
45
+ * * Data operations operate on 128-bit blocks, either as bytes or as 4 32-bit words in platform byte order. We share
46
+ * underlying memory for both formats, but on little-endian platforms they are not directly interchangeable without a
47
+ * round-trip through a DataView
48
+ *
49
+ * * We encode words as a signed Int32Array because JS bit operations operate on signed 32-bit integers and a
50
+ * Uint32Array would require manually casting from signed to unsigned
51
+ *
52
+ * * Use of singleton buffers require this code to be synchronous. If that were to change we would need to convert to a
53
+ * buffer pool
54
+ *
55
+ * * Some functions only modify singleton buffers and thus do not directly return a value
56
+ *
57
+ * * We use {@link DataView} to read/write words where possible. However, byte buffers may not align to word
58
+ * boundaries. We detect this case and manually read/write the last word
59
+ */
60
+ export function Ccm(key: Uint8Array) {
61
+ const aes = Aes(key);
62
+
63
+ return {
64
+ encrypt(input: Ccm.EncryptInput): Uint8Array {
65
+ validateNonceAndAdata(input);
66
+
67
+ const ptLength = input.pt.length;
68
+ if (ptLength > MAX_PLAINTEXT_LENGTH) {
69
+ throw new CryptoInputError(
70
+ `Cannot encrypt plaintext exceeding maximum length of ${MAX_PLAINTEXT_LENGTH}`,
71
+ );
72
+ }
73
+
74
+ // Create view for loading plaintext words in platform byte order
75
+ const ptView = new DataView(input.pt.buffer);
76
+
77
+ // Allocate ciphertext output buffer
78
+ const ct = new Uint8Array(ptLength + CRYPTO_AEAD_MIC_LENGTH_BYTES);
79
+ const ctView = new DataView(ct.buffer);
80
+
81
+ // Compute MIC using CBC-MAC
82
+ cbcMac(input, ptView, ptLength);
83
+
84
+ // Encrypt using CTR mode
85
+ ctr(input, ptView, ctView, ptLength, computedMic);
86
+
87
+ for (let i = 0; i < computedMic.words.length; i++) {
88
+ ctView.setInt32(input.pt.length + i * 4, computedMic.words[i]);
89
+ }
90
+
91
+ return ct;
92
+ },
93
+
94
+ decrypt(input: Ccm.DecryptInput): Uint8Array {
95
+ validateNonceAndAdata(input);
96
+
97
+ if (input.ct.length > MAX_CIPHERTEXT_LENGTH) {
98
+ throw new CryptoInputError(
99
+ `Cannot decrypt ciphertext longer than maximum length of ${MAX_CIPHERTEXT_LENGTH}`,
100
+ );
101
+ }
102
+
103
+ const ptLength = input.ct.length - CRYPTO_AEAD_MIC_LENGTH_BYTES;
104
+
105
+ if (ptLength < 0) {
106
+ throw new CryptoInputError(
107
+ `Cannot decrypt ciphertext shorter than minimum length of ${CRYPTO_AEAD_MIC_LENGTH_BYTES}`,
108
+ );
109
+ }
110
+
111
+ // Extract ciphertext (zero copy using a DataView) and MIC from input ciphertext
112
+ const ctView = new DataView(input.ct.buffer, input.ct.byteOffset, ptLength);
113
+ WordArray.bytesToBlock(
114
+ new DataView(input.ct.buffer, input.ct.byteOffset, input.ct.byteLength),
115
+ inputMic.words,
116
+ ptLength,
117
+ );
118
+
119
+ // Allocate plaintext output buffer
120
+ const pt = new Uint8Array(ptLength);
121
+ const ptView = new DataView(pt.buffer);
122
+
123
+ // Decrypt using CTR mode
124
+ ctr(input, ctView, ptView, ptLength, inputMic);
125
+
126
+ // Compute MIC using CBC-MAC
127
+ cbcMac(input, ptView, ptLength);
128
+
129
+ for (let i = 0; i < computedMic.words.length; i++) {
130
+ if (inputMic.words[i] !== computedMic.words[i]) {
131
+ throw new CryptoInputError("Message authentication failed due to invalid signature");
132
+ }
133
+ }
134
+
135
+ return pt;
136
+ },
137
+ };
138
+
139
+ /**
140
+ * CBC-MAC MIC computation.
141
+ *
142
+ * This includes header generation then CBC-MAC passes on the header, adata and plaintext.
143
+ *
144
+ * Writes to {@link computedMic}.
145
+ */
146
+ function cbcMac(input: Ccm.Input, pt: DataView, ptLength: number) {
147
+ const adataLength = input.adata?.length;
148
+
149
+ // Create the header; first add the flag byte and nonce
150
+ computedMic.bytes[0] =
151
+ (adataLength ? 1 << 6 : 0) | ((CRYPTO_AEAD_MIC_LENGTH_BYTES - 2) << 2) | (BYTES_IN_LENGTH - 1);
152
+ computedMic.bytes.set(input.nonce, 1);
153
+
154
+ // Convert header to platform byte order
155
+ WordArray.bytesToBlock(computedMic.view, computedMic.words); // Convert to platform byte order
156
+
157
+ // Add plaintext length (must occur after conversion to platform byte order)
158
+ computedMic.words[3] = (computedMic.words[3] & 0xffff0000) | ptLength;
159
+
160
+ // Start computation
161
+ aes.encrypt(computedMic.words);
162
+
163
+ // Add adata
164
+ if (adataLength) {
165
+ // Add length (2 bytes) + first 14 bytes of adata
166
+ tempBlock1.view.setInt16(0, input.adata!.length);
167
+ for (let i = 0; i < 14; i++) {
168
+ tempBlock1.bytes[i + 2] = i < adataLength ? input.adata![i] : 0;
169
+ }
170
+
171
+ // Flip to platform byte order and add
172
+ WordArray.bytesToBlock(tempBlock1.view, tempBlock1.words);
173
+ add();
174
+
175
+ // Add remainder of adata
176
+ if (adataLength > 14) {
177
+ const adataView = new DataView(input.adata!.buffer);
178
+ for (let i = 14; i < adataLength; i += 16) {
179
+ WordArray.bytesToBlock(adataView, tempBlock1.words, i);
180
+ add();
181
+ }
182
+ }
183
+ }
184
+
185
+ // Add plaintext
186
+ if (ptLength) {
187
+ for (let i = 0; i < ptLength; i += 16) {
188
+ WordArray.bytesToBlock(pt, tempBlock1.words, i);
189
+ add();
190
+ }
191
+ }
192
+
193
+ function add() {
194
+ computedMic.words[0] ^= tempBlock1.words[0];
195
+ computedMic.words[1] ^= tempBlock1.words[1];
196
+ computedMic.words[2] ^= tempBlock1.words[2];
197
+ computedMic.words[3] ^= tempBlock1.words[3];
198
+ aes.encrypt(computedMic.words);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * A CTR mode implementation specialized for CCM.
204
+ *
205
+ * Used both for encryption and decryption.
206
+ *
207
+ * Encrypts/decrypts {@link ptLength} bytes of {@link input} into {@link output}. Also encrypts/decrypts
208
+ * {@link mic} in place.
209
+ */
210
+ function ctr(input: Ccm.Input, from: DataView, to: DataView, ptLength: number, mic: SingletonBuffer) {
211
+ // Initialize the counter (big endian)
212
+ tempBlock1.bytes[0] = BYTES_IN_LENGTH - 1;
213
+ tempBlock1.bytes.set(input.nonce, 1); // Bytes 1 - 13
214
+ tempBlock1.bytes[14] = 0;
215
+ tempBlock1.bytes[15] = 0;
216
+
217
+ // Change to platform byte order for CTR computation
218
+ WordArray.bytesToBlock(tempBlock1.view, ctrBlock.words);
219
+
220
+ // Encrypt the MIC
221
+ aes.encrypt(ctrBlock.words, tempBlock1.words);
222
+ mic.words[0] ^= tempBlock1.words[0];
223
+ mic.words[1] ^= tempBlock1.words[1];
224
+ mic.words[2] ^= tempBlock1.words[2];
225
+ mic.words[3] ^= tempBlock1.words[3];
226
+
227
+ // Process the data
228
+ for (let i = 0; i < ptLength; ) {
229
+ ctrBlock.words[3]++;
230
+
231
+ // Convert input block to platform-endian words
232
+ WordArray.bytesToBlock(from, tempBlock1.words, i);
233
+
234
+ // Encrypt CTR
235
+ aes.encrypt(ctrBlock.words, tempBlock2.words);
236
+
237
+ // Copy block to output buffer
238
+ for (let j = 0; j < 4 && i < ptLength; j++, i += 4) {
239
+ const tempWord = tempBlock2.words[j];
240
+ if (i + 4 < ptLength) {
241
+ // Full word
242
+ to.setInt32(i, from.getInt32(i) ^ tempWord);
243
+ } else {
244
+ // Partial word
245
+ const partial = WordArray.readPartialWord(from, i, ptLength - i) ^ tempWord;
246
+ WordArray.writePartialWord(partial, to, i, ptLength - i);
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ export namespace Ccm {
254
+ export interface Input {
255
+ nonce: Uint8Array;
256
+ adata: Uint8Array | undefined; // Do not use ? to ensure object shape remains stable
257
+ }
258
+
259
+ export interface EncryptInput extends Input {
260
+ /**
261
+ * Plaintext
262
+ */
263
+ pt: Uint8Array;
264
+ }
265
+
266
+ export interface DecryptInput extends Input {
267
+ /**
268
+ * Ciphertext + tag
269
+ */
270
+ ct: Uint8Array;
271
+ }
272
+ }
273
+
274
+ export const BYTES_IN_LENGTH = 2; // NIST q parameter, length of payload length; only 2 bytes supported by Matter
275
+ export const MAX_CIPHERTEXT_LENGTH = Math.pow(2, BYTES_IN_LENGTH * 8);
276
+ export const MAX_PLAINTEXT_LENGTH = MAX_CIPHERTEXT_LENGTH - CRYPTO_AEAD_MIC_LENGTH_BYTES;
277
+
278
+ /**
279
+ * A buffer used for encryption.
280
+ *
281
+ * We encrypt/decrypt synchronously, so it is safe to use singletons to avoid GC hit.
282
+ */
283
+ class SingletonBuffer {
284
+ #words?: Int32Array;
285
+ #bytes?: Uint8Array;
286
+ #view?: DataView;
287
+
288
+ get words() {
289
+ if (this.#words === undefined) {
290
+ this.#words = new Int32Array(4);
291
+ }
292
+ return this.#words;
293
+ }
294
+
295
+ get bytes() {
296
+ if (this.#bytes === undefined) {
297
+ this.#bytes = new Uint8Array(this.words.buffer);
298
+ }
299
+ return this.#bytes;
300
+ }
301
+
302
+ /**
303
+ * The word and byte views of the buffer above are insufficient because we must account for platform endianness. So
304
+ * we also make a DataView available.
305
+ */
306
+ get view() {
307
+ if (this.#view === undefined) {
308
+ this.#view = new DataView(this.words.buffer);
309
+ }
310
+ return this.#view;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * The "MIC" is the 16-byte authentication tag, called the "MAC" in the NIST spec and "tag" in SJCL. We store
316
+ * concatenated to the end of the ciphertext as required by Matter.
317
+ *
318
+ * This buffer is for the MIC we compute from the input plaintext.
319
+ */
320
+ const computedMic = new SingletonBuffer();
321
+
322
+ /**
323
+ * This buffer is for the MIC we receive as input on decryption.
324
+ */
325
+ const inputMic = new SingletonBuffer();
326
+
327
+ /**
328
+ * CTR block buffer.
329
+ */
330
+ const ctrBlock = new SingletonBuffer();
331
+
332
+ /**
333
+ * General-purpose block buffer.
334
+ */
335
+ const tempBlock1 = new SingletonBuffer();
336
+
337
+ /**
338
+ * General-purpose block buffer.
339
+ */
340
+ const tempBlock2 = new SingletonBuffer();
341
+
342
+ function validateNonceAndAdata(input: Ccm.Input) {
343
+ if (input.nonce.length !== CRYPTO_AEAD_NONCE_LENGTH_BYTES) {
344
+ throw new CryptoInputError("Nonce must be 13 bytes");
345
+ }
346
+
347
+ if (input.adata && input.adata.length > 0xffff) {
348
+ throw new CryptoInputError(`Associated adata exceeds maximum length of ${MAX_PLAINTEXT_LENGTH}`);
349
+ }
350
+ }
@@ -0,0 +1,4 @@
1
+ This pure-JS AES-CCM implementation is necessary because the Web Crypto does not support AES CCM mode.
2
+
3
+ Implemented based on the NIST and Matter specifications, and SJCL and OpenSSL as reference implementations. See source
4
+ files for specific links.
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Project CHIP Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * A 32-bit word array for AES algorithm.
9
+ *
10
+ * Note that we use signed integers so we can use JS bit shifts with signed 32-bit numbers.
11
+ */
12
+ export type WordArray = Int32Array;
13
+
14
+ export function WordArray(length: number): WordArray {
15
+ return new Int32Array(length);
16
+ }
17
+
18
+ const endianConversionView = new DataView(new ArrayBuffer(4));
19
+ const endianConversionBytes = new Uint8Array(endianConversionView.buffer);
20
+
21
+ export namespace WordArray {
22
+ export function fromByteArray(bytes: Uint8Array, alignment = 1) {
23
+ return fromByteView(new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength), alignment);
24
+ }
25
+
26
+ export function fromByteView(bytes: DataView, alignment = 1) {
27
+ const inputBytes = bytes.byteLength;
28
+ const allocWords = Math.ceil(inputBytes / 4 / alignment) * alignment;
29
+ const result = WordArray(allocWords);
30
+
31
+ // Convert full words from bytes to words
32
+ let i = 0;
33
+ for (; i + 4 <= inputBytes; i += 4) {
34
+ result[i / 4] = bytes.getInt32(i);
35
+ }
36
+
37
+ // If byte array is not aligned to words, compute final word manually
38
+ if (i < inputBytes) {
39
+ result[i / 4] = readPartialWord(bytes, i, inputBytes - i);
40
+ }
41
+
42
+ return result;
43
+ }
44
+
45
+ /**
46
+ * Copy bytes into a 4-word block. If the input bytes is too short sets missing bytes to zero.
47
+ */
48
+ export function bytesToBlock(bytes: DataView, block: WordArray, byteOffset?: number) {
49
+ if (byteOffset === undefined) {
50
+ byteOffset = 0;
51
+ }
52
+ for (let i = 0; i < 4; i++, byteOffset += 4) {
53
+ if (byteOffset + 4 <= bytes.byteLength) {
54
+ block[i] = bytes.getInt32(byteOffset);
55
+ } else if (byteOffset >= bytes.byteLength) {
56
+ block[i] = 0;
57
+ } else {
58
+ block[i] = readPartialWord(bytes, byteOffset);
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Read a word from a byte array that may be smaller than four bytes. On little-endian platforms, flips the byte
65
+ * order.
66
+ */
67
+ export function readPartialWord(
68
+ bytes: DataView,
69
+ offset: number,
70
+ bytesAvailable = bytes.byteLength - offset,
71
+ ): number {
72
+ // Write big-endian word to conversion buffer
73
+ for (let i = 0; i < 4; i++) {
74
+ if (i < bytesAvailable) {
75
+ endianConversionBytes[i] = bytes.getUint8(offset + i);
76
+ } else {
77
+ endianConversionBytes[i] = 0;
78
+ }
79
+ }
80
+
81
+ // Flip to platform endianness
82
+ return endianConversionView.getUint32(0);
83
+ }
84
+
85
+ /**
86
+ * Inverse of {@link writePartialWord}.
87
+ */
88
+ export function writePartialWord(
89
+ word: number,
90
+ bytes: DataView,
91
+ offset: number,
92
+ bytesAvailable = Math.max(bytes.byteLength - offset, 4),
93
+ ) {
94
+ endianConversionView.setUint32(0, word);
95
+ endianConversionBytes[0] = word >>> 24;
96
+ endianConversionBytes[1] = (word >> 16) & 0xff;
97
+ endianConversionBytes[2] = (word >> 8) & 0xff;
98
+ endianConversionBytes[3] = word & 0xff;
99
+
100
+ // Buffer is now LE if platform is LE but this will flip
101
+ for (let i = 0; i < bytesAvailable; i++) {
102
+ bytes.setUint8(offset + i, endianConversionBytes[i]);
103
+ }
104
+ }
105
+ }
@@ -6,5 +6,8 @@
6
6
 
7
7
  export * from "./Crypto.js";
8
8
  export * from "./CryptoConstants.js";
9
+ export * from "./CryptoError.js";
9
10
  export * from "./Key.js";
11
+ export * from "./nonentropic.js";
10
12
  export * from "./Spake2p.js";
13
+ export * from "./StandardCrypto.js";
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2022-2025 Project CHIP Authors
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { ImplementationError } from "#MatterError.js";
8
+ import { MaybePromise } from "#util/Promises.js";
9
+ import { Crypto, ec } from "./Crypto.js";
10
+ import { CurveType, Key, KeyType, PrivateKey } from "./Key.js";
11
+
12
+ /**
13
+ * WARNING: ONLY FOR USE IN PROTECTED TESTING ENVIRONMENTS WHERE SECURITY IS NOT A CONCERN
14
+ *
15
+ * Execute {@link actor} with sources of entropy replaced to produce stable values based on an input index.
16
+ *
17
+ * This is useful in testing environments where Matter logic is difficult to test with true entropy.
18
+ *
19
+ * I believe there are some crypto functions that are not fully mocked here but this covers current test cases.
20
+ */
21
+ export function nonentropic<T>(index: number, actor: () => T): T {
22
+ if (index < 0 || index > 255) {
23
+ throw new ImplementationError(`Index for stable crypto must be 0-255`);
24
+ }
25
+
26
+ const crypto = Crypto.default;
27
+ const { getRandomData, createKeyPair } = crypto;
28
+ let isAsync = false;
29
+
30
+ try {
31
+ // Create random data consisting of index repeated
32
+ crypto.getRandomData = function getRandomDataNONENTROPIC(length) {
33
+ const result = new Uint8Array(length);
34
+ result.fill(index);
35
+ return result;
36
+ };
37
+
38
+ // Ensure EC key generation uses our own "entropy" source rather than the platform's
39
+ crypto.createKeyPair = function getRandomDataNONENTROPIC() {
40
+ const privateBits = ec.mapHashToField(crypto.getRandomData(48), ec.p256.CURVE.n);
41
+ return Key({
42
+ kty: KeyType.EC,
43
+ crv: CurveType.p256,
44
+ privateBits,
45
+ }) as PrivateKey;
46
+ };
47
+
48
+ let result = actor();
49
+ if (MaybePromise.is(result)) {
50
+ isAsync = true;
51
+ result = Promise.resolve(result).finally(revert) as T;
52
+ }
53
+
54
+ return result;
55
+ } finally {
56
+ if (!isAsync) {
57
+ revert();
58
+ }
59
+ }
60
+
61
+ function revert() {
62
+ crypto.getRandomData = getRandomData;
63
+ crypto.createKeyPair = createKeyPair;
64
+ }
65
+ }
@@ -4,7 +4,6 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { Boot } from "#util/Boot.js";
8
7
  import { MaybePromise } from "#util/Promises.js";
9
8
  import { DiagnosticSource } from "../log/DiagnosticSource.js";
10
9
  import { Logger } from "../log/Logger.js";
@@ -242,8 +241,4 @@ export class Environment {
242
241
  }
243
242
  }
244
243
 
245
- let global: Environment;
246
-
247
- Boot.init(() => {
248
- global = new Environment("default");
249
- });
244
+ let global = new Environment("default");
@@ -155,16 +155,16 @@ export class RuntimeService implements Multiplex {
155
155
  }
156
156
 
157
157
  /**
158
- * Interrupt handler. Triggered by e.g. on SIGINT on unixish systems.
158
+ * Interrupt handler. Triggered by e.g. SIGINT on unixish systems.
159
159
  *
160
160
  * The default implementation cancels the runtime.
161
- *
162
- * @returns a boolean indicating whether to continue trapping interrupts
163
161
  */
164
- interrupt(): boolean {
162
+ interrupt() {
165
163
  this.cancel();
166
164
 
167
- return false;
165
+ if (typeof MatterHooks !== "undefined") {
166
+ MatterHooks.interrupt();
167
+ }
168
168
  }
169
169
 
170
170
  /**
@@ -158,6 +158,7 @@ export class VariableService {
158
158
 
159
159
  case null:
160
160
  case 0:
161
+ case "0":
161
162
  case false:
162
163
  case "false":
163
164
  case "off":
@@ -557,8 +557,8 @@ function valueFor(value: unknown) {
557
557
  if (typeof value !== "object" || value === null) {
558
558
  return value;
559
559
  }
560
- const proxied = (value as Diagnostic)[Diagnostic.value];
561
- if (proxied) {
560
+ if (Diagnostic.value in value) {
561
+ const proxied = (value as Diagnostic)[Diagnostic.value];
562
562
  if (proxied === value) {
563
563
  throw new InternalError("Diagnostic value proxies to itself");
564
564
  }
@@ -586,9 +586,17 @@ function presentationFor(value: unknown) {
586
586
  /**
587
587
  * Render a value with presentation support
588
588
  */
589
- function renderDiagnostic(value: unknown, formatter: Formatter): string {
590
- const presentation = presentationFor(value);
591
- value = valueFor(value);
589
+ function renderDiagnostic(value: unknown, formatter: Formatter, ignorePresentation?: boolean): string {
590
+ const presentation = ignorePresentation ? undefined : presentationFor(value);
591
+
592
+ const logValue = valueFor(value);
593
+ if (logValue === value) {
594
+ // Ignore presentation when we recurse or it would be an infinite loop
595
+ ignorePresentation = true;
596
+ } else {
597
+ ignorePresentation = undefined;
598
+ value = logValue;
599
+ }
592
600
 
593
601
  switch (presentation) {
594
602
  case undefined:
@@ -610,22 +618,22 @@ function renderDiagnostic(value: unknown, formatter: Formatter): string {
610
618
  return renderValue(value, formatter, true);
611
619
 
612
620
  case Diagnostic.Presentation.Strong:
613
- return formatter.strong(() => renderDiagnostic(value, formatter));
621
+ return formatter.strong(() => renderDiagnostic(value, formatter, ignorePresentation));
614
622
 
615
623
  case Diagnostic.Presentation.Weak:
616
- return formatter.weak(() => renderDiagnostic(value, formatter));
624
+ return formatter.weak(() => renderDiagnostic(value, formatter, ignorePresentation));
617
625
 
618
626
  case Diagnostic.Presentation.Added:
619
- return formatter.added(() => renderDiagnostic(value, formatter));
627
+ return formatter.added(() => renderDiagnostic(value, formatter, ignorePresentation));
620
628
 
621
629
  case Diagnostic.Presentation.Deleted:
622
- return formatter.deleted(() => renderDiagnostic(value, formatter));
630
+ return formatter.deleted(() => renderDiagnostic(value, formatter, ignorePresentation));
623
631
 
624
632
  case Diagnostic.Presentation.Flag:
625
633
  return (value as string).length ? formatter.keylike(value as string) : "";
626
634
 
627
635
  case Diagnostic.Presentation.Error:
628
- return formatter.error(() => renderDiagnostic(value, formatter));
636
+ return formatter.error(() => renderDiagnostic(value, formatter, ignorePresentation));
629
637
 
630
638
  case Diagnostic.Presentation.Via:
631
639
  return formatter.via(`${value}`);
@@ -642,7 +650,7 @@ function renderDiagnostic(value: unknown, formatter: Formatter): string {
642
650
  case Lifecycle.Status.Active:
643
651
  case Lifecycle.Status.Crashed:
644
652
  case Lifecycle.Status.Destroyed:
645
- return formatter.status(presentation, () => renderDiagnostic(value, formatter));
653
+ return formatter.status(presentation, () => renderDiagnostic(value, formatter, ignorePresentation));
646
654
 
647
655
  default:
648
656
  throw new ImplementationError(`Unsupported diagnostic presentation "${presentation}"`);
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import { MatterError, NoProviderError } from "../MatterError.js";
7
+ import { MatterError } from "../MatterError.js";
8
8
  import { MaybePromise } from "../util/Promises.js";
9
9
  import { UdpChannel, UdpChannelOptions } from "./UdpChannel.js";
10
10
 
@@ -57,13 +57,7 @@ export type NetworkInterfaceDetails = {
57
57
  };
58
58
 
59
59
  export type NetworkInterfaceDetailed = NetworkInterface & NetworkInterfaceDetails;
60
-
61
60
  export abstract class Network {
62
- // TODO - remove this singleton
63
- static get: () => Network = () => {
64
- throw new NoProviderError("No provider configured");
65
- };
66
-
67
61
  abstract getNetInterfaces(configuration?: NetworkInterface[]): MaybePromise<NetworkInterface[]>;
68
62
  abstract getIpMac(netInterface: string): MaybePromise<NetworkInterfaceDetails | undefined>;
69
63
  abstract createUdpChannel(options: UdpChannelOptions): Promise<UdpChannel>;