kersiosocket 0.5.0-dev → 0.6.0-dev

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.
@@ -22,6 +22,9 @@ const DEFAULTS = {
22
22
  url: 'wss://web.whatsapp.com/ws/chat',
23
23
  origin: 'https://web.whatsapp.com',
24
24
  clientPayload: EMPTY, // empty buffer is transport smoke-test only; real login needs verified auth payload
25
+ // Fail closed by default: callers must supply a pinned, independently tested server-certificate verifier.
26
+ strictServerCertificateVerification: true,
27
+ verifyServerCertificate: null,
25
28
  // stableMs: koneksi baru dianggap 'sehat' (counter backoff direset) kalau bertahan selama ini
26
29
  reconnect: { enabled: true, minDelay: 1000, maxDelay: 30000, factor: 2, stableMs: 30000 }
27
30
  }
@@ -48,6 +51,7 @@ class Connection extends EventEmitter {
48
51
  this.counters = { bytesIn: 0, bytesOut: 0, framesIn: 0, framesOut: 0 }
49
52
  this.frameLatency = new LatencyTracker() // dekripsi + dispatch per frame
50
53
  this.handshakeMs = 0
54
+ this.serverCertificateVerified = false
51
55
  this._t = null
52
56
  this._decoder = null
53
57
  this._hs = null
@@ -110,7 +114,7 @@ class Connection extends EventEmitter {
110
114
  ...this.counters,
111
115
  handshakeMs: this.handshakeMs,
112
116
  frameLatencyMs: this.frameLatency.summary(),
113
- serverCertificateVerified: false,
117
+ serverCertificateVerified: this.serverCertificateVerified,
114
118
  rssMB: +(m.rss / 1048576).toFixed(1),
115
119
  heapUsedMB: +(m.heapUsed / 1048576).toFixed(1)
116
120
  }
@@ -129,7 +133,11 @@ class Connection extends EventEmitter {
129
133
 
130
134
  _onOpen() {
131
135
  this._decoder = new FrameDecoder()
132
- this._hs = new ClientHandshake({ staticKey: this.staticKey })
136
+ this._hs = new ClientHandshake({
137
+ staticKey: this.staticKey,
138
+ verifyServerCertificate: this.config.verifyServerCertificate,
139
+ requireServerCertificateVerification: this.config.strictServerCertificateVerification
140
+ })
133
141
  this.startedAt = Date.now()
134
142
  this._setState('handshaking')
135
143
  this._write(encodeFrame(this._hs.hello(), WA_HEADER))
@@ -160,6 +168,7 @@ class Connection extends EventEmitter {
160
168
  const hs = this._hs
161
169
  const finish = hs.processServerHello(frame, this.clientPayload)
162
170
  this._noise = hs.noise
171
+ this.serverCertificateVerified = hs.serverCertificateVerified === true
163
172
  this._hs = null
164
173
  // Publish the completed Noise state before writing ClientFinish. A fake or
165
174
  // custom transport may synchronously deliver the server reply from send().
@@ -167,7 +176,7 @@ class Connection extends EventEmitter {
167
176
  this.handshakeMs = Date.now() - this.startedAt
168
177
  this._setState('noise-ready', {
169
178
  serverStatic: hs.serverStatic,
170
- serverCertificateVerified: false
179
+ serverCertificateVerified: this.serverCertificateVerified
171
180
  })
172
181
  this._write(encodeFrame(finish))
173
182
  return
@@ -219,6 +228,10 @@ class Connection extends EventEmitter {
219
228
  _abortConnection(error, transport = this._t, generation = this._generation) {
220
229
  const err = error instanceof Error ? error : new Error(String(error))
221
230
  this._safeEmit('connection.error', err)
231
+ if (['ERR_NOISE_CERTIFICATE_VERIFIER_MISSING', 'ERR_NOISE_CERTIFICATE_REJECTED'].includes(err.code)) {
232
+ // Configuration/trust failures are not transient network failures. Stop auto-retry.
233
+ this.ended = true
234
+ }
222
235
  if (!transport || transport !== this._t || generation !== this._generation) return
223
236
  // Transition/cleanup synchronously. The transport's eventual close event is de-duplicated.
224
237
  this._onClose(1002, Buffer.from('protocol/transport failure'), transport, generation)
@@ -81,8 +81,11 @@ class ClientHandshake {
81
81
  /**
82
82
  * @param {{ staticKey: {public:Buffer, private:Buffer}, ephemeralKey?: object, header?: Buffer }} opts
83
83
  */
84
- constructor({ staticKey, ephemeralKey = generateKeyPair(), header = WA_HEADER }) {
84
+ constructor({ staticKey, ephemeralKey = generateKeyPair(), header = WA_HEADER, verifyServerCertificate, requireServerCertificateVerification = false }) {
85
85
  this.staticKey = staticKey
86
+ this.verifyServerCertificate = verifyServerCertificate
87
+ this.requireServerCertificateVerification = requireServerCertificateVerification
88
+ this.serverCertificateVerified = false
86
89
  this.ephemeralKey = ephemeralKey
87
90
  this.noise = new NoiseState(header)
88
91
  this.noise.authenticate(ephemeralKey.public)
@@ -97,7 +100,8 @@ class ClientHandshake {
97
100
 
98
101
  /**
99
102
  * Proses ServerHello, kembalikan ClientFinish (protobuf).
100
- * TODO v0.2: verifikasi rantai sertifikat server terhadap root key WhatsApp.
103
+ * Verifikasi sertifikat dipasok oleh verifier yang menggunakan root/intermediate key
104
+ * resmi yang dipin dan test vector independen. Tidak ada verifier bawaan yang diklaim.
101
105
  * @param {Buffer} data isi frame ServerHello
102
106
  * @param {Buffer} clientPayload ClientPayload (login/registrasi), sudah di-encode protobuf
103
107
  */
@@ -112,6 +116,22 @@ class ClientHandshake {
112
116
  n.mixIntoKey(sharedKey(this.ephemeralKey.private, this.serverStatic))
113
117
 
114
118
  this.serverCertificate = n.decrypt(sh.payload)
119
+ if (typeof this.verifyServerCertificate === 'function') {
120
+ const verified = this.verifyServerCertificate(this.serverCertificate, {
121
+ serverStatic: Buffer.from(this.serverStatic),
122
+ header: Buffer.from(WA_HEADER)
123
+ })
124
+ if (verified !== true) {
125
+ const error = new Error('Noise: sertifikat server ditolak oleh verifier')
126
+ error.code = 'ERR_NOISE_CERTIFICATE_REJECTED'
127
+ throw error
128
+ }
129
+ this.serverCertificateVerified = true
130
+ } else if (this.requireServerCertificateVerification) {
131
+ const error = new Error('Noise: koneksi ditolak karena verifier sertifikat server belum dikonfigurasi')
132
+ error.code = 'ERR_NOISE_CERTIFICATE_VERIFIER_MISSING'
133
+ throw error
134
+ }
115
135
 
116
136
  const encStatic = n.encrypt(this.staticKey.public)
117
137
  n.mixIntoKey(sharedKey(this.staticKey.private, sh.ephemeral))