@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.
- package/LICENSE +202 -21
- package/NOTICE +60 -0
- package/README.md +201 -1
- package/dist/cbor.d.ts +22 -0
- package/dist/cbor.js +266 -0
- package/dist/cjs/cbor.d.ts +22 -0
- package/dist/cjs/cbor.js +271 -0
- package/dist/cjs/codec.d.ts +30 -0
- package/dist/cjs/codec.js +349 -0
- package/dist/cjs/gateway.d.ts +76 -0
- package/dist/cjs/gateway.js +511 -0
- package/dist/cjs/index.d.ts +13 -0
- package/dist/cjs/index.js +17 -0
- package/dist/cjs/native-transport.d.ts +33 -0
- package/dist/cjs/native-transport.js +183 -0
- package/dist/cjs/oscore.d.ts +32 -0
- package/dist/cjs/oscore.js +562 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/security.d.ts +29 -0
- package/dist/cjs/security.js +19 -0
- package/dist/cjs/sensor-bridge.d.ts +31 -0
- package/dist/cjs/sensor-bridge.js +99 -0
- package/dist/cjs/transport.d.ts +77 -0
- package/dist/cjs/transport.js +9 -0
- package/dist/codec.d.ts +30 -0
- package/dist/codec.js +339 -0
- package/dist/gateway.d.ts +61 -7
- package/dist/gateway.js +470 -67
- package/dist/index.d.ts +9 -3
- package/dist/index.js +6 -9
- package/dist/native-transport.d.ts +5 -3
- package/dist/native-transport.js +37 -18
- package/dist/oscore.d.ts +32 -0
- package/dist/oscore.js +526 -0
- package/dist/security.d.ts +29 -0
- package/dist/security.js +16 -0
- package/dist/sensor-bridge.d.ts +6 -1
- package/dist/sensor-bridge.js +73 -24
- package/dist/transport.d.ts +31 -1
- package/dist/transport.js +1 -2
- package/package.json +27 -16
- package/sidecar.json +34 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OSCORE (RFC 8613) security context for Node.js.
|
|
4
|
+
*
|
|
5
|
+
* The connector stays crypto-free; this adapter implements the OSCORE client
|
|
6
|
+
* and server roles behind `CoapSecurityPort` using Node's `aes-128-ccm` and
|
|
7
|
+
* HKDF-SHA-256. It is validated against the RFC 8613 Appendix C vectors.
|
|
8
|
+
*
|
|
9
|
+
* Supported profile: AES-CCM-16-64-128 (alg 10), HKDF-SHA-256, no Class I
|
|
10
|
+
* options (none are currently defined), and the CoAP fields modeled by
|
|
11
|
+
* `CoapMessage`. Uri-Host/Uri-Port remain outer (Class U); Uri-Path and
|
|
12
|
+
* Uri-Query are inner (Class E). Block options are inner. Blockwise
|
|
13
|
+
* observations and multicast are outside this adapter's scope.
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.createOscoreContext = createOscoreContext;
|
|
50
|
+
const codec_js_1 = require("./codec.js");
|
|
51
|
+
const AEAD_ALGORITHM = 10; // AES-CCM-16-64-128
|
|
52
|
+
const KEY_LENGTH = 16;
|
|
53
|
+
const NONCE_LENGTH = 13;
|
|
54
|
+
const TAG_LENGTH = 8;
|
|
55
|
+
const MAX_PIV = 0xffffffffff; // 5 bytes
|
|
56
|
+
const METHOD_CODES = { GET: 1, POST: 2, PUT: 3, DELETE: 4, FETCH: 5 };
|
|
57
|
+
const CODE_METHODS = { 1: 'GET', 2: 'POST', 3: 'PUT', 4: 'DELETE', 5: 'FETCH' };
|
|
58
|
+
async function createOscoreContext(config) {
|
|
59
|
+
const nodeCrypto = await Promise.resolve().then(() => __importStar(require('node:crypto')));
|
|
60
|
+
const masterSalt = config.masterSalt ?? new Uint8Array(0);
|
|
61
|
+
const idContext = config.idContext;
|
|
62
|
+
if (config.senderId.length > NONCE_LENGTH - 6)
|
|
63
|
+
throw new Error('OSCORE sender ID exceeds 7 bytes');
|
|
64
|
+
if (config.recipientId.length > NONCE_LENGTH - 6)
|
|
65
|
+
throw new Error('OSCORE recipient ID exceeds 7 bytes');
|
|
66
|
+
const senderKey = deriveKey(nodeCrypto, config.masterSecret, masterSalt, config.senderId, idContext, 'Key', KEY_LENGTH);
|
|
67
|
+
const recipientKey = deriveKey(nodeCrypto, config.masterSecret, masterSalt, config.recipientId, idContext, 'Key', KEY_LENGTH);
|
|
68
|
+
const commonIv = deriveKey(nodeCrypto, config.masterSecret, masterSalt, new Uint8Array(0), idContext, 'IV', NONCE_LENGTH);
|
|
69
|
+
let sequence = config.senderSequenceNumber ?? 0;
|
|
70
|
+
if (!Number.isInteger(sequence) || sequence < 0 || sequence > MAX_PIV)
|
|
71
|
+
throw new Error('Invalid OSCORE sender sequence number');
|
|
72
|
+
const exchanges = new Map();
|
|
73
|
+
const requestReplay = new ReplayWindow(config.replayWindowSize ?? 32);
|
|
74
|
+
const responseReplay = new ReplayWindow(config.replayWindowSize ?? 32);
|
|
75
|
+
function protects(host, port) {
|
|
76
|
+
if (!config.endpoints || config.endpoints.length === 0)
|
|
77
|
+
return true;
|
|
78
|
+
return config.endpoints.some((endpoint) => endpoint.host === host && endpoint.port === port);
|
|
79
|
+
}
|
|
80
|
+
function nextPiv() {
|
|
81
|
+
if (sequence > MAX_PIV)
|
|
82
|
+
throw new Error('OSCORE sender sequence exhausted');
|
|
83
|
+
const piv = encodePiv(sequence);
|
|
84
|
+
sequence++;
|
|
85
|
+
return piv;
|
|
86
|
+
}
|
|
87
|
+
function protectRequest(message) {
|
|
88
|
+
const method = message.method;
|
|
89
|
+
const code = METHOD_CODES[method];
|
|
90
|
+
if (code === undefined)
|
|
91
|
+
throw new Error(`Unsupported OSCORE method ${method}`);
|
|
92
|
+
const piv = nextPiv();
|
|
93
|
+
const plaintext = encodePlaintext(code, message);
|
|
94
|
+
const aad = buildAad(config.senderId, piv);
|
|
95
|
+
const nonce = buildNonce(commonIv, config.senderId, piv);
|
|
96
|
+
const ciphertext = encrypt(nodeCrypto, senderKey, nonce, aad, plaintext);
|
|
97
|
+
exchanges.set((0, codec_js_1.tokenToKey)(message.token), { kid: config.senderId.slice(), piv, nonce });
|
|
98
|
+
trimExchanges(exchanges);
|
|
99
|
+
return {
|
|
100
|
+
type: message.type,
|
|
101
|
+
method: message.observe !== undefined ? 'FETCH' : 'POST',
|
|
102
|
+
messageId: message.messageId,
|
|
103
|
+
token: message.token,
|
|
104
|
+
path: [],
|
|
105
|
+
...(message.uriHost !== undefined ? { uriHost: message.uriHost } : {}),
|
|
106
|
+
...(message.uriPort !== undefined ? { uriPort: message.uriPort } : {}),
|
|
107
|
+
payload: ciphertext,
|
|
108
|
+
...(message.observe !== undefined ? { observe: message.observe } : {}),
|
|
109
|
+
oscore: encodeOscoreOption(piv, { kid: config.senderId, kidContext: idContext, includeKid: true }),
|
|
110
|
+
remote: message.remote,
|
|
111
|
+
receivedAt: message.receivedAt,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function protectResponse(message) {
|
|
115
|
+
const key = (0, codec_js_1.tokenToKey)(message.token);
|
|
116
|
+
const state = exchanges.get(key);
|
|
117
|
+
if (!state)
|
|
118
|
+
throw new Error('No matching OSCORE request context for the response');
|
|
119
|
+
const code = (0, codec_js_1.parseResponseCode)(message.responseCode);
|
|
120
|
+
const plaintext = encodePlaintext(code, message);
|
|
121
|
+
const aad = buildAad(state.kid, state.piv);
|
|
122
|
+
// Notifications carry a fresh Partial IV; a single response reuses the
|
|
123
|
+
// request nonce and omits the Partial IV (RFC 8613 §8.3).
|
|
124
|
+
const notification = message.observe !== undefined;
|
|
125
|
+
const piv = notification ? nextPiv() : new Uint8Array(0);
|
|
126
|
+
const nonce = notification ? buildNonce(commonIv, config.senderId, piv) : state.nonce;
|
|
127
|
+
const ciphertext = encrypt(nodeCrypto, senderKey, nonce, aad, plaintext);
|
|
128
|
+
if (!notification)
|
|
129
|
+
exchanges.delete(key);
|
|
130
|
+
return {
|
|
131
|
+
type: message.type,
|
|
132
|
+
responseCode: notification ? '2.05' : '2.04',
|
|
133
|
+
messageId: message.messageId,
|
|
134
|
+
token: message.token,
|
|
135
|
+
path: [],
|
|
136
|
+
...(message.uriHost !== undefined ? { uriHost: message.uriHost } : {}),
|
|
137
|
+
...(message.uriPort !== undefined ? { uriPort: message.uriPort } : {}),
|
|
138
|
+
payload: ciphertext,
|
|
139
|
+
...(message.observe !== undefined ? { observe: message.observe } : {}),
|
|
140
|
+
oscore: notification
|
|
141
|
+
? encodeOscoreOption(piv, { includeKid: false })
|
|
142
|
+
: new Uint8Array(0),
|
|
143
|
+
remote: message.remote,
|
|
144
|
+
receivedAt: message.receivedAt,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function protect(message) {
|
|
148
|
+
if (message.method !== undefined)
|
|
149
|
+
return protectRequest(message);
|
|
150
|
+
if (message.responseCode !== undefined)
|
|
151
|
+
return protectResponse(message);
|
|
152
|
+
throw new Error('OSCORE protection requires a request method or a response code');
|
|
153
|
+
}
|
|
154
|
+
function unprotectRequest(message) {
|
|
155
|
+
const option = decodeOscoreOption(message.oscore);
|
|
156
|
+
if (!option.kid)
|
|
157
|
+
throw new Error('OSCORE request is missing kid');
|
|
158
|
+
if (!equalBytes(option.kid, config.recipientId))
|
|
159
|
+
throw new Error('Unknown OSCORE recipient ID');
|
|
160
|
+
if (option.kidContext && !equalBytes(option.kidContext, idContext ?? new Uint8Array(0))) {
|
|
161
|
+
throw new Error('Unknown OSCORE ID context');
|
|
162
|
+
}
|
|
163
|
+
if (option.piv.length === 0)
|
|
164
|
+
throw new Error('OSCORE request is missing a Partial IV');
|
|
165
|
+
if (!requestReplay.accept(uintFromBytes(option.piv)))
|
|
166
|
+
throw new Error('OSCORE replay detected');
|
|
167
|
+
const aad = buildAad(option.kid, option.piv);
|
|
168
|
+
const nonce = buildNonce(commonIv, option.kid, option.piv);
|
|
169
|
+
const plaintext = decryptPayload(nodeCrypto, recipientKey, nonce, aad, message.payload);
|
|
170
|
+
const decoded = decodePlaintext(plaintext, message);
|
|
171
|
+
exchanges.set((0, codec_js_1.tokenToKey)(message.token), { kid: option.kid.slice(), piv: option.piv, nonce });
|
|
172
|
+
trimExchanges(exchanges);
|
|
173
|
+
return decoded;
|
|
174
|
+
}
|
|
175
|
+
function unprotectResponse(message) {
|
|
176
|
+
const option = decodeOscoreOption(message.oscore);
|
|
177
|
+
if (option.kid && !equalBytes(option.kid, config.recipientId))
|
|
178
|
+
throw new Error('Unknown OSCORE recipient ID');
|
|
179
|
+
if (option.kidContext && !equalBytes(option.kidContext, idContext ?? new Uint8Array(0))) {
|
|
180
|
+
throw new Error('Unknown OSCORE ID context');
|
|
181
|
+
}
|
|
182
|
+
const state = exchanges.get((0, codec_js_1.tokenToKey)(message.token));
|
|
183
|
+
if (!state)
|
|
184
|
+
throw new Error('No matching OSCORE request context');
|
|
185
|
+
const hasPiv = option.piv.length > 0;
|
|
186
|
+
if (hasPiv && !responseReplay.accept(uintFromBytes(option.piv)))
|
|
187
|
+
throw new Error('OSCORE replay detected');
|
|
188
|
+
const nonce = hasPiv
|
|
189
|
+
? buildNonce(commonIv, config.recipientId, option.piv)
|
|
190
|
+
: state.nonce;
|
|
191
|
+
const aad = buildAad(state.kid, state.piv);
|
|
192
|
+
const plaintext = decryptPayload(nodeCrypto, recipientKey, nonce, aad, message.payload);
|
|
193
|
+
return decodePlaintext(plaintext, message);
|
|
194
|
+
}
|
|
195
|
+
async function unprotect(message) {
|
|
196
|
+
if (message.oscore === undefined)
|
|
197
|
+
throw new Error('Missing OSCORE option');
|
|
198
|
+
return message.method !== undefined ? unprotectRequest(message) : unprotectResponse(message);
|
|
199
|
+
}
|
|
200
|
+
return { protects, protect, unprotect };
|
|
201
|
+
}
|
|
202
|
+
function encodePlaintext(code, message) {
|
|
203
|
+
const innerOptions = [];
|
|
204
|
+
if (message.observe !== undefined)
|
|
205
|
+
innerOptions.push({ number: 6, value: uintOption(message.observe) });
|
|
206
|
+
for (const segment of message.path)
|
|
207
|
+
innerOptions.push({ number: 11, value: new TextEncoder().encode(segment) });
|
|
208
|
+
if (message.contentFormat !== undefined)
|
|
209
|
+
innerOptions.push({ number: 12, value: uintOption(message.contentFormat) });
|
|
210
|
+
for (const query of message.uriQuery ?? [])
|
|
211
|
+
innerOptions.push({ number: 15, value: new TextEncoder().encode(query) });
|
|
212
|
+
if (message.accept !== undefined)
|
|
213
|
+
innerOptions.push({ number: 17, value: uintOption(message.accept) });
|
|
214
|
+
if (message.block2)
|
|
215
|
+
innerOptions.push({ number: 23, value: uintOption((0, codec_js_1.encodeBlock)(message.block2)) });
|
|
216
|
+
if (message.block1)
|
|
217
|
+
innerOptions.push({ number: 27, value: uintOption((0, codec_js_1.encodeBlock)(message.block1)) });
|
|
218
|
+
if (message.size2 !== undefined)
|
|
219
|
+
innerOptions.push({ number: 28, value: uintOption(message.size2) });
|
|
220
|
+
if (message.size1 !== undefined)
|
|
221
|
+
innerOptions.push({ number: 60, value: uintOption(message.size1) });
|
|
222
|
+
return concat([
|
|
223
|
+
Uint8Array.of(code),
|
|
224
|
+
encodeOptions(innerOptions),
|
|
225
|
+
message.payload.length ? Uint8Array.of(0xff) : new Uint8Array(0),
|
|
226
|
+
message.payload,
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
function deriveKey(nodeCrypto, masterSecret, masterSalt, id, idContext, type, length) {
|
|
230
|
+
const info = cborArray([
|
|
231
|
+
cborBytes(id),
|
|
232
|
+
idContext ? cborBytes(idContext) : [0xf6],
|
|
233
|
+
cborUint(AEAD_ALGORITHM),
|
|
234
|
+
cborText(type),
|
|
235
|
+
cborUint(length),
|
|
236
|
+
]);
|
|
237
|
+
const derived = nodeCrypto.hkdfSync('sha256', Buffer.from(masterSecret), Buffer.from(masterSalt), Buffer.from(Uint8Array.from(info)), length);
|
|
238
|
+
return new Uint8Array(derived);
|
|
239
|
+
}
|
|
240
|
+
function buildAad(requestKid, requestPiv) {
|
|
241
|
+
const aadArray = cborArray([
|
|
242
|
+
cborUint(1),
|
|
243
|
+
cborArray([cborUint(AEAD_ALGORITHM)]),
|
|
244
|
+
cborBytes(requestKid),
|
|
245
|
+
cborBytes(requestPiv),
|
|
246
|
+
cborBytes(new Uint8Array(0)),
|
|
247
|
+
]);
|
|
248
|
+
const external = cborBytes(Uint8Array.from(aadArray));
|
|
249
|
+
return Uint8Array.from(cborArray([cborText('Encrypt0'), cborBytes(new Uint8Array(0)), external]));
|
|
250
|
+
}
|
|
251
|
+
function buildNonce(commonIv, id, piv) {
|
|
252
|
+
if (piv.length > 5)
|
|
253
|
+
throw new Error('OSCORE Partial IV exceeds 5 bytes');
|
|
254
|
+
if (id.length > 7)
|
|
255
|
+
throw new Error('OSCORE ID exceeds 7 bytes');
|
|
256
|
+
const pad = new Uint8Array(NONCE_LENGTH);
|
|
257
|
+
pad[0] = id.length;
|
|
258
|
+
// ID and PIV are left-padded within their fields: ID in bytes 1..7, PIV in
|
|
259
|
+
// bytes 8..12 (RFC 8613 §5.2, Figure 8).
|
|
260
|
+
pad.set(id, 8 - id.length);
|
|
261
|
+
pad.set(piv, NONCE_LENGTH - piv.length);
|
|
262
|
+
return Uint8Array.from(commonIv, (byte, index) => byte ^ pad[index]);
|
|
263
|
+
}
|
|
264
|
+
function encrypt(nodeCrypto, key, nonce, aad, plaintext) {
|
|
265
|
+
const cipher = nodeCrypto.createCipheriv('aes-128-ccm', Buffer.from(key), Buffer.from(nonce), { authTagLength: TAG_LENGTH });
|
|
266
|
+
cipher.setAAD(Buffer.from(aad), { plaintextLength: plaintext.length });
|
|
267
|
+
const ciphertext = Buffer.concat([cipher.update(Buffer.from(plaintext)), cipher.final()]);
|
|
268
|
+
const tag = cipher.getAuthTag();
|
|
269
|
+
const out = new Uint8Array(ciphertext.length + tag.length);
|
|
270
|
+
out.set(ciphertext, 0);
|
|
271
|
+
out.set(tag, ciphertext.length);
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
function decryptPayload(nodeCrypto, key, nonce, aad, payload) {
|
|
275
|
+
if (payload.length < TAG_LENGTH)
|
|
276
|
+
throw new Error('OSCORE payload is too short');
|
|
277
|
+
const ciphertext = payload.slice(0, payload.length - TAG_LENGTH);
|
|
278
|
+
const tag = payload.slice(payload.length - TAG_LENGTH);
|
|
279
|
+
const decipher = nodeCrypto.createDecipheriv('aes-128-ccm', Buffer.from(key), Buffer.from(nonce), { authTagLength: TAG_LENGTH });
|
|
280
|
+
decipher.setAAD(Buffer.from(aad), { plaintextLength: ciphertext.length });
|
|
281
|
+
decipher.setAuthTag(Buffer.from(tag));
|
|
282
|
+
try {
|
|
283
|
+
return new Uint8Array(Buffer.concat([decipher.update(Buffer.from(ciphertext)), decipher.final()]));
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
throw new Error('OSCORE authentication failed');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function encodePiv(sequence) {
|
|
290
|
+
if (sequence === 0)
|
|
291
|
+
return Uint8Array.of(0);
|
|
292
|
+
const bytes = [];
|
|
293
|
+
let remaining = sequence;
|
|
294
|
+
while (remaining > 0) {
|
|
295
|
+
bytes.unshift(remaining & 0xff);
|
|
296
|
+
remaining = Math.floor(remaining / 256);
|
|
297
|
+
}
|
|
298
|
+
return Uint8Array.from(bytes);
|
|
299
|
+
}
|
|
300
|
+
function encodeOscoreOption(piv, { kid, kidContext, includeKid }) {
|
|
301
|
+
const flags = (kidContext ? 0x10 : 0) | (includeKid ? 0x08 : 0) | piv.length;
|
|
302
|
+
const out = [flags, ...piv];
|
|
303
|
+
if (kidContext)
|
|
304
|
+
out.push(kidContext.length, ...kidContext);
|
|
305
|
+
if (includeKid && kid)
|
|
306
|
+
out.push(...kid);
|
|
307
|
+
return Uint8Array.from(out);
|
|
308
|
+
}
|
|
309
|
+
function decodeOscoreOption(value) {
|
|
310
|
+
if (value.length === 0)
|
|
311
|
+
return { piv: new Uint8Array(0) };
|
|
312
|
+
const flags = value[0];
|
|
313
|
+
if ((flags & 0xe0) !== 0)
|
|
314
|
+
throw new Error('Reserved OSCORE flag bits set');
|
|
315
|
+
const length = flags & 0x07;
|
|
316
|
+
if (length === 6 || length === 7)
|
|
317
|
+
throw new Error('Reserved OSCORE Partial IV length');
|
|
318
|
+
let offset = 1;
|
|
319
|
+
if (offset + length > value.length)
|
|
320
|
+
throw new Error('Truncated OSCORE Partial IV');
|
|
321
|
+
const piv = value.slice(offset, offset + length);
|
|
322
|
+
offset += length;
|
|
323
|
+
let kidContext;
|
|
324
|
+
if (flags & 0x10) {
|
|
325
|
+
if (offset >= value.length)
|
|
326
|
+
throw new Error('Truncated OSCORE kid context');
|
|
327
|
+
const contextLength = value[offset++];
|
|
328
|
+
if (offset + contextLength > value.length)
|
|
329
|
+
throw new Error('Truncated OSCORE kid context');
|
|
330
|
+
kidContext = value.slice(offset, offset + contextLength);
|
|
331
|
+
offset += contextLength;
|
|
332
|
+
}
|
|
333
|
+
let kid;
|
|
334
|
+
if (flags & 0x08) {
|
|
335
|
+
kid = value.slice(offset);
|
|
336
|
+
offset = value.length;
|
|
337
|
+
}
|
|
338
|
+
if (offset !== value.length)
|
|
339
|
+
throw new Error('Trailing OSCORE option bytes');
|
|
340
|
+
return { piv, kid, kidContext };
|
|
341
|
+
}
|
|
342
|
+
function encodeOptions(entries) {
|
|
343
|
+
const sorted = [...entries].sort((a, b) => a.number - b.number);
|
|
344
|
+
const out = [];
|
|
345
|
+
let previous = 0;
|
|
346
|
+
for (const entry of sorted) {
|
|
347
|
+
const delta = entry.number - previous;
|
|
348
|
+
const length = entry.value.length;
|
|
349
|
+
const deltaNibble = delta < 13 ? delta : delta < 269 ? 13 : 14;
|
|
350
|
+
const lengthNibble = length < 13 ? length : length < 269 ? 13 : 14;
|
|
351
|
+
out.push((deltaNibble << 4) | lengthNibble);
|
|
352
|
+
if (deltaNibble === 13)
|
|
353
|
+
out.push(delta - 13);
|
|
354
|
+
else if (deltaNibble === 14)
|
|
355
|
+
out.push((delta - 269) >> 8, (delta - 269) & 0xff);
|
|
356
|
+
if (lengthNibble === 13)
|
|
357
|
+
out.push(length - 13);
|
|
358
|
+
else if (lengthNibble === 14)
|
|
359
|
+
out.push((length - 269) >> 8, (length - 269) & 0xff);
|
|
360
|
+
out.push(...entry.value);
|
|
361
|
+
previous = entry.number;
|
|
362
|
+
}
|
|
363
|
+
return Uint8Array.from(out);
|
|
364
|
+
}
|
|
365
|
+
function decodePlaintext(plaintext, outer) {
|
|
366
|
+
if (plaintext.length === 0)
|
|
367
|
+
throw new Error('Empty OSCORE plaintext');
|
|
368
|
+
const code = plaintext[0];
|
|
369
|
+
let offset = 1;
|
|
370
|
+
let optionNumber = 0;
|
|
371
|
+
const path = [];
|
|
372
|
+
const uriQuery = [];
|
|
373
|
+
let contentFormat;
|
|
374
|
+
let accept;
|
|
375
|
+
let observe;
|
|
376
|
+
let block1;
|
|
377
|
+
let block2;
|
|
378
|
+
let size1;
|
|
379
|
+
let size2;
|
|
380
|
+
let payload = new Uint8Array(0);
|
|
381
|
+
while (offset < plaintext.length) {
|
|
382
|
+
if (plaintext[offset] === 0xff) {
|
|
383
|
+
offset++;
|
|
384
|
+
payload = plaintext.slice(offset);
|
|
385
|
+
offset = plaintext.length;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
let delta = plaintext[offset] >> 4;
|
|
389
|
+
let length = plaintext[offset] & 0x0f;
|
|
390
|
+
offset++;
|
|
391
|
+
if (delta === 13) {
|
|
392
|
+
delta = plaintext[offset] + 13;
|
|
393
|
+
offset++;
|
|
394
|
+
}
|
|
395
|
+
else if (delta === 14) {
|
|
396
|
+
delta = ((plaintext[offset] << 8) | plaintext[offset + 1]) + 269;
|
|
397
|
+
offset += 2;
|
|
398
|
+
}
|
|
399
|
+
if (length === 13) {
|
|
400
|
+
length = plaintext[offset] + 13;
|
|
401
|
+
offset++;
|
|
402
|
+
}
|
|
403
|
+
else if (length === 14) {
|
|
404
|
+
length = ((plaintext[offset] << 8) | plaintext[offset + 1]) + 269;
|
|
405
|
+
offset += 2;
|
|
406
|
+
}
|
|
407
|
+
optionNumber += delta;
|
|
408
|
+
if (offset + length > plaintext.length)
|
|
409
|
+
throw new Error('Truncated OSCORE inner option');
|
|
410
|
+
const value = plaintext.slice(offset, offset + length);
|
|
411
|
+
offset += length;
|
|
412
|
+
if (optionNumber === 6)
|
|
413
|
+
observe = uintFromBytes(value);
|
|
414
|
+
else if (optionNumber === 11)
|
|
415
|
+
path.push(new TextDecoder().decode(value));
|
|
416
|
+
else if (optionNumber === 12)
|
|
417
|
+
contentFormat = uintFromBytes(value);
|
|
418
|
+
else if (optionNumber === 15)
|
|
419
|
+
uriQuery.push(new TextDecoder().decode(value));
|
|
420
|
+
else if (optionNumber === 17)
|
|
421
|
+
accept = uintFromBytes(value);
|
|
422
|
+
else if (optionNumber === 23)
|
|
423
|
+
block2 = (0, codec_js_1.decodeBlock)(value);
|
|
424
|
+
else if (optionNumber === 27)
|
|
425
|
+
block1 = (0, codec_js_1.decodeBlock)(value);
|
|
426
|
+
else if (optionNumber === 28)
|
|
427
|
+
size2 = uintFromBytes(value);
|
|
428
|
+
else if (optionNumber === 60)
|
|
429
|
+
size1 = uintFromBytes(value);
|
|
430
|
+
}
|
|
431
|
+
const message = {
|
|
432
|
+
type: outer.type,
|
|
433
|
+
messageId: outer.messageId,
|
|
434
|
+
token: outer.token,
|
|
435
|
+
path,
|
|
436
|
+
payload,
|
|
437
|
+
remote: outer.remote,
|
|
438
|
+
receivedAt: outer.receivedAt,
|
|
439
|
+
};
|
|
440
|
+
if (code >> 5 === 0) {
|
|
441
|
+
const method = CODE_METHODS[code];
|
|
442
|
+
if (method)
|
|
443
|
+
message.method = method;
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
message.responseCode = (0, codec_js_1.formatResponseCode)(code);
|
|
447
|
+
}
|
|
448
|
+
if (outer.uriHost !== undefined)
|
|
449
|
+
message.uriHost = outer.uriHost;
|
|
450
|
+
if (outer.uriPort !== undefined)
|
|
451
|
+
message.uriPort = outer.uriPort;
|
|
452
|
+
if (uriQuery.length > 0)
|
|
453
|
+
message.uriQuery = uriQuery;
|
|
454
|
+
if (contentFormat !== undefined)
|
|
455
|
+
message.contentFormat = contentFormat;
|
|
456
|
+
if (accept !== undefined)
|
|
457
|
+
message.accept = accept;
|
|
458
|
+
if (observe !== undefined)
|
|
459
|
+
message.observe = observe;
|
|
460
|
+
if (block1 !== undefined)
|
|
461
|
+
message.block1 = block1;
|
|
462
|
+
if (block2 !== undefined)
|
|
463
|
+
message.block2 = block2;
|
|
464
|
+
if (size1 !== undefined)
|
|
465
|
+
message.size1 = size1;
|
|
466
|
+
if (size2 !== undefined)
|
|
467
|
+
message.size2 = size2;
|
|
468
|
+
return message;
|
|
469
|
+
}
|
|
470
|
+
function cborHeader(major, length) {
|
|
471
|
+
if (length < 24)
|
|
472
|
+
return [(major << 5) | length];
|
|
473
|
+
if (length < 256)
|
|
474
|
+
return [(major << 5) | 24, length];
|
|
475
|
+
if (length < 65536)
|
|
476
|
+
return [(major << 5) | 25, (length >> 8) & 0xff, length & 0xff];
|
|
477
|
+
return [(major << 5) | 26, (length >>> 24) & 0xff, (length >>> 16) & 0xff, (length >>> 8) & 0xff, length & 0xff];
|
|
478
|
+
}
|
|
479
|
+
function cborUint(value) {
|
|
480
|
+
return cborHeader(0, value);
|
|
481
|
+
}
|
|
482
|
+
function cborBytes(value) {
|
|
483
|
+
return [...cborHeader(2, value.length), ...value];
|
|
484
|
+
}
|
|
485
|
+
function cborText(value) {
|
|
486
|
+
return [...cborHeader(3, value.length), ...new TextEncoder().encode(value)];
|
|
487
|
+
}
|
|
488
|
+
function cborArray(items) {
|
|
489
|
+
return [...cborHeader(4, items.length), ...items.flat()];
|
|
490
|
+
}
|
|
491
|
+
function uintOption(value) {
|
|
492
|
+
if (!Number.isInteger(value) || value < 0)
|
|
493
|
+
throw new Error(`Invalid CoAP option value ${value}`);
|
|
494
|
+
if (value === 0)
|
|
495
|
+
return new Uint8Array(0);
|
|
496
|
+
const bytes = [];
|
|
497
|
+
let remaining = value;
|
|
498
|
+
while (remaining > 0) {
|
|
499
|
+
bytes.unshift(remaining & 0xff);
|
|
500
|
+
remaining = Math.floor(remaining / 256);
|
|
501
|
+
}
|
|
502
|
+
return Uint8Array.from(bytes);
|
|
503
|
+
}
|
|
504
|
+
function uintFromBytes(value) {
|
|
505
|
+
if (value.length > 4)
|
|
506
|
+
throw new Error('CoAP option value exceeds 4 bytes');
|
|
507
|
+
let result = 0;
|
|
508
|
+
for (const byte of value)
|
|
509
|
+
result = result * 256 + byte;
|
|
510
|
+
return result;
|
|
511
|
+
}
|
|
512
|
+
function equalBytes(a, b) {
|
|
513
|
+
if (a.length !== b.length)
|
|
514
|
+
return false;
|
|
515
|
+
for (let i = 0; i < a.length; i++)
|
|
516
|
+
if (a[i] !== b[i])
|
|
517
|
+
return false;
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
function concat(parts) {
|
|
521
|
+
const length = parts.reduce((total, part) => total + part.length, 0);
|
|
522
|
+
const out = new Uint8Array(length);
|
|
523
|
+
let offset = 0;
|
|
524
|
+
for (const part of parts) {
|
|
525
|
+
out.set(part, offset);
|
|
526
|
+
offset += part.length;
|
|
527
|
+
}
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
class ReplayWindow {
|
|
531
|
+
constructor(size) {
|
|
532
|
+
this.size = size;
|
|
533
|
+
this.highest = -1;
|
|
534
|
+
this.bitmap = 0n;
|
|
535
|
+
if (!Number.isInteger(size) || size < 1)
|
|
536
|
+
throw new Error('Invalid OSCORE replay window size');
|
|
537
|
+
}
|
|
538
|
+
accept(sequence) {
|
|
539
|
+
if (sequence > this.highest) {
|
|
540
|
+
const shift = BigInt(sequence - this.highest);
|
|
541
|
+
this.bitmap = ((this.bitmap << shift) | 1n) & ((1n << BigInt(this.size)) - 1n);
|
|
542
|
+
this.highest = sequence;
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
const difference = this.highest - sequence;
|
|
546
|
+
if (difference >= this.size)
|
|
547
|
+
return false;
|
|
548
|
+
const bit = 1n << BigInt(difference);
|
|
549
|
+
if ((this.bitmap & bit) !== 0n)
|
|
550
|
+
return false;
|
|
551
|
+
this.bitmap |= bit;
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function trimExchanges(exchanges) {
|
|
556
|
+
while (exchanges.size > 1024) {
|
|
557
|
+
const oldest = exchanges.keys().next().value;
|
|
558
|
+
if (oldest === undefined)
|
|
559
|
+
return;
|
|
560
|
+
exchanges.delete(oldest);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoAP security boundary.
|
|
3
|
+
*
|
|
4
|
+
* The connector owns framing, correlation and observation semantics, but not
|
|
5
|
+
* cryptographic primitives. OSCORE (RFC 8613) contexts are supplied by a
|
|
6
|
+
* reviewed implementation behind this port: the port decides which endpoints
|
|
7
|
+
* are protected, applies/removes the OSCORE option and authenticates payloads.
|
|
8
|
+
*
|
|
9
|
+
* A gateway with a security port fails closed: protected endpoints must send
|
|
10
|
+
* OSCORE messages, and OSCORE messages from unconfigured endpoints are dropped
|
|
11
|
+
* rather than interpreted as plaintext.
|
|
12
|
+
*/
|
|
13
|
+
import type { CoapMessage } from './transport.js';
|
|
14
|
+
export interface CoapSecurityPort {
|
|
15
|
+
/** Whether outbound traffic to (and inbound traffic from) this endpoint must be protected. */
|
|
16
|
+
protects(host: string, port: number): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Add OSCORE protection. The implementation may replace `payload`, add the
|
|
19
|
+
* OSCORE option and must preserve `messageId` and `token`.
|
|
20
|
+
*/
|
|
21
|
+
protect(message: CoapMessage): Promise<CoapMessage>;
|
|
22
|
+
/**
|
|
23
|
+
* Authenticate and decrypt an inbound OSCORE message. Must throw on replay,
|
|
24
|
+
* authentication failure or an unknown context; the gateway drops the message.
|
|
25
|
+
*/
|
|
26
|
+
unprotect(message: CoapMessage): Promise<CoapMessage>;
|
|
27
|
+
}
|
|
28
|
+
/** True when a message carries an OSCORE option (it is not plaintext). */
|
|
29
|
+
export declare function isOscoreProtected(message: CoapMessage): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CoAP security boundary.
|
|
4
|
+
*
|
|
5
|
+
* The connector owns framing, correlation and observation semantics, but not
|
|
6
|
+
* cryptographic primitives. OSCORE (RFC 8613) contexts are supplied by a
|
|
7
|
+
* reviewed implementation behind this port: the port decides which endpoints
|
|
8
|
+
* are protected, applies/removes the OSCORE option and authenticates payloads.
|
|
9
|
+
*
|
|
10
|
+
* A gateway with a security port fails closed: protected endpoints must send
|
|
11
|
+
* OSCORE messages, and OSCORE messages from unconfigured endpoints are dropped
|
|
12
|
+
* rather than interpreted as plaintext.
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.isOscoreProtected = isOscoreProtected;
|
|
16
|
+
/** True when a message carries an OSCORE option (it is not plaintext). */
|
|
17
|
+
function isOscoreProtected(message) {
|
|
18
|
+
return message.oscore !== undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoAP Sensor Bridge — maps CoAP resources and RFC 7641 observations to Edge
|
|
3
|
+
* proof inputs. CBOR payloads (content-format 60) are decoded into structured
|
|
4
|
+
* claims; malformed CBOR is not signed as sensor data.
|
|
5
|
+
*/
|
|
6
|
+
import type { EdgeRuntime } from '@totemsdk/edge';
|
|
7
|
+
import type { CoapTransportPort } from './transport.js';
|
|
8
|
+
import type { CoapGateway } from './gateway.js';
|
|
9
|
+
export interface CoapSensorBinding {
|
|
10
|
+
sensorId: string;
|
|
11
|
+
path: string[];
|
|
12
|
+
host: string;
|
|
13
|
+
port: number;
|
|
14
|
+
intervalMs: number;
|
|
15
|
+
dataType?: string;
|
|
16
|
+
/** Register an RFC 7641 observation instead of polling for updates. */
|
|
17
|
+
observe?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface CoapSensorBridgeConfig {
|
|
20
|
+
runtime: EdgeRuntime;
|
|
21
|
+
transport: CoapTransportPort;
|
|
22
|
+
gateway?: CoapGateway;
|
|
23
|
+
bindings: CoapSensorBinding[];
|
|
24
|
+
}
|
|
25
|
+
export interface CoapSensorBridge {
|
|
26
|
+
start(): Promise<void>;
|
|
27
|
+
stop(): Promise<void>;
|
|
28
|
+
poll(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export declare const APPLICATION_CBOR = 60;
|
|
31
|
+
export declare function createCoapSensorBridge(config: CoapSensorBridgeConfig): CoapSensorBridge;
|