@totemsdk/edge-coap 0.2.0 → 0.3.0

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.
@@ -1,21 +1,19 @@
1
- "use strict";
2
1
  /**
3
2
  * NativeCoapTransport — connects to the edge-coap Go binary over TCP.
4
3
  *
5
- * The Go binary listens on a local TCP socket (default 127.0.0.1:15003)
6
- * and accepts newline-delimited JSON requests. This class implements
7
- * CoapTransportPort by forwarding all calls to the Go process.
4
+ * The Go binary listens on a local TCP socket (default 127.0.0.1:15003) and
5
+ * accepts newline-delimited JSON send requests. Inbound datagrams are pushed
6
+ * back as `message` events; token correlation stays in the gateway.
8
7
  */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.NativeCoapTransport = void 0;
11
8
  let requestCounter = 0;
12
- class NativeCoapTransport {
9
+ export class NativeCoapTransport {
13
10
  constructor(config = {}) {
14
11
  this.socket = null;
15
12
  this.buffer = '';
16
13
  this.pending = new Map();
17
14
  this.messageHandlers = [];
18
15
  this.errorHandlers = [];
16
+ this.closedByUser = false;
19
17
  this.config = {
20
18
  host: config.host ?? '127.0.0.1',
21
19
  port: config.port ?? 15003,
@@ -23,15 +21,27 @@ class NativeCoapTransport {
23
21
  requestTimeoutMs: config.requestTimeoutMs ?? 10000,
24
22
  };
25
23
  }
24
+ emitError(error) {
25
+ for (const handler of this.errorHandlers) {
26
+ try {
27
+ handler(error);
28
+ }
29
+ catch { }
30
+ }
31
+ }
26
32
  async bind(_port) {
27
33
  const net = await import('net');
34
+ this.closedByUser = false;
35
+ this.buffer = '';
28
36
  return new Promise((resolve, reject) => {
37
+ let connected = false;
29
38
  const timer = setTimeout(() => {
30
39
  this.socket?.destroy();
31
- reject(new Error(`NativeCoapTransport: connect timeout`));
40
+ reject(new Error('NativeCoapTransport: connect timeout'));
32
41
  }, this.config.connectTimeoutMs);
33
42
  this.socket = net.createConnection({ host: this.config.host, port: this.config.port }, () => {
34
43
  clearTimeout(timer);
44
+ connected = true;
35
45
  resolve();
36
46
  });
37
47
  this.socket.on('data', (chunk) => {
@@ -44,15 +54,19 @@ class NativeCoapTransport {
44
54
  try {
45
55
  const msg = JSON.parse(line);
46
56
  if (msg.type === 'message') {
47
- const push = msg;
48
- const data = hexToBytes(push.data);
57
+ if (typeof msg.host !== 'string' || typeof msg.port !== 'number' || typeof msg.data !== 'string')
58
+ continue;
59
+ const data = hexToBytes(msg.data);
49
60
  for (const handler of this.messageHandlers) {
50
61
  try {
51
- handler(data, { host: push.host, port: push.port });
62
+ handler(data, { host: msg.host, port: msg.port });
52
63
  }
53
64
  catch { }
54
65
  }
55
66
  }
67
+ else if (msg.type === 'error') {
68
+ this.emitError(new Error(`NativeCoapTransport: ${msg.code ?? 'error'}: ${msg.error ?? 'unknown error'}`));
69
+ }
56
70
  else if (msg.id && this.pending.has(msg.id)) {
57
71
  const { resolve: res, reject: rej } = this.pending.get(msg.id);
58
72
  this.pending.delete(msg.id);
@@ -66,28 +80,34 @@ class NativeCoapTransport {
66
80
  }
67
81
  });
68
82
  this.socket.on('error', (err) => {
69
- for (const handler of this.errorHandlers) {
70
- try {
71
- handler(err);
72
- }
73
- catch { }
83
+ if (!connected) {
84
+ clearTimeout(timer);
85
+ this.socket?.destroy();
86
+ reject(err);
74
87
  }
88
+ this.emitError(err);
75
89
  });
76
90
  this.socket.on('close', () => {
77
91
  for (const [, p] of this.pending)
78
92
  p.reject(new Error('connection closed'));
79
93
  this.pending.clear();
94
+ if (!this.closedByUser)
95
+ this.emitError(new Error('NativeCoapTransport: connection closed'));
80
96
  });
81
97
  });
82
98
  }
83
99
  async close() {
100
+ this.closedByUser = true;
84
101
  if (this.socket) {
85
102
  this.socket.destroy();
86
103
  this.socket = null;
87
104
  }
105
+ for (const [, p] of this.pending)
106
+ p.reject(new Error('connection closed'));
107
+ this.pending.clear();
88
108
  }
89
109
  async send(host, port, message) {
90
- if (!this.socket)
110
+ if (!this.socket || this.socket.destroyed)
91
111
  throw new Error('NativeCoapTransport: not connected');
92
112
  const id = `coap-${++requestCounter}`;
93
113
  const req = { id, host, port, data: bytesToHex(message) };
@@ -112,7 +132,6 @@ class NativeCoapTransport {
112
132
  return () => { this.errorHandlers = this.errorHandlers.filter(h => h !== handler); };
113
133
  }
114
134
  }
115
- exports.NativeCoapTransport = NativeCoapTransport;
116
135
  function bytesToHex(bytes) {
117
136
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
118
137
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * OSCORE (RFC 8613) security context for Node.js.
3
+ *
4
+ * The connector stays crypto-free; this adapter implements the OSCORE client
5
+ * and server roles behind `CoapSecurityPort` using Node's `aes-128-ccm` and
6
+ * HKDF-SHA-256. It is validated against the RFC 8613 Appendix C vectors.
7
+ *
8
+ * Supported profile: AES-CCM-16-64-128 (alg 10), HKDF-SHA-256, no Class I
9
+ * options (none are currently defined), and the CoAP fields modeled by
10
+ * `CoapMessage`. Uri-Host/Uri-Port remain outer (Class U); Uri-Path and
11
+ * Uri-Query are inner (Class E). Block options are inner. Blockwise
12
+ * observations and multicast are outside this adapter's scope.
13
+ */
14
+ import type { CoapSecurityPort } from './security.js';
15
+ export interface OscoreContextConfig {
16
+ masterSecret: Uint8Array;
17
+ /** Optional master salt; defaults to the empty byte string. */
18
+ masterSalt?: Uint8Array;
19
+ senderId: Uint8Array;
20
+ recipientId: Uint8Array;
21
+ idContext?: Uint8Array;
22
+ /** Initial sender sequence number (Partial IV). Default 0. */
23
+ senderSequenceNumber?: number;
24
+ /** Anti-replay window for inbound Partial IVs. Default 32. */
25
+ replayWindowSize?: number;
26
+ /** Restrict protection to these endpoints. Default: all endpoints. */
27
+ endpoints?: Array<{
28
+ host: string;
29
+ port: number;
30
+ }>;
31
+ }
32
+ export declare function createOscoreContext(config: OscoreContextConfig): Promise<CoapSecurityPort>;
package/dist/oscore.js ADDED
@@ -0,0 +1,526 @@
1
+ /**
2
+ * OSCORE (RFC 8613) security context for Node.js.
3
+ *
4
+ * The connector stays crypto-free; this adapter implements the OSCORE client
5
+ * and server roles behind `CoapSecurityPort` using Node's `aes-128-ccm` and
6
+ * HKDF-SHA-256. It is validated against the RFC 8613 Appendix C vectors.
7
+ *
8
+ * Supported profile: AES-CCM-16-64-128 (alg 10), HKDF-SHA-256, no Class I
9
+ * options (none are currently defined), and the CoAP fields modeled by
10
+ * `CoapMessage`. Uri-Host/Uri-Port remain outer (Class U); Uri-Path and
11
+ * Uri-Query are inner (Class E). Block options are inner. Blockwise
12
+ * observations and multicast are outside this adapter's scope.
13
+ */
14
+ import { decodeBlock, encodeBlock, formatResponseCode, parseResponseCode, tokenToKey } from './codec.js';
15
+ const AEAD_ALGORITHM = 10; // AES-CCM-16-64-128
16
+ const KEY_LENGTH = 16;
17
+ const NONCE_LENGTH = 13;
18
+ const TAG_LENGTH = 8;
19
+ const MAX_PIV = 0xffffffffff; // 5 bytes
20
+ const METHOD_CODES = { GET: 1, POST: 2, PUT: 3, DELETE: 4, FETCH: 5 };
21
+ const CODE_METHODS = { 1: 'GET', 2: 'POST', 3: 'PUT', 4: 'DELETE', 5: 'FETCH' };
22
+ export async function createOscoreContext(config) {
23
+ const nodeCrypto = await import('node:crypto');
24
+ const masterSalt = config.masterSalt ?? new Uint8Array(0);
25
+ const idContext = config.idContext;
26
+ if (config.senderId.length > NONCE_LENGTH - 6)
27
+ throw new Error('OSCORE sender ID exceeds 7 bytes');
28
+ if (config.recipientId.length > NONCE_LENGTH - 6)
29
+ throw new Error('OSCORE recipient ID exceeds 7 bytes');
30
+ const senderKey = deriveKey(nodeCrypto, config.masterSecret, masterSalt, config.senderId, idContext, 'Key', KEY_LENGTH);
31
+ const recipientKey = deriveKey(nodeCrypto, config.masterSecret, masterSalt, config.recipientId, idContext, 'Key', KEY_LENGTH);
32
+ const commonIv = deriveKey(nodeCrypto, config.masterSecret, masterSalt, new Uint8Array(0), idContext, 'IV', NONCE_LENGTH);
33
+ let sequence = config.senderSequenceNumber ?? 0;
34
+ if (!Number.isInteger(sequence) || sequence < 0 || sequence > MAX_PIV)
35
+ throw new Error('Invalid OSCORE sender sequence number');
36
+ const exchanges = new Map();
37
+ const requestReplay = new ReplayWindow(config.replayWindowSize ?? 32);
38
+ const responseReplay = new ReplayWindow(config.replayWindowSize ?? 32);
39
+ function protects(host, port) {
40
+ if (!config.endpoints || config.endpoints.length === 0)
41
+ return true;
42
+ return config.endpoints.some((endpoint) => endpoint.host === host && endpoint.port === port);
43
+ }
44
+ function nextPiv() {
45
+ if (sequence > MAX_PIV)
46
+ throw new Error('OSCORE sender sequence exhausted');
47
+ const piv = encodePiv(sequence);
48
+ sequence++;
49
+ return piv;
50
+ }
51
+ function protectRequest(message) {
52
+ const method = message.method;
53
+ const code = METHOD_CODES[method];
54
+ if (code === undefined)
55
+ throw new Error(`Unsupported OSCORE method ${method}`);
56
+ const piv = nextPiv();
57
+ const plaintext = encodePlaintext(code, message);
58
+ const aad = buildAad(config.senderId, piv);
59
+ const nonce = buildNonce(commonIv, config.senderId, piv);
60
+ const ciphertext = encrypt(nodeCrypto, senderKey, nonce, aad, plaintext);
61
+ exchanges.set(tokenToKey(message.token), { kid: config.senderId.slice(), piv, nonce });
62
+ trimExchanges(exchanges);
63
+ return {
64
+ type: message.type,
65
+ method: message.observe !== undefined ? 'FETCH' : 'POST',
66
+ messageId: message.messageId,
67
+ token: message.token,
68
+ path: [],
69
+ ...(message.uriHost !== undefined ? { uriHost: message.uriHost } : {}),
70
+ ...(message.uriPort !== undefined ? { uriPort: message.uriPort } : {}),
71
+ payload: ciphertext,
72
+ ...(message.observe !== undefined ? { observe: message.observe } : {}),
73
+ oscore: encodeOscoreOption(piv, { kid: config.senderId, kidContext: idContext, includeKid: true }),
74
+ remote: message.remote,
75
+ receivedAt: message.receivedAt,
76
+ };
77
+ }
78
+ function protectResponse(message) {
79
+ const key = tokenToKey(message.token);
80
+ const state = exchanges.get(key);
81
+ if (!state)
82
+ throw new Error('No matching OSCORE request context for the response');
83
+ const code = parseResponseCode(message.responseCode);
84
+ const plaintext = encodePlaintext(code, message);
85
+ const aad = buildAad(state.kid, state.piv);
86
+ // Notifications carry a fresh Partial IV; a single response reuses the
87
+ // request nonce and omits the Partial IV (RFC 8613 §8.3).
88
+ const notification = message.observe !== undefined;
89
+ const piv = notification ? nextPiv() : new Uint8Array(0);
90
+ const nonce = notification ? buildNonce(commonIv, config.senderId, piv) : state.nonce;
91
+ const ciphertext = encrypt(nodeCrypto, senderKey, nonce, aad, plaintext);
92
+ if (!notification)
93
+ exchanges.delete(key);
94
+ return {
95
+ type: message.type,
96
+ responseCode: notification ? '2.05' : '2.04',
97
+ messageId: message.messageId,
98
+ token: message.token,
99
+ path: [],
100
+ ...(message.uriHost !== undefined ? { uriHost: message.uriHost } : {}),
101
+ ...(message.uriPort !== undefined ? { uriPort: message.uriPort } : {}),
102
+ payload: ciphertext,
103
+ ...(message.observe !== undefined ? { observe: message.observe } : {}),
104
+ oscore: notification
105
+ ? encodeOscoreOption(piv, { includeKid: false })
106
+ : new Uint8Array(0),
107
+ remote: message.remote,
108
+ receivedAt: message.receivedAt,
109
+ };
110
+ }
111
+ async function protect(message) {
112
+ if (message.method !== undefined)
113
+ return protectRequest(message);
114
+ if (message.responseCode !== undefined)
115
+ return protectResponse(message);
116
+ throw new Error('OSCORE protection requires a request method or a response code');
117
+ }
118
+ function unprotectRequest(message) {
119
+ const option = decodeOscoreOption(message.oscore);
120
+ if (!option.kid)
121
+ throw new Error('OSCORE request is missing kid');
122
+ if (!equalBytes(option.kid, config.recipientId))
123
+ throw new Error('Unknown OSCORE recipient ID');
124
+ if (option.kidContext && !equalBytes(option.kidContext, idContext ?? new Uint8Array(0))) {
125
+ throw new Error('Unknown OSCORE ID context');
126
+ }
127
+ if (option.piv.length === 0)
128
+ throw new Error('OSCORE request is missing a Partial IV');
129
+ if (!requestReplay.accept(uintFromBytes(option.piv)))
130
+ throw new Error('OSCORE replay detected');
131
+ const aad = buildAad(option.kid, option.piv);
132
+ const nonce = buildNonce(commonIv, option.kid, option.piv);
133
+ const plaintext = decryptPayload(nodeCrypto, recipientKey, nonce, aad, message.payload);
134
+ const decoded = decodePlaintext(plaintext, message);
135
+ exchanges.set(tokenToKey(message.token), { kid: option.kid.slice(), piv: option.piv, nonce });
136
+ trimExchanges(exchanges);
137
+ return decoded;
138
+ }
139
+ function unprotectResponse(message) {
140
+ const option = decodeOscoreOption(message.oscore);
141
+ if (option.kid && !equalBytes(option.kid, config.recipientId))
142
+ throw new Error('Unknown OSCORE recipient ID');
143
+ if (option.kidContext && !equalBytes(option.kidContext, idContext ?? new Uint8Array(0))) {
144
+ throw new Error('Unknown OSCORE ID context');
145
+ }
146
+ const state = exchanges.get(tokenToKey(message.token));
147
+ if (!state)
148
+ throw new Error('No matching OSCORE request context');
149
+ const hasPiv = option.piv.length > 0;
150
+ if (hasPiv && !responseReplay.accept(uintFromBytes(option.piv)))
151
+ throw new Error('OSCORE replay detected');
152
+ const nonce = hasPiv
153
+ ? buildNonce(commonIv, config.recipientId, option.piv)
154
+ : state.nonce;
155
+ const aad = buildAad(state.kid, state.piv);
156
+ const plaintext = decryptPayload(nodeCrypto, recipientKey, nonce, aad, message.payload);
157
+ return decodePlaintext(plaintext, message);
158
+ }
159
+ async function unprotect(message) {
160
+ if (message.oscore === undefined)
161
+ throw new Error('Missing OSCORE option');
162
+ return message.method !== undefined ? unprotectRequest(message) : unprotectResponse(message);
163
+ }
164
+ return { protects, protect, unprotect };
165
+ }
166
+ function encodePlaintext(code, message) {
167
+ const innerOptions = [];
168
+ if (message.observe !== undefined)
169
+ innerOptions.push({ number: 6, value: uintOption(message.observe) });
170
+ for (const segment of message.path)
171
+ innerOptions.push({ number: 11, value: new TextEncoder().encode(segment) });
172
+ if (message.contentFormat !== undefined)
173
+ innerOptions.push({ number: 12, value: uintOption(message.contentFormat) });
174
+ for (const query of message.uriQuery ?? [])
175
+ innerOptions.push({ number: 15, value: new TextEncoder().encode(query) });
176
+ if (message.accept !== undefined)
177
+ innerOptions.push({ number: 17, value: uintOption(message.accept) });
178
+ if (message.block2)
179
+ innerOptions.push({ number: 23, value: uintOption(encodeBlock(message.block2)) });
180
+ if (message.block1)
181
+ innerOptions.push({ number: 27, value: uintOption(encodeBlock(message.block1)) });
182
+ if (message.size2 !== undefined)
183
+ innerOptions.push({ number: 28, value: uintOption(message.size2) });
184
+ if (message.size1 !== undefined)
185
+ innerOptions.push({ number: 60, value: uintOption(message.size1) });
186
+ return concat([
187
+ Uint8Array.of(code),
188
+ encodeOptions(innerOptions),
189
+ message.payload.length ? Uint8Array.of(0xff) : new Uint8Array(0),
190
+ message.payload,
191
+ ]);
192
+ }
193
+ function deriveKey(nodeCrypto, masterSecret, masterSalt, id, idContext, type, length) {
194
+ const info = cborArray([
195
+ cborBytes(id),
196
+ idContext ? cborBytes(idContext) : [0xf6],
197
+ cborUint(AEAD_ALGORITHM),
198
+ cborText(type),
199
+ cborUint(length),
200
+ ]);
201
+ const derived = nodeCrypto.hkdfSync('sha256', Buffer.from(masterSecret), Buffer.from(masterSalt), Buffer.from(Uint8Array.from(info)), length);
202
+ return new Uint8Array(derived);
203
+ }
204
+ function buildAad(requestKid, requestPiv) {
205
+ const aadArray = cborArray([
206
+ cborUint(1),
207
+ cborArray([cborUint(AEAD_ALGORITHM)]),
208
+ cborBytes(requestKid),
209
+ cborBytes(requestPiv),
210
+ cborBytes(new Uint8Array(0)),
211
+ ]);
212
+ const external = cborBytes(Uint8Array.from(aadArray));
213
+ return Uint8Array.from(cborArray([cborText('Encrypt0'), cborBytes(new Uint8Array(0)), external]));
214
+ }
215
+ function buildNonce(commonIv, id, piv) {
216
+ if (piv.length > 5)
217
+ throw new Error('OSCORE Partial IV exceeds 5 bytes');
218
+ if (id.length > 7)
219
+ throw new Error('OSCORE ID exceeds 7 bytes');
220
+ const pad = new Uint8Array(NONCE_LENGTH);
221
+ pad[0] = id.length;
222
+ // ID and PIV are left-padded within their fields: ID in bytes 1..7, PIV in
223
+ // bytes 8..12 (RFC 8613 §5.2, Figure 8).
224
+ pad.set(id, 8 - id.length);
225
+ pad.set(piv, NONCE_LENGTH - piv.length);
226
+ return Uint8Array.from(commonIv, (byte, index) => byte ^ pad[index]);
227
+ }
228
+ function encrypt(nodeCrypto, key, nonce, aad, plaintext) {
229
+ const cipher = nodeCrypto.createCipheriv('aes-128-ccm', Buffer.from(key), Buffer.from(nonce), { authTagLength: TAG_LENGTH });
230
+ cipher.setAAD(Buffer.from(aad), { plaintextLength: plaintext.length });
231
+ const ciphertext = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
232
+ const tag = cipher.getAuthTag();
233
+ const out = new Uint8Array(ciphertext.length + tag.length);
234
+ out.set(ciphertext, 0);
235
+ out.set(tag, ciphertext.length);
236
+ return out;
237
+ }
238
+ function decryptPayload(nodeCrypto, key, nonce, aad, payload) {
239
+ if (payload.length < TAG_LENGTH)
240
+ throw new Error('OSCORE payload is too short');
241
+ const ciphertext = payload.slice(0, payload.length - TAG_LENGTH);
242
+ const tag = payload.slice(payload.length - TAG_LENGTH);
243
+ const decipher = nodeCrypto.createDecipheriv('aes-128-ccm', Buffer.from(key), Buffer.from(nonce), { authTagLength: TAG_LENGTH });
244
+ decipher.setAAD(Buffer.from(aad), { plaintextLength: ciphertext.length });
245
+ decipher.setAuthTag(Buffer.from(tag));
246
+ try {
247
+ return new Uint8Array(Buffer.concat([decipher.update(Buffer.from(ciphertext)), decipher.final()]));
248
+ }
249
+ catch {
250
+ throw new Error('OSCORE authentication failed');
251
+ }
252
+ }
253
+ function encodePiv(sequence) {
254
+ if (sequence === 0)
255
+ return Uint8Array.of(0);
256
+ const bytes = [];
257
+ let remaining = sequence;
258
+ while (remaining > 0) {
259
+ bytes.unshift(remaining & 0xff);
260
+ remaining = Math.floor(remaining / 256);
261
+ }
262
+ return Uint8Array.from(bytes);
263
+ }
264
+ function encodeOscoreOption(piv, { kid, kidContext, includeKid }) {
265
+ const flags = (kidContext ? 0x10 : 0) | (includeKid ? 0x08 : 0) | piv.length;
266
+ const out = [flags, ...piv];
267
+ if (kidContext)
268
+ out.push(kidContext.length, ...kidContext);
269
+ if (includeKid && kid)
270
+ out.push(...kid);
271
+ return Uint8Array.from(out);
272
+ }
273
+ function decodeOscoreOption(value) {
274
+ if (value.length === 0)
275
+ return { piv: new Uint8Array(0) };
276
+ const flags = value[0];
277
+ if ((flags & 0xe0) !== 0)
278
+ throw new Error('Reserved OSCORE flag bits set');
279
+ const length = flags & 0x07;
280
+ if (length === 6 || length === 7)
281
+ throw new Error('Reserved OSCORE Partial IV length');
282
+ let offset = 1;
283
+ if (offset + length > value.length)
284
+ throw new Error('Truncated OSCORE Partial IV');
285
+ const piv = value.slice(offset, offset + length);
286
+ offset += length;
287
+ let kidContext;
288
+ if (flags & 0x10) {
289
+ if (offset >= value.length)
290
+ throw new Error('Truncated OSCORE kid context');
291
+ const contextLength = value[offset++];
292
+ if (offset + contextLength > value.length)
293
+ throw new Error('Truncated OSCORE kid context');
294
+ kidContext = value.slice(offset, offset + contextLength);
295
+ offset += contextLength;
296
+ }
297
+ let kid;
298
+ if (flags & 0x08) {
299
+ kid = value.slice(offset);
300
+ offset = value.length;
301
+ }
302
+ if (offset !== value.length)
303
+ throw new Error('Trailing OSCORE option bytes');
304
+ return { piv, kid, kidContext };
305
+ }
306
+ function encodeOptions(entries) {
307
+ const sorted = [...entries].sort((a, b) => a.number - b.number);
308
+ const out = [];
309
+ let previous = 0;
310
+ for (const entry of sorted) {
311
+ const delta = entry.number - previous;
312
+ const length = entry.value.length;
313
+ const deltaNibble = delta < 13 ? delta : delta < 269 ? 13 : 14;
314
+ const lengthNibble = length < 13 ? length : length < 269 ? 13 : 14;
315
+ out.push((deltaNibble << 4) | lengthNibble);
316
+ if (deltaNibble === 13)
317
+ out.push(delta - 13);
318
+ else if (deltaNibble === 14)
319
+ out.push((delta - 269) >> 8, (delta - 269) & 0xff);
320
+ if (lengthNibble === 13)
321
+ out.push(length - 13);
322
+ else if (lengthNibble === 14)
323
+ out.push((length - 269) >> 8, (length - 269) & 0xff);
324
+ out.push(...entry.value);
325
+ previous = entry.number;
326
+ }
327
+ return Uint8Array.from(out);
328
+ }
329
+ function decodePlaintext(plaintext, outer) {
330
+ if (plaintext.length === 0)
331
+ throw new Error('Empty OSCORE plaintext');
332
+ const code = plaintext[0];
333
+ let offset = 1;
334
+ let optionNumber = 0;
335
+ const path = [];
336
+ const uriQuery = [];
337
+ let contentFormat;
338
+ let accept;
339
+ let observe;
340
+ let block1;
341
+ let block2;
342
+ let size1;
343
+ let size2;
344
+ let payload = new Uint8Array(0);
345
+ while (offset < plaintext.length) {
346
+ if (plaintext[offset] === 0xff) {
347
+ offset++;
348
+ payload = plaintext.slice(offset);
349
+ offset = plaintext.length;
350
+ break;
351
+ }
352
+ let delta = plaintext[offset] >> 4;
353
+ let length = plaintext[offset] & 0x0f;
354
+ offset++;
355
+ if (delta === 13) {
356
+ delta = plaintext[offset] + 13;
357
+ offset++;
358
+ }
359
+ else if (delta === 14) {
360
+ delta = ((plaintext[offset] << 8) | plaintext[offset + 1]) + 269;
361
+ offset += 2;
362
+ }
363
+ if (length === 13) {
364
+ length = plaintext[offset] + 13;
365
+ offset++;
366
+ }
367
+ else if (length === 14) {
368
+ length = ((plaintext[offset] << 8) | plaintext[offset + 1]) + 269;
369
+ offset += 2;
370
+ }
371
+ optionNumber += delta;
372
+ if (offset + length > plaintext.length)
373
+ throw new Error('Truncated OSCORE inner option');
374
+ const value = plaintext.slice(offset, offset + length);
375
+ offset += length;
376
+ if (optionNumber === 6)
377
+ observe = uintFromBytes(value);
378
+ else if (optionNumber === 11)
379
+ path.push(new TextDecoder().decode(value));
380
+ else if (optionNumber === 12)
381
+ contentFormat = uintFromBytes(value);
382
+ else if (optionNumber === 15)
383
+ uriQuery.push(new TextDecoder().decode(value));
384
+ else if (optionNumber === 17)
385
+ accept = uintFromBytes(value);
386
+ else if (optionNumber === 23)
387
+ block2 = decodeBlock(value);
388
+ else if (optionNumber === 27)
389
+ block1 = decodeBlock(value);
390
+ else if (optionNumber === 28)
391
+ size2 = uintFromBytes(value);
392
+ else if (optionNumber === 60)
393
+ size1 = uintFromBytes(value);
394
+ }
395
+ const message = {
396
+ type: outer.type,
397
+ messageId: outer.messageId,
398
+ token: outer.token,
399
+ path,
400
+ payload,
401
+ remote: outer.remote,
402
+ receivedAt: outer.receivedAt,
403
+ };
404
+ if (code >> 5 === 0) {
405
+ const method = CODE_METHODS[code];
406
+ if (method)
407
+ message.method = method;
408
+ }
409
+ else {
410
+ message.responseCode = formatResponseCode(code);
411
+ }
412
+ if (outer.uriHost !== undefined)
413
+ message.uriHost = outer.uriHost;
414
+ if (outer.uriPort !== undefined)
415
+ message.uriPort = outer.uriPort;
416
+ if (uriQuery.length > 0)
417
+ message.uriQuery = uriQuery;
418
+ if (contentFormat !== undefined)
419
+ message.contentFormat = contentFormat;
420
+ if (accept !== undefined)
421
+ message.accept = accept;
422
+ if (observe !== undefined)
423
+ message.observe = observe;
424
+ if (block1 !== undefined)
425
+ message.block1 = block1;
426
+ if (block2 !== undefined)
427
+ message.block2 = block2;
428
+ if (size1 !== undefined)
429
+ message.size1 = size1;
430
+ if (size2 !== undefined)
431
+ message.size2 = size2;
432
+ return message;
433
+ }
434
+ function cborHeader(major, length) {
435
+ if (length < 24)
436
+ return [(major << 5) | length];
437
+ if (length < 256)
438
+ return [(major << 5) | 24, length];
439
+ if (length < 65536)
440
+ return [(major << 5) | 25, (length >> 8) & 0xff, length & 0xff];
441
+ return [(major << 5) | 26, (length >>> 24) & 0xff, (length >>> 16) & 0xff, (length >>> 8) & 0xff, length & 0xff];
442
+ }
443
+ function cborUint(value) {
444
+ return cborHeader(0, value);
445
+ }
446
+ function cborBytes(value) {
447
+ return [...cborHeader(2, value.length), ...value];
448
+ }
449
+ function cborText(value) {
450
+ return [...cborHeader(3, value.length), ...new TextEncoder().encode(value)];
451
+ }
452
+ function cborArray(items) {
453
+ return [...cborHeader(4, items.length), ...items.flat()];
454
+ }
455
+ function uintOption(value) {
456
+ if (!Number.isInteger(value) || value < 0)
457
+ throw new Error(`Invalid CoAP option value ${value}`);
458
+ if (value === 0)
459
+ return new Uint8Array(0);
460
+ const bytes = [];
461
+ let remaining = value;
462
+ while (remaining > 0) {
463
+ bytes.unshift(remaining & 0xff);
464
+ remaining = Math.floor(remaining / 256);
465
+ }
466
+ return Uint8Array.from(bytes);
467
+ }
468
+ function uintFromBytes(value) {
469
+ if (value.length > 4)
470
+ throw new Error('CoAP option value exceeds 4 bytes');
471
+ let result = 0;
472
+ for (const byte of value)
473
+ result = result * 256 + byte;
474
+ return result;
475
+ }
476
+ function equalBytes(a, b) {
477
+ if (a.length !== b.length)
478
+ return false;
479
+ for (let i = 0; i < a.length; i++)
480
+ if (a[i] !== b[i])
481
+ return false;
482
+ return true;
483
+ }
484
+ function concat(parts) {
485
+ const length = parts.reduce((total, part) => total + part.length, 0);
486
+ const out = new Uint8Array(length);
487
+ let offset = 0;
488
+ for (const part of parts) {
489
+ out.set(part, offset);
490
+ offset += part.length;
491
+ }
492
+ return out;
493
+ }
494
+ class ReplayWindow {
495
+ constructor(size) {
496
+ this.size = size;
497
+ this.highest = -1;
498
+ this.bitmap = 0n;
499
+ if (!Number.isInteger(size) || size < 1)
500
+ throw new Error('Invalid OSCORE replay window size');
501
+ }
502
+ accept(sequence) {
503
+ if (sequence > this.highest) {
504
+ const shift = BigInt(sequence - this.highest);
505
+ this.bitmap = ((this.bitmap << shift) | 1n) & ((1n << BigInt(this.size)) - 1n);
506
+ this.highest = sequence;
507
+ return true;
508
+ }
509
+ const difference = this.highest - sequence;
510
+ if (difference >= this.size)
511
+ return false;
512
+ const bit = 1n << BigInt(difference);
513
+ if ((this.bitmap & bit) !== 0n)
514
+ return false;
515
+ this.bitmap |= bit;
516
+ return true;
517
+ }
518
+ }
519
+ function trimExchanges(exchanges) {
520
+ while (exchanges.size > 1024) {
521
+ const oldest = exchanges.keys().next().value;
522
+ if (oldest === undefined)
523
+ return;
524
+ exchanges.delete(oldest);
525
+ }
526
+ }