@matter/general 0.14.1-alpha.0-20250606-a9bcd03f9 → 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.
- package/dist/cjs/codec/DerCodec.d.ts +12 -17
- package/dist/cjs/codec/DerCodec.d.ts.map +1 -1
- package/dist/cjs/codec/DerCodec.js +90 -51
- package/dist/cjs/codec/DerCodec.js.map +1 -1
- package/dist/cjs/codec/DerTypes.js +1 -1
- package/dist/cjs/codec/DnsCodec.d.ts +5 -5
- package/dist/cjs/crypto/Crypto.d.ts +111 -62
- package/dist/cjs/crypto/Crypto.d.ts.map +1 -1
- package/dist/cjs/crypto/Crypto.js +92 -31
- package/dist/cjs/crypto/Crypto.js.map +1 -1
- package/dist/cjs/crypto/CryptoError.d.ts +32 -0
- package/dist/cjs/crypto/CryptoError.d.ts.map +1 -0
- package/dist/cjs/crypto/CryptoError.js +44 -0
- package/dist/cjs/crypto/CryptoError.js.map +6 -0
- package/dist/cjs/crypto/Key.d.ts +2 -2
- package/dist/cjs/crypto/Key.d.ts.map +1 -1
- package/dist/cjs/crypto/Key.js +15 -16
- package/dist/cjs/crypto/Key.js.map +1 -1
- package/dist/cjs/crypto/Spake2p.js +5 -5
- package/dist/cjs/crypto/Spake2p.js.map +1 -1
- package/dist/cjs/crypto/StandardCrypto.d.ts +33 -0
- package/dist/cjs/crypto/StandardCrypto.d.ts.map +1 -0
- package/dist/cjs/crypto/StandardCrypto.js +208 -0
- package/dist/cjs/crypto/StandardCrypto.js.map +6 -0
- package/dist/cjs/crypto/aes/Aes.d.ts +21 -0
- package/dist/cjs/crypto/aes/Aes.d.ts.map +1 -0
- package/dist/cjs/crypto/aes/Aes.js +132 -0
- package/dist/cjs/crypto/aes/Aes.js.map +6 -0
- package/dist/cjs/crypto/aes/Ccm.d.ts +71 -0
- package/dist/cjs/crypto/aes/Ccm.d.ts.map +1 -0
- package/dist/cjs/crypto/aes/Ccm.js +194 -0
- package/dist/cjs/crypto/aes/Ccm.js.map +6 -0
- package/dist/cjs/crypto/aes/WordArray.d.ts +30 -0
- package/dist/cjs/crypto/aes/WordArray.d.ts.map +1 -0
- package/dist/cjs/crypto/aes/WordArray.js +91 -0
- package/dist/cjs/crypto/aes/WordArray.js.map +6 -0
- package/dist/cjs/crypto/index.d.ts +3 -0
- package/dist/cjs/crypto/index.d.ts.map +1 -1
- package/dist/cjs/crypto/index.js +3 -0
- package/dist/cjs/crypto/index.js.map +1 -1
- package/dist/cjs/crypto/nonentropic.d.ts +16 -0
- package/dist/cjs/crypto/nonentropic.d.ts.map +1 -0
- package/dist/cjs/crypto/nonentropic.js +70 -0
- package/dist/cjs/crypto/nonentropic.js.map +6 -0
- package/dist/cjs/environment/Environment.d.ts.map +1 -1
- package/dist/cjs/environment/Environment.js +1 -5
- package/dist/cjs/environment/Environment.js.map +1 -1
- package/dist/cjs/environment/RuntimeService.d.ts +2 -4
- package/dist/cjs/environment/RuntimeService.d.ts.map +1 -1
- package/dist/cjs/environment/RuntimeService.js +4 -4
- package/dist/cjs/environment/RuntimeService.js.map +1 -1
- package/dist/cjs/environment/VariableService.d.ts.map +1 -1
- package/dist/cjs/environment/VariableService.js +1 -0
- package/dist/cjs/environment/VariableService.js.map +1 -1
- package/dist/cjs/log/LogFormat.js +17 -11
- package/dist/cjs/log/LogFormat.js.map +1 -1
- package/dist/cjs/net/Network.d.ts +0 -1
- package/dist/cjs/net/Network.d.ts.map +1 -1
- package/dist/cjs/net/Network.js +0 -4
- package/dist/cjs/net/Network.js.map +1 -1
- package/dist/cjs/time/Time.d.ts.map +1 -1
- package/dist/cjs/time/Time.js +2 -2
- package/dist/cjs/time/Time.js.map +1 -1
- package/dist/cjs/util/Bytes.d.ts +6 -0
- package/dist/cjs/util/Bytes.d.ts.map +1 -1
- package/dist/cjs/util/Bytes.js +15 -1
- package/dist/cjs/util/Bytes.js.map +1 -1
- package/dist/cjs/util/DataWriter.d.ts +1 -1
- package/dist/cjs/util/DataWriter.js +2 -2
- package/dist/cjs/util/DataWriter.js.map +1 -1
- package/dist/cjs/util/DeepCopy.js +1 -1
- package/dist/cjs/util/DeepCopy.js.map +1 -1
- package/dist/cjs/util/GeneratedClass.d.ts +3 -3
- package/dist/cjs/util/GeneratedClass.d.ts.map +1 -1
- package/dist/cjs/util/GeneratedClass.js +99 -73
- package/dist/cjs/util/GeneratedClass.js.map +2 -2
- package/dist/cjs/util/Number.d.ts +0 -1
- package/dist/cjs/util/Number.d.ts.map +1 -1
- package/dist/cjs/util/Number.js +0 -4
- package/dist/cjs/util/Number.js.map +1 -1
- package/dist/esm/codec/DerCodec.d.ts +12 -17
- package/dist/esm/codec/DerCodec.d.ts.map +1 -1
- package/dist/esm/codec/DerCodec.js +90 -51
- package/dist/esm/codec/DerCodec.js.map +1 -1
- package/dist/esm/codec/DerTypes.js +2 -2
- package/dist/esm/codec/DnsCodec.d.ts +5 -5
- package/dist/esm/crypto/Crypto.d.ts +111 -62
- package/dist/esm/crypto/Crypto.d.ts.map +1 -1
- package/dist/esm/crypto/Crypto.js +93 -32
- package/dist/esm/crypto/Crypto.js.map +1 -1
- package/dist/esm/crypto/CryptoError.d.ts +32 -0
- package/dist/esm/crypto/CryptoError.d.ts.map +1 -0
- package/dist/esm/crypto/CryptoError.js +24 -0
- package/dist/esm/crypto/CryptoError.js.map +6 -0
- package/dist/esm/crypto/Key.d.ts +2 -2
- package/dist/esm/crypto/Key.d.ts.map +1 -1
- package/dist/esm/crypto/Key.js +15 -16
- package/dist/esm/crypto/Key.js.map +1 -1
- package/dist/esm/crypto/Spake2p.js +5 -5
- package/dist/esm/crypto/Spake2p.js.map +1 -1
- package/dist/esm/crypto/StandardCrypto.d.ts +33 -0
- package/dist/esm/crypto/StandardCrypto.d.ts.map +1 -0
- package/dist/esm/crypto/StandardCrypto.js +188 -0
- package/dist/esm/crypto/StandardCrypto.js.map +6 -0
- package/dist/esm/crypto/aes/Aes.d.ts +21 -0
- package/dist/esm/crypto/aes/Aes.d.ts.map +1 -0
- package/dist/esm/crypto/aes/Aes.js +112 -0
- package/dist/esm/crypto/aes/Aes.js.map +6 -0
- package/dist/esm/crypto/aes/Ccm.d.ts +71 -0
- package/dist/esm/crypto/aes/Ccm.d.ts.map +1 -0
- package/dist/esm/crypto/aes/Ccm.js +174 -0
- package/dist/esm/crypto/aes/Ccm.js.map +6 -0
- package/dist/esm/crypto/aes/WordArray.d.ts +30 -0
- package/dist/esm/crypto/aes/WordArray.d.ts.map +1 -0
- package/dist/esm/crypto/aes/WordArray.js +71 -0
- package/dist/esm/crypto/aes/WordArray.js.map +6 -0
- package/dist/esm/crypto/index.d.ts +3 -0
- package/dist/esm/crypto/index.d.ts.map +1 -1
- package/dist/esm/crypto/index.js +3 -0
- package/dist/esm/crypto/index.js.map +1 -1
- package/dist/esm/crypto/nonentropic.d.ts +16 -0
- package/dist/esm/crypto/nonentropic.d.ts.map +1 -0
- package/dist/esm/crypto/nonentropic.js +50 -0
- package/dist/esm/crypto/nonentropic.js.map +6 -0
- package/dist/esm/environment/Environment.d.ts.map +1 -1
- package/dist/esm/environment/Environment.js +1 -5
- package/dist/esm/environment/Environment.js.map +1 -1
- package/dist/esm/environment/RuntimeService.d.ts +2 -4
- package/dist/esm/environment/RuntimeService.d.ts.map +1 -1
- package/dist/esm/environment/RuntimeService.js +4 -4
- package/dist/esm/environment/RuntimeService.js.map +1 -1
- package/dist/esm/environment/VariableService.d.ts.map +1 -1
- package/dist/esm/environment/VariableService.js +1 -0
- package/dist/esm/environment/VariableService.js.map +1 -1
- package/dist/esm/log/LogFormat.js +17 -11
- package/dist/esm/log/LogFormat.js.map +1 -1
- package/dist/esm/net/Network.d.ts +0 -1
- package/dist/esm/net/Network.d.ts.map +1 -1
- package/dist/esm/net/Network.js +1 -5
- package/dist/esm/net/Network.js.map +1 -1
- package/dist/esm/time/Time.d.ts.map +1 -1
- package/dist/esm/time/Time.js +2 -2
- package/dist/esm/time/Time.js.map +1 -1
- package/dist/esm/util/Bytes.d.ts +6 -0
- package/dist/esm/util/Bytes.d.ts.map +1 -1
- package/dist/esm/util/Bytes.js +15 -1
- package/dist/esm/util/Bytes.js.map +1 -1
- package/dist/esm/util/DataWriter.d.ts +1 -1
- package/dist/esm/util/DataWriter.js +3 -3
- package/dist/esm/util/DataWriter.js.map +1 -1
- package/dist/esm/util/DeepCopy.js +1 -1
- package/dist/esm/util/DeepCopy.js.map +1 -1
- package/dist/esm/util/GeneratedClass.d.ts +3 -3
- package/dist/esm/util/GeneratedClass.d.ts.map +1 -1
- package/dist/esm/util/GeneratedClass.js +97 -71
- package/dist/esm/util/GeneratedClass.js.map +2 -2
- package/dist/esm/util/Number.d.ts +0 -1
- package/dist/esm/util/Number.d.ts.map +1 -1
- package/dist/esm/util/Number.js +0 -4
- package/dist/esm/util/Number.js.map +1 -1
- package/package.json +3 -3
- package/src/codec/DerCodec.ts +106 -52
- package/src/codec/DerTypes.ts +2 -2
- package/src/crypto/Crypto.ts +196 -76
- package/src/crypto/CryptoError.ts +32 -0
- package/src/crypto/Key.ts +17 -18
- package/src/crypto/Spake2p.ts +5 -5
- package/src/crypto/StandardCrypto.ts +252 -0
- package/src/crypto/aes/Aes.ts +210 -0
- package/src/crypto/aes/Ccm.ts +350 -0
- package/src/crypto/aes/README.md +4 -0
- package/src/crypto/aes/WordArray.ts +105 -0
- package/src/crypto/index.ts +3 -0
- package/src/crypto/nonentropic.ts +65 -0
- package/src/environment/Environment.ts +1 -6
- package/src/environment/RuntimeService.ts +5 -5
- package/src/environment/VariableService.ts +1 -0
- package/src/log/LogFormat.ts +19 -11
- package/src/net/Network.ts +1 -7
- package/src/time/Time.ts +4 -4
- package/src/util/Bytes.ts +19 -0
- package/src/util/DataWriter.ts +3 -3
- package/src/util/DeepCopy.ts +2 -2
- package/src/util/GeneratedClass.ts +161 -102
- 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,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
|
+
}
|
package/src/crypto/index.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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()
|
|
162
|
+
interrupt() {
|
|
165
163
|
this.cancel();
|
|
166
164
|
|
|
167
|
-
|
|
165
|
+
if (typeof MatterHooks !== "undefined") {
|
|
166
|
+
MatterHooks.interrupt();
|
|
167
|
+
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
package/src/log/LogFormat.ts
CHANGED
|
@@ -557,8 +557,8 @@ function valueFor(value: unknown) {
|
|
|
557
557
|
if (typeof value !== "object" || value === null) {
|
|
558
558
|
return value;
|
|
559
559
|
}
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
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}"`);
|
package/src/net/Network.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { MatterError
|
|
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>;
|