iosignal 6.1.1 → 7.0.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/README.ko.md +323 -0
- package/README.md +233 -57
- package/dist/browser/esm/io.js +1 -1
- package/dist/browser/iife/io.js +1 -1
- package/dist/node/iosignal.cjs +122 -19
- package/dist/node/iosignal.js +122 -19
- package/package.json +1 -1
package/dist/node/iosignal.js
CHANGED
|
@@ -9,12 +9,13 @@ import require$$7 from 'url';
|
|
|
9
9
|
import require$$0 from 'zlib';
|
|
10
10
|
import require$$0$1 from 'buffer';
|
|
11
11
|
import require$$2 from 'util';
|
|
12
|
+
import { performance } from 'node:perf_hooks';
|
|
12
13
|
import { networkInterfaces } from 'os';
|
|
13
14
|
import fs, { readFileSync } from 'fs';
|
|
14
15
|
import { memoryUsage } from 'process';
|
|
15
16
|
import path from 'path';
|
|
16
17
|
|
|
17
|
-
var version$1 = "
|
|
18
|
+
var version$1 = "7.0.0";
|
|
18
19
|
var pkg = {
|
|
19
20
|
version: version$1};
|
|
20
21
|
|
|
@@ -7369,6 +7370,13 @@ let serverOption = {
|
|
|
7369
7370
|
limitCounter: 1000
|
|
7370
7371
|
},
|
|
7371
7372
|
|
|
7373
|
+
// Applied to each new Server instance; packet layouts remain unchanged.
|
|
7374
|
+
security: {
|
|
7375
|
+
maxClockSkewMs: 60000,
|
|
7376
|
+
authChallengeMaxAgeMs: 60000,
|
|
7377
|
+
maxReplayEntries: 65536
|
|
7378
|
+
},
|
|
7379
|
+
|
|
7372
7380
|
auth: {
|
|
7373
7381
|
delay_auth_fail: 600
|
|
7374
7382
|
},
|
|
@@ -7392,6 +7400,13 @@ class RemoteCore {
|
|
|
7392
7400
|
socket.openTime = Date.now();
|
|
7393
7401
|
|
|
7394
7402
|
this.boho = new it();
|
|
7403
|
+
this.security = manager.server.security;
|
|
7404
|
+
this._challengeIssuedAt = null;
|
|
7405
|
+
this._challengeUsed = false;
|
|
7406
|
+
this._replayBuckets = new Map();
|
|
7407
|
+
this._replayEntries = 0;
|
|
7408
|
+
this._replayExpiredBefore = 0;
|
|
7409
|
+
this.securityFailure = null;
|
|
7395
7410
|
this.encMode = ENC_MODE.AUTO;
|
|
7396
7411
|
this.channels = new Set(); // subscribed tags
|
|
7397
7412
|
this.retain_signal = new Map();
|
|
@@ -7480,15 +7495,92 @@ class RemoteCore {
|
|
|
7480
7495
|
return true;
|
|
7481
7496
|
}
|
|
7482
7497
|
|
|
7498
|
+
issueAuthChallenge() {
|
|
7499
|
+
const packet = this.boho.server_time_nonce();
|
|
7500
|
+
this._challengeIssuedAt = performance.now();
|
|
7501
|
+
return packet;
|
|
7502
|
+
}
|
|
7503
|
+
|
|
7504
|
+
securityMismatch(reason) {
|
|
7505
|
+
if (this._closing || this.state === STATE.CLOSED) return false;
|
|
7506
|
+
this.securityFailure = reason;
|
|
7507
|
+
this.boho.clearAuth();
|
|
7508
|
+
this.isAdmin = false;
|
|
7509
|
+
this._replayBuckets.clear();
|
|
7510
|
+
this._replayEntries = 0;
|
|
7511
|
+
this.close(true);
|
|
7512
|
+
this.manager.attackLogger?.log(`SECURITY #${this.ssid} reason:${reason}`);
|
|
7513
|
+
this.manager.server.emit('security:mismatch', this, reason);
|
|
7514
|
+
return false;
|
|
7515
|
+
}
|
|
7516
|
+
|
|
7517
|
+
checkAuthChallenge() {
|
|
7518
|
+
if (this._closing || this.state === STATE.CLOSED) return false;
|
|
7519
|
+
if (this._challengeUsed) return this.securityMismatch('AUTH_CHALLENGE_REUSED');
|
|
7520
|
+
if (this._challengeIssuedAt === null ||
|
|
7521
|
+
performance.now() - this._challengeIssuedAt > this.security.authChallengeMaxAgeMs) {
|
|
7522
|
+
return this.securityMismatch('AUTH_CHALLENGE_EXPIRED');
|
|
7523
|
+
}
|
|
7524
|
+
return true;
|
|
7525
|
+
}
|
|
7526
|
+
|
|
7527
|
+
// Called only after Boho has authenticated the packet's clock and payload.
|
|
7528
|
+
acceptEncryptedClock(message, receivedAtMs) {
|
|
7529
|
+
const seconds = message.readUInt32LE(5);
|
|
7530
|
+
const milliseconds = message.readUInt16LE(9);
|
|
7531
|
+
const counter = message.readUInt16LE(11);
|
|
7532
|
+
const packetTime = seconds * 1000 + milliseconds;
|
|
7533
|
+
const earliest = receivedAtMs - this.security.maxClockSkewMs;
|
|
7534
|
+
if (milliseconds >= 1000 || Math.abs(receivedAtMs - packetTime) > this.security.maxClockSkewMs ||
|
|
7535
|
+
packetTime < this._replayExpiredBefore) {
|
|
7536
|
+
return this.securityMismatch('TIME_MISMATCH');
|
|
7537
|
+
}
|
|
7538
|
+
// Never reopen an expired timestamp range after the server clock moves back.
|
|
7539
|
+
this._replayExpiredBefore = Math.max(this._replayExpiredBefore, earliest);
|
|
7540
|
+
for (const [second, values] of this._replayBuckets) {
|
|
7541
|
+
if ((second + 1) * 1000 <= this._replayExpiredBefore) {
|
|
7542
|
+
this._replayEntries -= values.size;
|
|
7543
|
+
this._replayBuckets.delete(second);
|
|
7544
|
+
}
|
|
7545
|
+
}
|
|
7546
|
+
let bucket = this._replayBuckets.get(seconds);
|
|
7547
|
+
const value = milliseconds * 65536 + counter;
|
|
7548
|
+
if (bucket?.has(value)) return this.securityMismatch('REPLAY_DETECTED');
|
|
7549
|
+
// Fail closed instead of evicting still-valid replay evidence under load.
|
|
7550
|
+
if (this._replayEntries >= this.security.maxReplayEntries) {
|
|
7551
|
+
return this.securityMismatch('REPLAY_CAPACITY_EXCEEDED');
|
|
7552
|
+
}
|
|
7553
|
+
if (!bucket) this._replayBuckets.set(seconds, bucket = new Set());
|
|
7554
|
+
bucket.add(value);
|
|
7555
|
+
this._replayEntries++;
|
|
7556
|
+
return true;
|
|
7557
|
+
}
|
|
7558
|
+
|
|
7559
|
+
decryptSessionPacket(message, receivedAtMs) {
|
|
7560
|
+
let decoded;
|
|
7561
|
+
try {
|
|
7562
|
+
decoded = this.boho.decrypt_488(message);
|
|
7563
|
+
} catch {
|
|
7564
|
+
this.securityMismatch('INVALID_ENCRYPTED_PACKET');
|
|
7565
|
+
return;
|
|
7566
|
+
}
|
|
7567
|
+
if (!decoded) {
|
|
7568
|
+
this.securityMismatch('INVALID_ENCRYPTED_PACKET');
|
|
7569
|
+
return;
|
|
7570
|
+
}
|
|
7571
|
+
if (this.acceptEncryptedClock(message, receivedAtMs)) return decoded;
|
|
7572
|
+
}
|
|
7573
|
+
|
|
7483
7574
|
onTimeDelayMessage(message, isBinary = true) {
|
|
7575
|
+
const receivedAtMs = Date.now();
|
|
7484
7576
|
setTimeout(() => {
|
|
7485
|
-
this.onSocketMessage(message, isBinary);
|
|
7577
|
+
this.onSocketMessage(message, isBinary, receivedAtMs);
|
|
7486
7578
|
}
|
|
7487
7579
|
, serverOption.debug.delay);
|
|
7488
7580
|
}
|
|
7489
7581
|
|
|
7490
7582
|
// CongSocket or WebSocket
|
|
7491
|
-
onSocketMessage(message, isBinary = true) {
|
|
7583
|
+
onSocketMessage(message, isBinary = true, receivedAtMs = Date.now()) {
|
|
7492
7584
|
|
|
7493
7585
|
if (this._closing || this.state === STATE.CLOSED) return;
|
|
7494
7586
|
|
|
@@ -7504,12 +7596,7 @@ class RemoteCore {
|
|
|
7504
7596
|
|
|
7505
7597
|
if (msgType === it.BohoMsg.ENC_488) {
|
|
7506
7598
|
|
|
7507
|
-
|
|
7508
|
-
decoded = this.boho.decrypt_488(message);
|
|
7509
|
-
} catch (err) {
|
|
7510
|
-
console.log('-- E488 DEC_FAIL', err);
|
|
7511
|
-
return
|
|
7512
|
-
}
|
|
7599
|
+
decoded = this.decryptSessionPacket(message, receivedAtMs);
|
|
7513
7600
|
|
|
7514
7601
|
if (decoded) {
|
|
7515
7602
|
msgType = decoded[0];
|
|
@@ -7519,12 +7606,7 @@ class RemoteCore {
|
|
|
7519
7606
|
}
|
|
7520
7607
|
|
|
7521
7608
|
} else if (msgType === it.BohoMsg.ENC_E2E) { // symetric E2EE Signal
|
|
7522
|
-
|
|
7523
|
-
decoded = this.boho.decrypt_488(message);
|
|
7524
|
-
} catch (err) {
|
|
7525
|
-
// console.log('-- E2E DEC_FAIL', err)
|
|
7526
|
-
return
|
|
7527
|
-
}
|
|
7609
|
+
decoded = this.decryptSessionPacket(message, receivedAtMs);
|
|
7528
7610
|
// console.log('e2e unpack:', decoded )
|
|
7529
7611
|
if (decoded) {
|
|
7530
7612
|
msgType = decoded[0];
|
|
@@ -7669,13 +7751,17 @@ class RemoteCore {
|
|
|
7669
7751
|
// client's auth requst
|
|
7670
7752
|
case it.BohoMsg.AUTH_REQ:
|
|
7671
7753
|
if (!this.manager.authManager) return
|
|
7672
|
-
//
|
|
7673
|
-
if (this.state === STATE.AUTH_REQ)
|
|
7754
|
+
// A second request while verification is pending must not restart auth.
|
|
7755
|
+
if (this.state === STATE.AUTH_REQ) {
|
|
7756
|
+
this.securityMismatch('AUTH_IN_PROGRESS');
|
|
7757
|
+
return;
|
|
7758
|
+
}
|
|
7674
7759
|
if (![STATE.SERVER_READY, STATE.AUTH_RES, STATE.AUTH_FAIL, STATE.CID_RES].includes(this.state)) {
|
|
7675
7760
|
console.log('protocol error. must called auth_req after server_ready');
|
|
7676
7761
|
this.close();
|
|
7677
7762
|
return;
|
|
7678
7763
|
}
|
|
7764
|
+
if (!this.checkAuthChallenge()) return;
|
|
7679
7765
|
this.setState(STATE.AUTH_REQ);
|
|
7680
7766
|
//async
|
|
7681
7767
|
this.manager.authManager.verify_auth_req(message, this)
|
|
@@ -8317,7 +8403,7 @@ class Manager {
|
|
|
8317
8403
|
socket.isAlive = true;
|
|
8318
8404
|
let remote = new Remote(socket, req, this);
|
|
8319
8405
|
this.remotes.add(remote);
|
|
8320
|
-
remote.send(
|
|
8406
|
+
remote.send(remote.issueAuthChallenge());
|
|
8321
8407
|
remote.send( Buffer.from([ IOMsg.SERVER_READY]) );
|
|
8322
8408
|
remote.setState(STATE.SERVER_READY);
|
|
8323
8409
|
this.lastSSID = remote.ssid;
|
|
@@ -8699,6 +8785,13 @@ class Server extends require$$0$3 {
|
|
|
8699
8785
|
|
|
8700
8786
|
constructor(options, authManager) {
|
|
8701
8787
|
super();
|
|
8788
|
+
const security = { ...serverOption.security, ...options.security };
|
|
8789
|
+
for (const name of ['maxClockSkewMs', 'authChallengeMaxAgeMs', 'maxReplayEntries']) {
|
|
8790
|
+
if (!Number.isSafeInteger(security[name]) || security[name] <= 0) {
|
|
8791
|
+
throw new RangeError(`security.${name} must be a positive safe integer`);
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
this.security = Object.freeze(security);
|
|
8702
8795
|
this.serviceNames = new Set();
|
|
8703
8796
|
this.wss = {};
|
|
8704
8797
|
this.port = null;
|
|
@@ -8960,6 +9053,12 @@ class BohoAuth {
|
|
|
8960
9053
|
|
|
8961
9054
|
async verify_auth_req(auth_req, peer) {
|
|
8962
9055
|
try {
|
|
9056
|
+
if (!peer.checkAuthChallenge()) return;
|
|
9057
|
+
if (!(auth_req instanceof Uint8Array) || auth_req.length !== it.MetaSize.AUTH_REQ ||
|
|
9058
|
+
auth_req[0] !== it.BohoMsg.AUTH_REQ) {
|
|
9059
|
+
peer.securityMismatch('INVALID_AUTH_PACKET');
|
|
9060
|
+
return;
|
|
9061
|
+
}
|
|
8963
9062
|
//1. unpack
|
|
8964
9063
|
let infoPack = $$1.unpack(auth_req, it.Meta.AUTH_REQ);
|
|
8965
9064
|
if (!infoPack) {
|
|
@@ -8976,7 +9075,7 @@ class BohoAuth {
|
|
|
8976
9075
|
|
|
8977
9076
|
//2. get key of id from DB
|
|
8978
9077
|
let authInfo = await this.keyProvider.getAuth(id);
|
|
8979
|
-
if (peer.
|
|
9078
|
+
if (!peer.checkAuthChallenge()) return;
|
|
8980
9079
|
if (!authInfo) {
|
|
8981
9080
|
this.send_auth_fail(peer, 'NO ID:' + id);
|
|
8982
9081
|
return
|
|
@@ -9006,6 +9105,10 @@ class BohoAuth {
|
|
|
9006
9105
|
return
|
|
9007
9106
|
}
|
|
9008
9107
|
|
|
9108
|
+
// A valid proof consumes this challenge even if account policy rejects it.
|
|
9109
|
+
// Reauthentication requires a new connection, never a replay-state reset.
|
|
9110
|
+
peer._challengeUsed = true;
|
|
9111
|
+
|
|
9009
9112
|
//4. get info
|
|
9010
9113
|
|
|
9011
9114
|
//5. check duplicate login.
|