bedrock-portal-nethernet 2.4.0 → 2.6.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/dist/handshake/authorizationKeys.d.ts +6 -4
- package/dist/handshake/authorizationKeys.js +15 -52
- package/dist/handshake/keyExchange.js +13 -5
- package/dist/handshake/loginVerify.js +18 -27
- package/dist/nethernet/connection.d.ts +10 -2
- package/dist/nethernet/connection.js +72 -9
- package/dist/nethernet/identity.d.ts +9 -0
- package/dist/nethernet/identity.js +116 -0
- package/dist/nethernet/server.d.ts +7 -3
- package/dist/nethernet/server.js +71 -14
- package/dist/options.d.ts +1 -1
- package/dist/options.js +2 -2
- package/dist/protocol.d.ts +1332 -776
- package/dist/protocol.js +2719 -2280
- package/dist/server.d.ts +3 -3
- package/dist/server.js +8 -8
- package/dist/serverPlayer.js +11 -5
- package/dist/signaling/signal.js +19 -10
- package/dist/transforms/framer.d.ts +2 -2
- package/dist/transforms/framer.js +14 -4
- package/package.json +2 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { createRemoteJWKSet } from 'jose';
|
|
2
|
+
type AuthorizationVerifier = {
|
|
3
3
|
issuer: string;
|
|
4
|
-
|
|
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.
|
|
7
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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 =
|
|
31
|
+
const token = await new jose_1.SignJWT({
|
|
32
32
|
salt: toBase64(SALT),
|
|
33
33
|
signedToken: client.clientX509,
|
|
34
|
-
}
|
|
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',
|
|
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
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
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
|
-
})
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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.
|
|
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) =>
|
|
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.
|
|
102
|
+
this.chunks.push(data);
|
|
50
103
|
if (this.promisedSegments > 0) {
|
|
51
104
|
return;
|
|
52
105
|
}
|
|
53
|
-
this.onPacket(this.
|
|
54
|
-
this.
|
|
106
|
+
this.onPacket(Buffer.concat(this.chunks));
|
|
107
|
+
this.chunks = [];
|
|
55
108
|
}
|
|
56
109
|
onPacket(packet) {
|
|
57
|
-
this.nethernet.onEncapsulated(packet, this
|
|
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<NonSharedBuffer>;
|
|
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
|
-
|
|
9
|
+
identity: ServerIdentity;
|
|
10
|
+
connections: Map<string, Connection>;
|
|
9
11
|
onOpenConnection: (conn: Connection) => void;
|
|
10
|
-
onCloseConnection: (
|
|
11
|
-
onEncapsulated: (packet: Buffer,
|
|
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
|
}
|