bedrock-portal-nethernet 2.4.0 → 2.5.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.
Files changed (40) hide show
  1. package/README.md +11 -11
  2. package/dist/handshake/authorizationKeys.d.ts +6 -4
  3. package/dist/handshake/authorizationKeys.js +15 -52
  4. package/dist/handshake/keyExchange.js +13 -5
  5. package/dist/handshake/loginVerify.js +18 -27
  6. package/dist/nethernet/connection.d.ts +10 -2
  7. package/dist/nethernet/connection.js +72 -9
  8. package/dist/nethernet/identity.d.ts +9 -0
  9. package/dist/nethernet/identity.js +116 -0
  10. package/dist/nethernet/server.d.ts +7 -3
  11. package/dist/nethernet/server.js +71 -14
  12. package/dist/options.d.ts +1 -1
  13. package/dist/options.js +2 -2
  14. package/dist/protocol.d.ts +1332 -776
  15. package/dist/protocol.js +2724 -2288
  16. package/dist/server.d.ts +3 -3
  17. package/dist/server.js +8 -8
  18. package/dist/serverPlayer.js +11 -5
  19. package/dist/signaling/codec.d.ts +7 -0
  20. package/dist/signaling/codec.js +65 -0
  21. package/dist/signaling/envelope.d.ts +20 -0
  22. package/dist/signaling/envelope.js +41 -0
  23. package/dist/signaling/jsonRpcSignalingTransport.d.ts +44 -0
  24. package/dist/signaling/jsonRpcSignalingTransport.js +174 -0
  25. package/dist/signaling/route.d.ts +15 -0
  26. package/dist/signaling/route.js +80 -0
  27. package/dist/signaling/signal.js +19 -10
  28. package/dist/signaling/signalingClient.d.ts +28 -0
  29. package/dist/signaling/signalingClient.js +161 -0
  30. package/dist/signaling/transport.d.ts +44 -0
  31. package/dist/signaling/transport.js +174 -0
  32. package/dist/signaling/turn.d.ts +4 -0
  33. package/dist/signaling/turn.js +83 -0
  34. package/dist/signaling/webRtcSignal.d.ts +20 -0
  35. package/dist/signaling/webRtcSignal.js +41 -0
  36. package/dist/signaling/webRtcSignalCodec.d.ts +7 -0
  37. package/dist/signaling/webRtcSignalCodec.js +65 -0
  38. package/dist/transforms/framer.d.ts +2 -2
  39. package/dist/transforms/framer.js +14 -4
  40. package/package.json +2 -3
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
- # BedrockPortalNethernet
2
- [![NPM version](https://img.shields.io/npm/v/bedrock-portal-nethernet.svg)](http://npmjs.com/package/bedrock-portal-nethernet)
3
- [![Discord](https://img.shields.io/badge/chat-on%20discord-brightgreen.svg)](https://discord.gg/KTyd9HWuBD)
4
-
5
- A simple server implementation for the Bedrock Edition Nethernet protocol used by [BedrockPortal](https://github.com/LucienHH/bedrock-portal). Join our [Discord](https://discord.com/invite/KTyd9HWuBD) for support.
6
-
7
- Based on bedrock-protocol but stripped down to only the necessary components for the Nethernet protocol.
8
-
9
- ## Installation
10
- ```shell
11
- npm install bedrock-portal-nethernet
1
+ # BedrockPortalNethernet
2
+ [![NPM version](https://img.shields.io/npm/v/bedrock-portal-nethernet.svg)](http://npmjs.com/package/bedrock-portal-nethernet)
3
+ [![Discord](https://img.shields.io/badge/chat-on%20discord-brightgreen.svg)](https://discord.gg/KTyd9HWuBD)
4
+
5
+ A simple server implementation for the Bedrock Edition Nethernet protocol used by [BedrockPortal](https://github.com/LucienHH/bedrock-portal). Join our [Discord](https://discord.com/invite/KTyd9HWuBD) for support.
6
+
7
+ Based on bedrock-protocol but stripped down to only the necessary components for the Nethernet protocol.
8
+
9
+ ## Installation
10
+ ```shell
11
+ npm install bedrock-portal-nethernet
12
12
  ```
@@ -1,5 +1,7 @@
1
- import crypto from 'crypto';
2
- export declare function getAuthorizationKey(protocolVersion: number, keyId: string): Promise<{
1
+ import { createRemoteJWKSet } from 'jose';
2
+ type AuthorizationVerifier = {
3
3
  issuer: string;
4
- key: crypto.KeyObject;
5
- }>;
4
+ keys: ReturnType<typeof createRemoteJWKSet>;
5
+ };
6
+ export declare function getAuthorizationVerifier(protocolVersion: number): Promise<AuthorizationVerifier>;
7
+ export {};
@@ -1,19 +1,14 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getAuthorizationKey = getAuthorizationKey;
7
- const crypto_1 = __importDefault(require("crypto"));
3
+ exports.getAuthorizationVerifier = getAuthorizationVerifier;
4
+ const jose_1 = require("jose");
8
5
  const debug = require('debug')('bedrock-portal-nethernet');
9
- const AUTHORISATION_KEY_REFRESH_INTERVAL_MS = 30 * 60 * 1000;
10
6
  const AUTHORISATION_FETCH_TIMEOUT_MS = 10_000;
11
7
  const AUTHORISATION_SERVICE_URI_FALLBACK = 'https://authorization.franchise.minecraft-services.net';
12
8
  const AUTHORISATION_SERVICE_OPENID_CONFIGURATION_PATH = '/.well-known/openid-configuration';
13
9
  const AUTHORISATION_SERVICE_KEYS_PATH = '/.well-known/keys';
14
10
  const MINECRAFT_SERVICES_DISCOVERY_URL = 'https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/';
15
- let cachedAuthorizationKeyring = null;
16
- let inflightAuthorizationKeyring = null;
11
+ const authorizationVerifiers = new Map();
17
12
  async function fetchJson(url) {
18
13
  let response;
19
14
  try {
@@ -53,54 +48,22 @@ async function resolveAuthorizationUrls(protocolVersion) {
53
48
  }
54
49
  return { issuer, jwksUrl };
55
50
  }
56
- async function fetchAuthorizationKeyring(protocolVersion) {
51
+ async function createAuthorizationVerifier(protocolVersion) {
57
52
  const { issuer, jwksUrl } = await resolveAuthorizationUrls(protocolVersion);
58
- const payload = await fetchJson(jwksUrl);
59
- const keys = new Map();
60
- for (const jwk of payload.keys || []) {
61
- if (!jwk.kid)
62
- continue;
63
- keys.set(jwk.kid, crypto_1.default.createPublicKey({ key: jwk, format: 'jwk' }));
64
- }
65
- if (keys.size === 0) {
66
- throw new Error('No valid authentication keys returned by Microsoft');
67
- }
68
53
  return {
69
- fetchedAt: Date.now(),
70
54
  issuer,
71
- keys,
72
- protocolVersion,
55
+ keys: (0, jose_1.createRemoteJWKSet)(jwksUrl, {
56
+ cacheMaxAge: 30 * 60 * 1000,
57
+ timeoutDuration: AUTHORISATION_FETCH_TIMEOUT_MS,
58
+ }),
73
59
  };
74
60
  }
75
- function isAuthorizationKeyringFresh(protocolVersion) {
76
- return cachedAuthorizationKeyring !== null &&
77
- cachedAuthorizationKeyring.protocolVersion === protocolVersion &&
78
- Date.now() - cachedAuthorizationKeyring.fetchedAt < AUTHORISATION_KEY_REFRESH_INTERVAL_MS;
79
- }
80
- async function refreshAuthorizationKeyring(protocolVersion) {
81
- inflightAuthorizationKeyring ??= fetchAuthorizationKeyring(protocolVersion);
82
- try {
83
- const keyring = await inflightAuthorizationKeyring;
84
- cachedAuthorizationKeyring = keyring;
85
- return keyring;
86
- }
87
- finally {
88
- inflightAuthorizationKeyring = null;
89
- }
90
- }
91
- async function getAuthorizationKey(protocolVersion, keyId) {
92
- let keyring = isAuthorizationKeyringFresh(protocolVersion)
93
- ? cachedAuthorizationKeyring
94
- : await refreshAuthorizationKeyring(protocolVersion);
95
- if (!keyring || !keyring.keys.has(keyId)) {
96
- keyring = await refreshAuthorizationKeyring(protocolVersion);
97
- }
98
- const key = keyring.keys.get(keyId);
99
- if (!key) {
100
- throw new Error(`Unrecognized authentication key ID: ${keyId}`);
61
+ function getAuthorizationVerifier(protocolVersion) {
62
+ let verifier = authorizationVerifiers.get(protocolVersion);
63
+ if (!verifier) {
64
+ verifier = createAuthorizationVerifier(protocolVersion);
65
+ authorizationVerifiers.set(protocolVersion, verifier);
66
+ void verifier.catch(() => authorizationVerifiers.delete(protocolVersion));
101
67
  }
102
- return {
103
- issuer: keyring.issuer,
104
- key,
105
- };
68
+ return verifier;
106
69
  }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.KeyExchange = KeyExchange;
4
- const jsonwebtoken_1 = require("jsonwebtoken");
4
+ const jose_1 = require("jose");
5
5
  const crypto_1 = require("crypto");
6
6
  const debug = require('debug')('bedrock-portal-nethernet');
7
7
  const SALT = '🧂';
@@ -14,7 +14,7 @@ function KeyExchange(client) {
14
14
  client.publicKeyDER = client.ecdhKeyPair.publicKey.export(der);
15
15
  client.privateKeyPEM = client.ecdhKeyPair.privateKey.export(pem);
16
16
  client.clientX509 = client.publicKeyDER.toString('base64');
17
- function startClientboundEncryption(publicKey) {
17
+ async function startClientboundEncryption(publicKey) {
18
18
  debug('[encrypt] Client pub key base64: ', publicKey);
19
19
  // const pubKeyDer = crypto.createPublicKey({ key: Buffer.from(publicKey.key, 'base64'), ...der })
20
20
  // Shared secret from the client's public key + our private key
@@ -28,10 +28,13 @@ function KeyExchange(client) {
28
28
  // const secretHash = crypto.createHash('sha256')
29
29
  // secretHash.update(SALT)
30
30
  // secretHash.update(client.sharedSecret)
31
- const token = (0, jsonwebtoken_1.sign)({
31
+ const token = await new jose_1.SignJWT({
32
32
  salt: toBase64(SALT),
33
33
  signedToken: client.clientX509,
34
- }, client.ecdhKeyPair.privateKey, { algorithm: 'ES384', header: { x5u: client.clientX509, alg: 'ES384' } });
34
+ })
35
+ .setProtectedHeader({ x5u: client.clientX509, alg: 'ES384' })
36
+ .setIssuedAt()
37
+ .sign(client.ecdhKeyPair.privateKey);
35
38
  client.write('server_to_client_handshake', { token });
36
39
  // client.secretKeyBytes = secretHash.digest()
37
40
  // // The encryption scheme is AES/CFB8/NoPadding with the
@@ -40,7 +43,12 @@ function KeyExchange(client) {
40
43
  // const initial = client.secretKeyBytes.slice(0, 16)
41
44
  // client.startEncryption(initial)
42
45
  }
43
- client.on('server.client_handshake', startClientboundEncryption);
46
+ client.on('server.client_handshake', publicKey => {
47
+ void startClientboundEncryption(publicKey).catch(error => {
48
+ debug('Failed to create server handshake token', error);
49
+ client.server.nethernet?.closeConnection(client.connection, 'server handshake failed');
50
+ });
51
+ });
44
52
  }
45
53
  function toBase64(string) {
46
54
  return Buffer.from(string).toString('base64');
@@ -4,27 +4,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const crypto_1 = __importDefault(require("crypto"));
7
- const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
7
+ const jose_1 = require("jose");
8
+ const identity_1 = require("../nethernet/identity");
8
9
  const constants_1 = require("./constants");
9
10
  const authorizationKeys_1 = require("./authorizationKeys");
10
11
  const debug = require('debug')('bedrock-portal-nethernet');
11
12
  const AUTHORISATION_AUDIENCE = 'api://auth-minecraft-services/multiplayer';
12
13
  const AUTHORISATION_CLOCK_TOLERANCE_SECONDS = 60;
13
- function getJwtObject(decoded, errorMessage) {
14
- if (!decoded || typeof decoded === 'string') {
15
- throw new Error(errorMessage);
16
- }
17
- return decoded;
18
- }
19
14
  function normalizeToken(token) {
20
15
  return token.replace(/^MCToken\s+/i, '').trim();
21
16
  }
22
17
  function getJwtHeader(token) {
23
- const decoded = jsonwebtoken_1.default.decode(normalizeToken(token), { complete: true });
24
- if (!decoded?.header || typeof decoded.header !== 'object') {
25
- throw new Error('Missing JWT header');
26
- }
27
- return decoded.header;
18
+ return (0, jose_1.decodeProtectedHeader)(normalizeToken(token));
28
19
  }
29
20
  function getTokenUserData(decoded) {
30
21
  return {
@@ -43,17 +34,13 @@ exports.default = (client) => {
43
34
  const getDER = (b64) => crypto_1.default.createPublicKey({ key: Buffer.from(b64, 'base64'), format: 'der', type: 'spki' });
44
35
  async function verifyTokenAuth(token) {
45
36
  const normalizedToken = normalizeToken(token);
46
- const header = getJwtHeader(normalizedToken);
47
- if (!header.kid) {
48
- throw new Error('Authorization token missing key id');
49
- }
50
- const { issuer, key } = await (0, authorizationKeys_1.getAuthorizationKey)(client.options.protocolVersion, header.kid);
51
- const decoded = getJwtObject(jsonwebtoken_1.default.verify(normalizedToken, key, {
37
+ const { issuer, keys } = await (0, authorizationKeys_1.getAuthorizationVerifier)(client.options.protocolVersion);
38
+ const { payload: decoded } = await (0, jose_1.jwtVerify)(normalizedToken, keys, {
52
39
  algorithms: ['RS256'],
53
40
  clockTolerance: AUTHORISATION_CLOCK_TOLERANCE_SECONDS,
54
41
  issuer,
55
42
  audience: AUTHORISATION_AUDIENCE,
56
- }), 'Invalid login token');
43
+ });
57
44
  const publicKey = decoded.cpk || decoded.clientPublicKey;
58
45
  if (!publicKey) {
59
46
  throw new Error('Authorization token missing client public key');
@@ -63,7 +50,7 @@ exports.default = (client) => {
63
50
  data: getTokenUserData(decoded),
64
51
  };
65
52
  }
66
- function verifyChainAuth(chain) {
53
+ async function verifyChainAuth(chain) {
67
54
  let data = {};
68
55
  // There are three JWT tokens sent to us, one signed by the client
69
56
  // one signed by Mojang with the Mojang token we have and another one
@@ -74,14 +61,14 @@ exports.default = (client) => {
74
61
  let pubKey = getDER(getX5U(chain[0])); // the first one is client signed, allow it
75
62
  let finalKey = '';
76
63
  for (const token of chain) {
77
- const decoded = getJwtObject(jsonwebtoken_1.default.verify(token, pubKey, { algorithms: ['ES384'] }), 'Invalid login token');
64
+ const { payload: decoded } = await (0, jose_1.jwtVerify)(token, pubKey, { algorithms: ['ES384'] });
78
65
  // Check if signed by Mojang key
79
66
  const x5u = getX5U(token);
80
67
  if (x5u === constants_1.PUBLIC_KEY && !data.extraData?.XUID) {
81
68
  didVerify = true;
82
69
  debug('Verified client with mojang key', x5u);
83
70
  }
84
- pubKey = decoded.identityPublicKey ? getDER(decoded.identityPublicKey) : x5u;
71
+ pubKey = getDER(decoded.identityPublicKey || x5u);
85
72
  finalKey = decoded.identityPublicKey || finalKey; // non pem
86
73
  data = { ...data, ...decoded };
87
74
  }
@@ -90,16 +77,20 @@ exports.default = (client) => {
90
77
  }
91
78
  return { key: finalKey, data };
92
79
  }
93
- function verifySkin(publicKey, token) {
80
+ async function verifySkin(publicKey, token) {
94
81
  if (getX5U(token) !== publicKey) {
95
82
  throw new Error('Invalid JWT signature');
96
83
  }
97
84
  const pubKey = getDER(publicKey);
98
- return getJwtObject(jsonwebtoken_1.default.verify(token, pubKey, { algorithms: ['ES384'] }), 'Invalid JWT payload');
85
+ const { payload } = await (0, jose_1.jwtVerify)(token, pubKey, { algorithms: ['ES384'] });
86
+ return payload;
99
87
  }
100
88
  client.decodeLoginJWT = async (authTokens, skinTokens, authToken = '') => {
101
- const { key, data } = authToken ? await verifyTokenAuth(authToken) : verifyChainAuth(authTokens);
102
- const skinData = verifySkin(key, skinTokens);
89
+ const { key, data } = authToken ? await verifyTokenAuth(authToken) : await verifyChainAuth(authTokens);
90
+ if (!(0, identity_1.publicKeysEqual)(key, client.connection.peerPublicKey)) {
91
+ throw new Error('Login public key does not match WebRTC peer identity');
92
+ }
93
+ const skinData = await verifySkin(key, skinTokens);
103
94
  return { key, userData: data, skinData };
104
95
  };
105
96
  client.encodeLoginJWT = (localChain, mojangChain) => {
@@ -113,5 +104,5 @@ exports.default = (client) => {
113
104
  };
114
105
  function getX5U(token) {
115
106
  const hjson = getJwtHeader(token);
116
- return hjson.x5u || '';
107
+ return typeof hjson.x5u === 'string' ? hjson.x5u : '';
117
108
  }
@@ -4,13 +4,21 @@ export declare const maxMessageSize = 10000;
4
4
  export declare class Connection {
5
5
  nethernet: Server;
6
6
  connectionId: bigint;
7
+ networkId: bigint;
8
+ peerPublicKey: Buffer;
7
9
  rtcConnection: PeerConnection;
8
10
  reliable: DataChannel | null;
9
11
  unreliable: DataChannel | null;
10
12
  promisedSegments: number;
11
- buf: Buffer | null;
12
- constructor(nethernet: Server, connectionId: bigint, rtcConnection: PeerConnection);
13
+ chunks: Buffer[];
14
+ iceConnected: boolean;
15
+ opened: boolean;
16
+ closed: boolean;
17
+ readyTimeout: NodeJS.Timeout;
18
+ constructor(nethernet: Server, networkId: bigint, connectionId: bigint, peerPublicKey: Buffer, rtcConnection: PeerConnection);
13
19
  setChannels(reliable: DataChannel | null, unreliable?: DataChannel): void;
20
+ setIceConnected(connected: boolean): void;
21
+ private openIfReady;
14
22
  handleMessage(data: string | Buffer | ArrayBuffer): void;
15
23
  onPacket(packet: Buffer): void;
16
24
  write(data: string | Buffer): number;
@@ -3,32 +3,85 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Connection = exports.maxMessageSize = void 0;
4
4
  const debugFn = require('debug')('bedrock-portal-nethernet');
5
5
  exports.maxMessageSize = 10_000;
6
+ const maxSegments = 256;
7
+ const connectionReadyTimeout = 15_000;
6
8
  class Connection {
7
9
  nethernet;
8
10
  connectionId;
11
+ networkId;
12
+ peerPublicKey;
9
13
  rtcConnection;
10
14
  reliable;
11
15
  unreliable;
12
16
  promisedSegments;
13
- buf;
14
- constructor(nethernet, connectionId, rtcConnection) {
17
+ chunks;
18
+ iceConnected;
19
+ opened;
20
+ closed;
21
+ readyTimeout;
22
+ constructor(nethernet, networkId, connectionId, peerPublicKey, rtcConnection) {
15
23
  this.nethernet = nethernet;
16
24
  this.connectionId = connectionId;
25
+ this.networkId = networkId;
26
+ this.peerPublicKey = peerPublicKey;
17
27
  this.rtcConnection = rtcConnection;
18
28
  this.reliable = null;
19
29
  this.unreliable = null;
20
30
  this.promisedSegments = 0;
21
- this.buf = Buffer.alloc(0);
31
+ this.chunks = [];
32
+ this.iceConnected = false;
33
+ this.opened = false;
34
+ this.closed = false;
35
+ this.readyTimeout = setTimeout(() => {
36
+ debugFn('Connection timed out waiting for reliable data channel', this.connectionId);
37
+ this.nethernet.closeConnection(this, 'reliable data channel timed out');
38
+ }, connectionReadyTimeout);
39
+ this.readyTimeout.unref();
22
40
  }
23
41
  setChannels(reliable, unreliable) {
24
42
  if (reliable) {
25
43
  this.reliable = reliable;
26
- this.reliable.onMessage((msg) => this.handleMessage(msg));
44
+ this.reliable.onMessage((msg) => {
45
+ try {
46
+ this.handleMessage(msg);
47
+ }
48
+ catch (error) {
49
+ debugFn('Rejected invalid data channel message', this.connectionId, error);
50
+ this.nethernet.closeConnection(this, 'invalid data channel message');
51
+ }
52
+ });
53
+ this.reliable.onOpen(() => {
54
+ debugFn('Reliable data channel opened', this.connectionId);
55
+ this.openIfReady();
56
+ });
57
+ this.reliable.onClosed(() => {
58
+ debugFn('Reliable data channel closed', this.connectionId);
59
+ this.nethernet.closeConnection(this, 'reliable data channel closed');
60
+ });
61
+ this.reliable.onError(error => {
62
+ debugFn('Reliable data channel error', this.connectionId, error);
63
+ this.nethernet.closeConnection(this, `reliable data channel error: ${error}`);
64
+ });
65
+ this.openIfReady();
27
66
  }
28
67
  if (unreliable) {
29
68
  this.unreliable = unreliable;
69
+ this.unreliable.onOpen(() => debugFn('Unreliable data channel opened', this.connectionId));
70
+ this.unreliable.onClosed(() => debugFn('Unreliable data channel closed', this.connectionId));
71
+ this.unreliable.onError(error => debugFn('Unreliable data channel error', this.connectionId, error));
30
72
  }
31
73
  }
74
+ setIceConnected(connected) {
75
+ this.iceConnected = connected;
76
+ this.openIfReady();
77
+ }
78
+ openIfReady() {
79
+ if (this.opened || this.closed || !this.iceConnected || !this.reliable?.isOpen())
80
+ return;
81
+ this.opened = true;
82
+ clearTimeout(this.readyTimeout);
83
+ this.nethernet.onOpenConnection(this);
84
+ }
32
85
  handleMessage(data) {
33
86
  if (typeof data === 'string') {
34
87
  data = Buffer.from(data);
@@ -46,15 +99,15 @@ class Connection {
46
99
  throw new Error(`Invalid promised segments: expected ${this.promisedSegments - 1}, got ${segments}`);
47
100
  }
48
101
  this.promisedSegments = segments;
49
- this.buf = this.buf ? Buffer.concat([this.buf, data]) : data;
102
+ this.chunks.push(data);
50
103
  if (this.promisedSegments > 0) {
51
104
  return;
52
105
  }
53
- this.onPacket(this.buf);
54
- this.buf = null;
106
+ this.onPacket(Buffer.concat(this.chunks));
107
+ this.chunks = [];
55
108
  }
56
109
  onPacket(packet) {
57
- this.nethernet.onEncapsulated(packet, this.connectionId);
110
+ this.nethernet.onEncapsulated(packet, this);
58
111
  }
59
112
  write(data) {
60
113
  if (!this.reliable) {
@@ -65,6 +118,9 @@ class Connection {
65
118
  data = Buffer.from(data);
66
119
  }
67
120
  let segments = Math.ceil(data.length / exports.maxMessageSize);
121
+ if (segments > maxSegments) {
122
+ throw new Error(`Data requires ${segments} segments, maximum is ${maxSegments}`);
123
+ }
68
124
  for (let i = 0; i < data.length; i += exports.maxMessageSize) {
69
125
  segments--;
70
126
  let end = i + exports.maxMessageSize;
@@ -73,7 +129,10 @@ class Connection {
73
129
  const frag = data.subarray(i, end);
74
130
  const message = Buffer.concat([Buffer.from([segments]), frag]);
75
131
  debugFn('Sending fragment', segments, 'header', message[0]);
76
- this.reliable.sendMessageBinary(message);
132
+ if (!this.reliable.sendMessageBinary(message)) {
133
+ this.nethernet.closeConnection(this, 'failed to send reliable data channel message');
134
+ throw new Error(`Failed to send segment ${segments}`);
135
+ }
77
136
  n += frag.length;
78
137
  }
79
138
  if (segments !== 0) {
@@ -82,6 +141,10 @@ class Connection {
82
141
  return n;
83
142
  }
84
143
  close() {
144
+ if (this.closed)
145
+ return;
146
+ this.closed = true;
147
+ clearTimeout(this.readyTimeout);
85
148
  if (this.reliable) {
86
149
  this.reliable.close();
87
150
  }
@@ -0,0 +1,9 @@
1
+ import crypto from 'crypto';
2
+ export type ServerIdentity = {
3
+ privateKey: crypto.KeyObject;
4
+ publicKey: string;
5
+ };
6
+ export declare function createServerIdentity(): ServerIdentity;
7
+ export declare function signServerIdentity(sdp: string, identity: ServerIdentity): Promise<string>;
8
+ export declare function verifyIdentity(sdp: string): Promise<Buffer<ArrayBufferLike>>;
9
+ export declare function publicKeysEqual(encodedKey: string, expectedKey: Buffer): boolean;
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createServerIdentity = createServerIdentity;
7
+ exports.signServerIdentity = signServerIdentity;
8
+ exports.verifyIdentity = verifyIdentity;
9
+ exports.publicKeysEqual = publicKeysEqual;
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ const jose_1 = require("jose");
12
+ function createServerIdentity() {
13
+ const { privateKey, publicKey } = crypto_1.default.generateKeyPairSync('ec', { namedCurve: 'secp384r1' });
14
+ return {
15
+ privateKey,
16
+ publicKey: publicKey.export({ format: 'der', type: 'spki' }).toString('base64'),
17
+ };
18
+ }
19
+ async function signServerIdentity(sdp, identity) {
20
+ const now = Math.floor(Date.now() / 1000);
21
+ const token = await new jose_1.SignJWT({ cpk: identity.publicKey })
22
+ .setProtectedHeader({ alg: 'ES384', x5u: identity.publicKey })
23
+ .setIssuedAt(now)
24
+ .setExpirationTime(now + 60)
25
+ .sign(identity.privateKey);
26
+ const signedFingerprints = await new jose_1.CompactSign(fingerprintPayload(sdp))
27
+ .setProtectedHeader({ alg: 'ES384' })
28
+ .sign(identity.privateKey);
29
+ const [protectedHeader, , signature] = signedFingerprints.split('.');
30
+ const fingerprints = `${protectedHeader}..${signature}`;
31
+ const value = Buffer.from(JSON.stringify({
32
+ assertion: JSON.stringify({ fingerprints, token }),
33
+ idp: { domain: 'self', protocol: 'default' },
34
+ })).toString('base64');
35
+ return addSessionAttribute(sdp, `a=identity:${value}`);
36
+ }
37
+ async function verifyIdentity(sdp) {
38
+ const identityValue = getSdpAttribute(sdp, 'identity');
39
+ if (!identityValue)
40
+ throw new Error('Missing SDP identity assertion');
41
+ const identity = parseJson(Buffer.from(identityValue, 'base64').toString('utf8'), 'identity assertion');
42
+ if (!identity.assertion || !identity.idp?.domain || identity.idp.protocol !== 'default') {
43
+ throw new Error('Malformed SDP identity assertion');
44
+ }
45
+ const assertion = parseJson(identity.assertion, 'identity assertion payload');
46
+ if (!assertion.fingerprints || !assertion.token) {
47
+ throw new Error('Incomplete SDP identity assertion');
48
+ }
49
+ const token = (0, jose_1.decodeJwt)(assertion.token);
50
+ if (!token.cpk)
51
+ throw new Error('Identity token missing client public key');
52
+ const now = Date.now() / 1000;
53
+ if (token.exp !== undefined && token.exp <= now)
54
+ throw new Error('Identity token has expired');
55
+ if (token.nbf !== undefined && token.nbf > now)
56
+ throw new Error('Identity token is not valid yet');
57
+ const publicKey = parsePublicKey(token.cpk);
58
+ await verifyDetachedSignature(assertion.fingerprints, fingerprintPayload(sdp), publicKey);
59
+ return publicKey.export({ format: 'der', type: 'spki' });
60
+ }
61
+ function publicKeysEqual(encodedKey, expectedKey) {
62
+ return parsePublicKey(encodedKey).export({ format: 'der', type: 'spki' }).equals(expectedKey);
63
+ }
64
+ async function verifyDetachedSignature(compact, payload, publicKey) {
65
+ const parts = compact.split('.');
66
+ if (parts.length !== 3 || parts[1] !== '')
67
+ throw new Error('Malformed detached fingerprint signature');
68
+ await (0, jose_1.flattenedVerify)({
69
+ protected: parts[0],
70
+ payload: payload.toString('base64url'),
71
+ signature: parts[2],
72
+ }, publicKey, { algorithms: ['ES384'] });
73
+ }
74
+ function fingerprintPayload(sdp) {
75
+ const fingerprint = getSdpAttribute(sdp, 'fingerprint');
76
+ if (!fingerprint)
77
+ throw new Error('Missing SDP fingerprint');
78
+ const separator = fingerprint.indexOf(' ');
79
+ if (separator === -1)
80
+ throw new Error('Malformed SDP fingerprint');
81
+ return Buffer.from(JSON.stringify({
82
+ fingerprint: [{
83
+ algorithm: fingerprint.slice(0, separator),
84
+ digest: fingerprint.slice(separator + 1),
85
+ }],
86
+ }));
87
+ }
88
+ function addSessionAttribute(sdp, attribute) {
89
+ const lineEnding = sdp.includes('\r\n') ? '\r\n' : '\n';
90
+ const lines = sdp.split(lineEnding);
91
+ const mediaIndex = lines.findIndex(line => line.startsWith('m='));
92
+ if (mediaIndex === -1)
93
+ throw new Error('SDP has no media description');
94
+ lines.splice(mediaIndex, 0, attribute);
95
+ return lines.join(lineEnding);
96
+ }
97
+ function parsePublicKey(encodedKey) {
98
+ const publicKey = crypto_1.default.createPublicKey({ key: Buffer.from(encodedKey, 'base64'), format: 'der', type: 'spki' });
99
+ if (publicKey.asymmetricKeyType !== 'ec' || publicKey.asymmetricKeyDetails?.namedCurve !== 'secp384r1') {
100
+ throw new Error('Client public key must use the P-384 curve');
101
+ }
102
+ return publicKey;
103
+ }
104
+ function getSdpAttribute(sdp, name) {
105
+ const prefix = `a=${name}:`;
106
+ const line = sdp.split(/\r?\n/).find(line => line.startsWith(prefix));
107
+ return line?.slice(prefix.length).trim();
108
+ }
109
+ function parseJson(value, name) {
110
+ try {
111
+ return JSON.parse(value);
112
+ }
113
+ catch {
114
+ throw new Error(`Malformed ${name}`);
115
+ }
116
+ }
@@ -1,17 +1,21 @@
1
1
  import { Connection } from './connection';
2
+ import { ServerIdentity } from './identity';
2
3
  import { Signal } from '../signaling/signal';
3
4
  import { SignalStructure } from '../signaling/struct';
4
5
  export declare class Server {
5
6
  networkId: bigint;
6
7
  connectionId: bigint;
7
8
  signaling: Signal;
8
- connections: Map<bigint, Connection>;
9
+ identity: ServerIdentity;
10
+ connections: Map<string, Connection>;
9
11
  onOpenConnection: (conn: Connection) => void;
10
- onCloseConnection: (id: bigint, reason: string) => void;
11
- onEncapsulated: (packet: Buffer, id: bigint) => void;
12
+ onCloseConnection: (conn: Connection, reason: string) => void;
13
+ onEncapsulated: (packet: Buffer, conn: Connection) => void;
12
14
  constructor(signaling: Signal, networkId?: bigint, connectionId?: bigint);
13
15
  handleCandidate(signal: SignalStructure): Promise<void>;
14
16
  handleOffer(signal: SignalStructure): Promise<void>;
15
17
  listen(): Promise<void>;
18
+ closeConnection(connection: Connection, reason: string): void;
16
19
  close(): void;
20
+ private connectionKey;
17
21
  }