@dotrino/identity 0.117.0 → 0.117.2
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/package.json +1 -1
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/binpack.js +140 -0
- package/vault/vendor/proxy-client/client.js +214 -42
- package/vault/vendor/proxy-client/index.js +2 -1
- package/vault/vendor/proxy-client/parts.js +37 -7
- package/vault/vendor/proxy-client/sealing.js +42 -0
- package/vault/vendor/proxy-client/webrtc.js +6 -1
- package/vault/vendor/roadmap/VERSION.txt +1 -1
- package/vault/vendor/roadmap/manifests.js +14 -8
- package/vault/vendor/vault/VERSION.txt +1 -1
- package/vault/vendor/vault/core/libVersion.js +1 -1
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/proxy-client@0.
|
|
1
|
+
Copia vendorizada de @dotrino/proxy-client@0.36.1 (dotrino-proxy-client/src/{binpack,canonical,client,encpub,index,parts,sealing,signature,stats-log,stats,webrtc}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
3
|
sealing.js resuelve @dotrino/identity/content de forma PEREZOSA (= ../../content.js
|
|
4
4
|
por el import map): solo se carga si de verdad se sella algo.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BYTES QUE VIAJAN COMO BYTES (0.35.0).
|
|
3
|
+
*
|
|
4
|
+
* Hasta aquí un archivo viajaba dos veces en base64: la app lo metía en base64 en el JSON
|
|
5
|
+
* del mensaje (×1,33), el sellado cifraba ese JSON y lo volvía a pasar a base64 (×1,33), y
|
|
6
|
+
* el resultado iba como texto. Medido: 1,78 veces el tamaño del archivo en el cable.
|
|
7
|
+
*
|
|
8
|
+
* Ahora una app puede poner `Uint8Array` dentro de lo que manda, y el pilar lo lleva tal
|
|
9
|
+
* cual: lo empaqueta (`pack`), lo cifra como bytes y lo manda en una trama BINARIA, al
|
|
10
|
+
* proxio y al canal directo. Lo que no lleva bytes sale exactamente como antes, en texto:
|
|
11
|
+
* así las piezas que no saben de esto (las apps nativas, las versiones anteriores) siguen
|
|
12
|
+
* hablando con todos para lo de siempre.
|
|
13
|
+
*
|
|
14
|
+
* Dos formatos, y los dos son de este archivo para que no haya una segunda copia:
|
|
15
|
+
*
|
|
16
|
+
* · EL PAQUETE (`pack`/`unpack`): un objeto con bytes dentro.
|
|
17
|
+
* [0xB1][u32 largo del JSON][JSON][bytes, uno detrás de otro]
|
|
18
|
+
* Cada `Uint8Array` del objeto queda en el JSON como `{"__dotrino_bin":[desde,largo]}`.
|
|
19
|
+
*
|
|
20
|
+
* · LA TRAMA (`encodeFrame`/`decodeFrame`): lo que va por el WebSocket del proxio.
|
|
21
|
+
* [0x01][u32 largo de la cabecera][cabecera JSON][cuerpo]
|
|
22
|
+
* La cabecera es la trama de texto de siempre sin `message`; el cuerpo es el `message`.
|
|
23
|
+
* El proxio lee la cabecera para enrutar y no mira el cuerpo, que va sellado.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const BIN_TAG = '__dotrino_bin'
|
|
27
|
+
const PACK_MAGIC = 0xB1
|
|
28
|
+
const FRAME_VERSION = 0x01
|
|
29
|
+
|
|
30
|
+
const enc = new TextEncoder()
|
|
31
|
+
const dec = new TextDecoder()
|
|
32
|
+
|
|
33
|
+
function withCode (message, code) {
|
|
34
|
+
return Object.assign(new Error(message), { code })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Bytes, sin importar de qué vista vengan (`Buffer`, `ArrayBuffer`, `Uint8Array`). */
|
|
38
|
+
export function asBytes (v) {
|
|
39
|
+
if (v instanceof Uint8Array) return v
|
|
40
|
+
if (v instanceof ArrayBuffer) return new Uint8Array(v)
|
|
41
|
+
if (ArrayBuffer.isView(v)) return new Uint8Array(v.buffer, v.byteOffset, v.byteLength)
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** ¿Lleva bytes este valor, a cualquier profundidad? Es lo que decide texto o binario. */
|
|
46
|
+
export function hasBinary (v) {
|
|
47
|
+
if (v instanceof Uint8Array || v instanceof ArrayBuffer) return true
|
|
48
|
+
if (Array.isArray(v)) return v.some(hasBinary)
|
|
49
|
+
if (v && typeof v === 'object') {
|
|
50
|
+
for (const k in v) if (hasBinary(v[k])) return true
|
|
51
|
+
}
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Un objeto con bytes dentro → un solo `Uint8Array`. */
|
|
56
|
+
export function pack (obj) {
|
|
57
|
+
const blobs = []
|
|
58
|
+
let off = 0
|
|
59
|
+
const json = JSON.stringify(obj, function (k, v) {
|
|
60
|
+
// `this[k]` es el valor antes de `toJSON`: un Buffer de Node se convertiría en
|
|
61
|
+
// `{ type: 'Buffer', data: [...] }` y aquí lo queremos como bytes.
|
|
62
|
+
const b = asBytes(this[k])
|
|
63
|
+
if (!b) return v
|
|
64
|
+
blobs.push(b)
|
|
65
|
+
const ref = { [BIN_TAG]: [off, b.byteLength] }
|
|
66
|
+
off += b.byteLength
|
|
67
|
+
return ref
|
|
68
|
+
})
|
|
69
|
+
const head = enc.encode(json)
|
|
70
|
+
const out = new Uint8Array(5 + head.length + off)
|
|
71
|
+
out[0] = PACK_MAGIC
|
|
72
|
+
new DataView(out.buffer).setUint32(1, head.length)
|
|
73
|
+
out.set(head, 5)
|
|
74
|
+
let p = 5 + head.length
|
|
75
|
+
for (const b of blobs) { out.set(b, p); p += b.byteLength }
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const isPacked = (b) => b instanceof Uint8Array && b.length >= 5 && b[0] === PACK_MAGIC
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Un paquete → el objeto, con sus bytes como `Uint8Array` (vistas sobre `bytes`, sin copiar).
|
|
83
|
+
* Lanza con `code: 'bad-pack'` si no es un paquete o una referencia se sale de él.
|
|
84
|
+
*/
|
|
85
|
+
export function unpack (bytes) {
|
|
86
|
+
const b = asBytes(bytes)
|
|
87
|
+
if (!isPacked(b)) throw withCode('not a packed message', 'bad-pack')
|
|
88
|
+
const len = new DataView(b.buffer, b.byteOffset, b.byteLength).getUint32(1)
|
|
89
|
+
if (5 + len > b.length) throw withCode('packed message is shorter than its header says', 'bad-pack')
|
|
90
|
+
const area = b.subarray(5 + len)
|
|
91
|
+
let obj
|
|
92
|
+
try { obj = JSON.parse(dec.decode(b.subarray(5, 5 + len))) } catch (_) { throw withCode('packed message header is not JSON', 'bad-pack') }
|
|
93
|
+
const revive = (v) => {
|
|
94
|
+
if (Array.isArray(v)) return v.map(revive)
|
|
95
|
+
if (!v || typeof v !== 'object') return v
|
|
96
|
+
const ref = v[BIN_TAG]
|
|
97
|
+
if (ref !== undefined) {
|
|
98
|
+
if (!Array.isArray(ref) || ref.length !== 2 || !Number.isInteger(ref[0]) || !Number.isInteger(ref[1]) ||
|
|
99
|
+
ref[0] < 0 || ref[1] < 0 || ref[0] + ref[1] > area.length) {
|
|
100
|
+
throw withCode('packed message points outside itself', 'bad-pack')
|
|
101
|
+
}
|
|
102
|
+
return area.subarray(ref[0], ref[0] + ref[1])
|
|
103
|
+
}
|
|
104
|
+
const out = {}
|
|
105
|
+
for (const k of Object.keys(v)) out[k] = revive(v[k])
|
|
106
|
+
return out
|
|
107
|
+
}
|
|
108
|
+
return revive(obj)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Une varios trozos de bytes en uno. */
|
|
112
|
+
export function concatBytes (list) {
|
|
113
|
+
const out = new Uint8Array(list.reduce((a, b) => a + b.byteLength, 0))
|
|
114
|
+
let p = 0
|
|
115
|
+
for (const b of list) { out.set(b, p); p += b.byteLength }
|
|
116
|
+
return out
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** La trama binaria del proxio: la cabecera de texto de siempre, y el `message` en crudo. */
|
|
120
|
+
export function encodeFrame (header, body) {
|
|
121
|
+
const head = enc.encode(JSON.stringify(header))
|
|
122
|
+
const out = new Uint8Array(5 + head.length + body.byteLength)
|
|
123
|
+
out[0] = FRAME_VERSION
|
|
124
|
+
new DataView(out.buffer).setUint32(1, head.length)
|
|
125
|
+
out.set(head, 5)
|
|
126
|
+
out.set(body, 5 + head.length)
|
|
127
|
+
return out
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** El inverso: `{ header, body }`. Lanza con `code: 'bad-frame'` si no es una trama válida. */
|
|
131
|
+
export function decodeFrame (data) {
|
|
132
|
+
const b = asBytes(data)
|
|
133
|
+
if (!b || b.length < 5 || b[0] !== FRAME_VERSION) throw withCode('not a binary frame of a known version', 'bad-frame')
|
|
134
|
+
const len = new DataView(b.buffer, b.byteOffset, b.byteLength).getUint32(1)
|
|
135
|
+
if (5 + len > b.length) throw withCode('binary frame is shorter than its header says', 'bad-frame')
|
|
136
|
+
let header
|
|
137
|
+
try { header = JSON.parse(dec.decode(b.subarray(5, 5 + len))) } catch (_) { throw withCode('binary frame header is not JSON', 'bad-frame') }
|
|
138
|
+
if (!header || typeof header !== 'object' || Array.isArray(header)) throw withCode('binary frame header is not an object', 'bad-frame')
|
|
139
|
+
return { header, body: b.subarray(5 + len) }
|
|
140
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { buildSignedChannel, getPublicKeyJwk, signData, samePubkey } from './signature.js'
|
|
2
|
-
import { seal, open, isSealed } from './sealing.js'
|
|
2
|
+
import { seal, open, isSealed, isBinSealed, BIN_SEALED, makeMessageKey, encryptBytes, decryptBytes } from './sealing.js'
|
|
3
3
|
import { buildEncPubStatement, readEncPubStatement, isEncPub } from './encpub.js'
|
|
4
4
|
import { WebRTCManager, RTC_TAG, DEFAULT_ICE_SERVERS, loadNodePeerConnection, resolvePeerConnection, pinTurnToIPv4 } from './webrtc.js'
|
|
5
5
|
import { TrafficStats, utf8Length, registerTransport, unregisterTransport } from './stats.js'
|
|
6
|
-
import { splitParts, isPart, Reassembler, DIRECT_PART_CHARS, PROXY_PART_CHARS, QUEUE_MAX_CHARS, RELAY_MAX_CHARS } from './parts.js'
|
|
6
|
+
import { splitParts, splitBinParts, isPart, Reassembler, DIRECT_PART_CHARS, PROXY_PART_CHARS, QUEUE_MAX_CHARS, RELAY_MAX_CHARS, DIRECT_PART_BYTES, PROXY_PART_BYTES, QUEUE_MAX_BYTES, RELAY_MAX_BYTES } from './parts.js'
|
|
7
|
+
import { pack, unpack, hasBinary, encodeFrame, decodeFrame } from './binpack.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Error con un `code` estable.
|
|
@@ -23,6 +24,13 @@ import { splitParts, isPart, Reassembler, DIRECT_PART_CHARS, PROXY_PART_CHARS, Q
|
|
|
23
24
|
* proxio ya tiene atada a esta conexión desde `identify`. Nada del usuario viaja aquí.
|
|
24
25
|
*/
|
|
25
26
|
const HELLO_TAG = '__cc_hello__'
|
|
27
|
+
/**
|
|
28
|
+
* Lo que este cliente entiende, y lo dice en el saludo (0.36.0). Hoy una cosa: `binary`, las
|
|
29
|
+
* tramas con bytes (binpack.js). Quien saluda sin decirlo es anterior y no las entiende, así
|
|
30
|
+
* que no se le mandan: se lanza `peer-no-binary` y la app lo enseña. La idea del dueño es que
|
|
31
|
+
* todo pase a binario con el tiempo, y esta lista es por donde se sabrá quién ya puede.
|
|
32
|
+
*/
|
|
33
|
+
const SPEAKS = ['binary']
|
|
26
34
|
|
|
27
35
|
function errorCon (mensaje, code) {
|
|
28
36
|
const e = /** @type {Error & { code: string }} */ (new Error(mensaje))
|
|
@@ -135,6 +143,10 @@ export class WebSocketProxyClient {
|
|
|
135
143
|
* son de nadie.
|
|
136
144
|
*/
|
|
137
145
|
this._tokenPubkeys = new Map()
|
|
146
|
+
// Lo que dijo entender cada token en su saludo, y cada identidad (para lo que va por
|
|
147
|
+
// pubkey). Sin saludo no se sabe, y entonces no se frena nada.
|
|
148
|
+
this._tokenSpeaks = new Map()
|
|
149
|
+
this._pubkeySpeaks = new Map()
|
|
138
150
|
this._helloSent = new Set()
|
|
139
151
|
|
|
140
152
|
/** Mi propia identidad en el cable, la que `identify` dejó atada a este token. */
|
|
@@ -193,6 +205,9 @@ export class WebSocketProxyClient {
|
|
|
193
205
|
this._parts = new Reassembler({
|
|
194
206
|
onExpire: (info) => this._emit('error', { type: 'parts_incomplete', code: 'parts-incomplete', ...info })
|
|
195
207
|
})
|
|
208
|
+
// Las CEK de los mensajes con bytes ya abiertas, por su sobre: todas las partes de un
|
|
209
|
+
// mensaje traen la misma, y abrirla es una vuelta a la bóveda. Pocas: un mensaje vivo.
|
|
210
|
+
this._msgKeys = new Map()
|
|
196
211
|
this._traffic = new TrafficStats()
|
|
197
212
|
|
|
198
213
|
this._rtc = this.enableWebRTC ? new WebRTCManager({
|
|
@@ -341,7 +356,7 @@ export class WebSocketProxyClient {
|
|
|
341
356
|
'unsealed')
|
|
342
357
|
}
|
|
343
358
|
const tokens = Array.isArray(to) ? to : [to]
|
|
344
|
-
const messageStr =
|
|
359
|
+
const messageStr = this._wire(payload)
|
|
345
360
|
if (!this._rtc) {
|
|
346
361
|
this._sendRaw({ to: tokens, message: messageStr })
|
|
347
362
|
return
|
|
@@ -508,18 +523,23 @@ export class WebSocketProxyClient {
|
|
|
508
523
|
async sendSealed (toPubkeys, payload, { peerEncPub, ...opts } = {}) {
|
|
509
524
|
const list = Array.isArray(toPubkeys) ? toPubkeys : [toPubkeys]
|
|
510
525
|
// Por pubkey el mensaje puede quedarse ESPERANDO en el proxio, y ahí se deja como mucho
|
|
511
|
-
//
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
526
|
+
// 8 MB de archivo (parts.js). Lo que pasa de eso no se manda: se dice, y la app decide.
|
|
527
|
+
const m = this._measure(payload)
|
|
528
|
+
const queueMax = m.bin ? QUEUE_MAX_BYTES : QUEUE_MAX_CHARS
|
|
529
|
+
if (m.len > queueMax) {
|
|
530
|
+
throw errorCon(`sendSealed: ${m.len} ${m.bin ? 'bytes' : 'chars'} is more than can wait in the proxy (${queueMax}); send it by token while both ends are connected`, 'too-big-for-queue')
|
|
515
531
|
}
|
|
516
|
-
|
|
532
|
+
// Por pubkey todo va por el proxio: si no admite bytes, se dice antes de mandar nada. Y
|
|
533
|
+
// si alguno de los destinatarios dijo que no los entiende, tampoco.
|
|
534
|
+
if (m.bin) { this._needBinaryProxy(); this._needPeerBinary(list) }
|
|
535
|
+
const partSize = m.bin ? PROXY_PART_BYTES : PROXY_PART_CHARS
|
|
536
|
+
const pieces = m.len <= partSize ? [m.bin ? m.whole : payload]
|
|
537
|
+
: m.bin ? splitBinParts(m.whole, partSize) : splitParts(m.whole, partSize)
|
|
517
538
|
// Con la llave puesta a mano se respeta tal cual: quien la pasa está diciendo que ya
|
|
518
539
|
// sabe de quién es (se emparejaron), y un sobre vale para todos los destinatarios.
|
|
519
540
|
if (peerEncPub) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
541
|
+
const sealPiece = await this._sealerFor(m.bin, peerEncPub, list.length === 1 ? list[0] : undefined)
|
|
542
|
+
for (const piece of pieces) this._sendByPubkeyRaw(list, await sealPiece(piece), opts)
|
|
523
543
|
return
|
|
524
544
|
}
|
|
525
545
|
// Sin ella, se averigua. UNA ENVOLTURA POR DESTINATARIO: cada uno tiene su llave, así
|
|
@@ -530,7 +550,8 @@ export class WebSocketProxyClient {
|
|
|
530
550
|
// sin forma de saber cuál mitad.
|
|
531
551
|
const llaves = await Promise.all(list.map(async (pk) => [pk, await this.encPubOf(pk)]))
|
|
532
552
|
for (const [pk, encPub] of llaves) {
|
|
533
|
-
|
|
553
|
+
const sealPiece = await this._sealerFor(m.bin, encPub, pk)
|
|
554
|
+
for (const piece of pieces) this._sendByPubkeyRaw([pk], await sealPiece(piece), opts)
|
|
534
555
|
}
|
|
535
556
|
}
|
|
536
557
|
|
|
@@ -565,11 +586,18 @@ export class WebSocketProxyClient {
|
|
|
565
586
|
// Por token los dos están conectados y NO HAY TOPE (parts.js): lo que no cabe en un
|
|
566
587
|
// mensaje del camino que toca sale en partes. El tamaño de parte lo decide el camino
|
|
567
588
|
// de ahora: el canal directo admite mucho menos por mensaje que el proxio.
|
|
568
|
-
const
|
|
589
|
+
const m = this._measure(payload)
|
|
569
590
|
const direct = tokens.length === 1 && !!this._rtc?.isOpen(tokens[0])
|
|
570
|
-
const size = direct ? DIRECT_PART_CHARS : PROXY_PART_CHARS
|
|
571
|
-
|
|
572
|
-
|
|
591
|
+
const size = m.bin ? (direct ? DIRECT_PART_BYTES : PROXY_PART_BYTES) : (direct ? DIRECT_PART_CHARS : PROXY_PART_CHARS)
|
|
592
|
+
const relayMax = m.bin ? RELAY_MAX_BYTES : RELAY_MAX_CHARS
|
|
593
|
+
const queueMax = m.bin ? QUEUE_MAX_BYTES : QUEUE_MAX_CHARS
|
|
594
|
+
// Por token decide lo que dijo ESA conexión: lo recordado de la identidad puede ser de una
|
|
595
|
+
// versión que ya se actualizó.
|
|
596
|
+
if (m.bin) this._needPeerBinary(tokens)
|
|
597
|
+
if (m.bin && !direct) this._needBinaryProxy()
|
|
598
|
+
const sealPiece = await this._sealerFor(m.bin, peerEncPub, peerPubkey)
|
|
599
|
+
if (m.len <= size) {
|
|
600
|
+
const sobre = await sealPiece(m.bin ? m.whole : payload)
|
|
573
601
|
if (tokens.length === 1 && peerPubkey) { this._sendToTokenOrQueue(tokens[0], sobre, peerPubkey); return }
|
|
574
602
|
// Por `send`, no por `_sendRaw`: así sigue prefiriendo el canal directo si lo hay.
|
|
575
603
|
this.send(tokens, sobre)
|
|
@@ -577,18 +605,18 @@ export class WebSocketProxyClient {
|
|
|
577
605
|
}
|
|
578
606
|
// Por el proxio en vivo hay tope (parts.js): lo que pasa de él solo sale por canal
|
|
579
607
|
// directo. Se pide el canal para que el siguiente intento lo encuentre abierto.
|
|
580
|
-
if (!direct &&
|
|
608
|
+
if (!direct && m.len > relayMax) {
|
|
581
609
|
this._upgradeDirect(tokens)
|
|
582
|
-
throw errorCon(`sendSealedTo: ${
|
|
610
|
+
throw errorCon(`sendSealedTo: ${m.len} ${m.bin ? 'bytes' : 'chars'} is more than goes through the proxy live (${relayMax}); it needs a direct channel`, 'too-big-for-relay')
|
|
583
611
|
}
|
|
584
612
|
// Si el token muere a mitad, lo que quede se iría a la cola por pubkey; solo se permite
|
|
585
613
|
// si el mensaje entero cabe ahí. Si no, se dice (`too-big-for-queue`), no se deja.
|
|
586
|
-
const queueFallback =
|
|
614
|
+
const queueFallback = m.len <= queueMax
|
|
587
615
|
// Y si el canal directo se cae a mitad, el resto va por el proxio en vivo solo si el
|
|
588
616
|
// mensaje entero cabe por ahí; si no, se para y se dice (`direct-lost`).
|
|
589
|
-
const relayFallback =
|
|
590
|
-
for (const piece of splitParts(
|
|
591
|
-
const sobre = await
|
|
617
|
+
const relayFallback = m.len <= relayMax
|
|
618
|
+
for (const piece of (m.bin ? splitBinParts(m.whole, size) : splitParts(m.whole, size))) {
|
|
619
|
+
const sobre = await sealPiece(piece)
|
|
592
620
|
if (tokens.length === 1 && peerPubkey) await this._sendPiece(tokens[0], sobre, peerPubkey, queueFallback, relayFallback)
|
|
593
621
|
else this.send(tokens, sobre)
|
|
594
622
|
}
|
|
@@ -620,10 +648,11 @@ export class WebSocketProxyClient {
|
|
|
620
648
|
* una pausa corta entre partes, para no chocar con su límite de mensajes por segundo.
|
|
621
649
|
*/
|
|
622
650
|
async _sendPiece (token, sobre, peerPubkey, queueFallback, relayFallback = true) {
|
|
623
|
-
if (this._rtc && await this._rtc.sendDirect(token,
|
|
651
|
+
if (this._rtc && await this._rtc.sendDirect(token, this._wire(sobre))) return
|
|
624
652
|
if (!relayFallback) {
|
|
625
653
|
throw errorCon('the direct channel closed in the middle of a message too big for the proxy', 'direct-lost')
|
|
626
654
|
}
|
|
655
|
+
if (sobre instanceof Uint8Array) this._needBinaryProxy()
|
|
627
656
|
this._sendToTokenOrQueue(token, sobre, peerPubkey, { queueFallback, skipDirect: true })
|
|
628
657
|
await new Promise((r) => setTimeout(r, 20))
|
|
629
658
|
}
|
|
@@ -664,7 +693,7 @@ export class WebSocketProxyClient {
|
|
|
664
693
|
if (this.requireSealed && !this._isSealed(sobre)) {
|
|
665
694
|
throw errorCon('requireSealed: refusing to send a directed message in the clear — use sendSealedTo()', 'unsealed')
|
|
666
695
|
}
|
|
667
|
-
const messageStr =
|
|
696
|
+
const messageStr = this._wire(sobre)
|
|
668
697
|
if (!skipDirect && this._rtc && this._rtc.trySend(token, messageStr)) return
|
|
669
698
|
const id = `msg_${this._nextId++}`
|
|
670
699
|
const timer = setTimeout(() => this._tokenWatch.delete(id), 15000)
|
|
@@ -704,7 +733,8 @@ export class WebSocketProxyClient {
|
|
|
704
733
|
* Un token es una dirección del proxio y no dice de quién es, así que sin esto no hay
|
|
705
734
|
* a quién sellarle: una sala de desconocidos se queda muda o —lo que pasaba— hablando
|
|
706
735
|
* en claro. El saludo lleva SOLO mi llave pública, la misma que el proxio ya tiene
|
|
707
|
-
* atada a esta conexión desde `identify
|
|
736
|
+
* atada a esta conexión desde `identify`, y lo que este cliente entiende (`SPEAKS`, que es
|
|
737
|
+
* lo mismo para todos los de esta versión); no hay nada del usuario dentro y por eso no
|
|
708
738
|
* necesita sobre.
|
|
709
739
|
*
|
|
710
740
|
* Quien lo recibe contesta el suyo una vez, así que basta con que salude UNA de las
|
|
@@ -723,7 +753,7 @@ export class WebSocketProxyClient {
|
|
|
723
753
|
for (const t of tokens) {
|
|
724
754
|
if (!t || t === this.token) continue
|
|
725
755
|
this._helloSent.add(t)
|
|
726
|
-
this._proxySendOne(t, { t: HELLO_TAG, publickey: this.myPublickey })
|
|
756
|
+
this._proxySendOne(t, { t: HELLO_TAG, publickey: this.myPublickey, speaks: SPEAKS })
|
|
727
757
|
}
|
|
728
758
|
}
|
|
729
759
|
|
|
@@ -734,9 +764,34 @@ export class WebSocketProxyClient {
|
|
|
734
764
|
|
|
735
765
|
/** Olvidar un token (se fue, o se quiere volver a preguntar). Sin argumento, todos. */
|
|
736
766
|
forgetToken (token) {
|
|
737
|
-
if (token == null) { this._tokenPubkeys.clear(); this._helloSent.clear(); return }
|
|
767
|
+
if (token == null) { this._tokenPubkeys.clear(); this._helloSent.clear(); this._tokenSpeaks.clear(); return }
|
|
738
768
|
this._tokenPubkeys.delete(token)
|
|
739
769
|
this._helloSent.delete(token)
|
|
770
|
+
this._tokenSpeaks.delete(token)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* ¿Entiende esto la otra punta? `true` o `false` si lo dijo en su saludo, y `null` si
|
|
775
|
+
* todavía no ha saludado (no se sabe). Acepta un token o una pubkey.
|
|
776
|
+
* @param {string} who
|
|
777
|
+
* @param {string} [feature]
|
|
778
|
+
* @returns {boolean|null}
|
|
779
|
+
*/
|
|
780
|
+
speaks (who, feature = 'binary') {
|
|
781
|
+
let set = this._tokenSpeaks.get(who) || this._pubkeySpeaks.get(who)
|
|
782
|
+
if (!set) {
|
|
783
|
+
for (const [pk, s] of this._pubkeySpeaks) if (samePubkey(pk, who)) { set = s; break }
|
|
784
|
+
}
|
|
785
|
+
return set ? set.has(feature) : null
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** Antes de mandar bytes: si alguno dijo que no los entiende, no sale nada y se dice. */
|
|
789
|
+
_needPeerBinary (targets) {
|
|
790
|
+
for (const t of targets) {
|
|
791
|
+
if (t && this.speaks(t, 'binary') === false) {
|
|
792
|
+
throw errorCon('the other side does not take binary messages (it needs @dotrino/proxy-client >= 0.36.0)', 'peer-no-binary')
|
|
793
|
+
}
|
|
794
|
+
}
|
|
740
795
|
}
|
|
741
796
|
|
|
742
797
|
_onHello (from, msg) {
|
|
@@ -750,6 +805,9 @@ export class WebSocketProxyClient {
|
|
|
750
805
|
return
|
|
751
806
|
}
|
|
752
807
|
this._tokenPubkeys.set(from, msg.publickey)
|
|
808
|
+
const speaks = new Set(Array.isArray(msg.speaks) ? msg.speaks.filter((x) => typeof x === 'string').slice(0, 32) : [])
|
|
809
|
+
this._tokenSpeaks.set(from, speaks)
|
|
810
|
+
this._pubkeySpeaks.set(msg.publickey, speaks)
|
|
753
811
|
// Contestar UNA vez: el saludo queda simétrico sin rebotar para siempre.
|
|
754
812
|
if (!this._helloSent.has(from) && this.myPublickey) this.helloTo(from)
|
|
755
813
|
this._emit('peer_identity', from, msg.publickey)
|
|
@@ -763,6 +821,62 @@ export class WebSocketProxyClient {
|
|
|
763
821
|
: seal(payload, peerEncPub)
|
|
764
822
|
}
|
|
765
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Quién sella las piezas de UN mensaje hacia UN destinatario. Con bytes (sealing.js): una
|
|
826
|
+
* CEK para el mensaje entero, sellada una vez con el sellado de siempre, y cada pieza
|
|
827
|
+
* cifrada como bytes con ella. Sin bytes, el sobre de texto de siempre por pieza.
|
|
828
|
+
*/
|
|
829
|
+
async _sealerFor (bin, peerEncPub, peerPubkey) {
|
|
830
|
+
if (!bin) return (piece) => this._seal(piece, peerEncPub, peerPubkey)
|
|
831
|
+
const cek = await makeMessageKey()
|
|
832
|
+
const k = await this._seal({ cek }, peerEncPub, peerPubkey)
|
|
833
|
+
return async (bytes) => {
|
|
834
|
+
const { iv, ct } = await encryptBytes(cek, bytes)
|
|
835
|
+
return pack({ bv: BIN_SEALED, k, iv, ct })
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/** Lo que mide un mensaje para sus topes: su paquete si lleva bytes, su JSON si no. */
|
|
840
|
+
_measure (payload) {
|
|
841
|
+
if (hasBinary(payload)) {
|
|
842
|
+
const whole = pack(payload)
|
|
843
|
+
return { bin: true, whole, len: whole.byteLength }
|
|
844
|
+
}
|
|
845
|
+
const whole = JSON.stringify(payload)
|
|
846
|
+
return { bin: false, whole, len: whole.length }
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Cómo va por el cable: los bytes tal cual, el texto tal cual, y un objeto en JSON (o empaquetado, si lleva bytes). */
|
|
850
|
+
_wire (payload) {
|
|
851
|
+
if (typeof payload === 'string' || payload instanceof Uint8Array) return payload
|
|
852
|
+
if (hasBinary(payload)) return pack(payload)
|
|
853
|
+
return JSON.stringify(payload)
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/** Un mensaje con bytes por el proxio necesita uno que admita tramas binarias (websocket-proxy ≥ 1.6.0). */
|
|
857
|
+
_needBinaryProxy () {
|
|
858
|
+
if (!Array.isArray(this.caps) || !this.caps.includes('binary')) {
|
|
859
|
+
throw errorCon('this proxy does not take binary frames (it needs websocket-proxy >= 1.6.0)', 'no-binary-support')
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** La CEK de un mensaje con bytes, abriendo su sobre una vez por mensaje. */
|
|
864
|
+
_openMessageKey (k, meta) {
|
|
865
|
+
const id = JSON.stringify(k)
|
|
866
|
+
let p = this._msgKeys.get(id)
|
|
867
|
+
if (!p) {
|
|
868
|
+
p = (async () => {
|
|
869
|
+
const o = this.sealing ? await this.sealing.open(k, meta) : await open(k, this.myEncPrivateKey)
|
|
870
|
+
if (!o || typeof o.cek !== 'string') throw errorCon('the sealed key of a message has no key inside', 'bad-pack')
|
|
871
|
+
return o.cek
|
|
872
|
+
})()
|
|
873
|
+
this._msgKeys.set(id, p)
|
|
874
|
+
if (this._msgKeys.size > 64) this._msgKeys.delete(this._msgKeys.keys().next().value)
|
|
875
|
+
p.catch(() => this._msgKeys.delete(id))
|
|
876
|
+
}
|
|
877
|
+
return p
|
|
878
|
+
}
|
|
879
|
+
|
|
766
880
|
// ---------- llaves de cifrado ajenas ----------
|
|
767
881
|
|
|
768
882
|
/**
|
|
@@ -859,6 +973,11 @@ export class WebSocketProxyClient {
|
|
|
859
973
|
}
|
|
860
974
|
|
|
861
975
|
_isSealed (msg) {
|
|
976
|
+
if (msg instanceof Uint8Array) {
|
|
977
|
+
let env
|
|
978
|
+
try { env = unpack(msg) } catch (_) { return false }
|
|
979
|
+
return isBinSealed(env) && this._isSealed(env.k)
|
|
980
|
+
}
|
|
862
981
|
return this.sealing ? this.sealing.isSealed(msg) : isSealed(msg)
|
|
863
982
|
}
|
|
864
983
|
|
|
@@ -881,6 +1000,33 @@ export class WebSocketProxyClient {
|
|
|
881
1000
|
* into the app.
|
|
882
1001
|
*/
|
|
883
1002
|
async _deliver (from, payload, meta) {
|
|
1003
|
+
// BYTES (binpack.js): un paquete. Si es un sobre binario se abre aquí; si no, es un
|
|
1004
|
+
// objeto con bytes sin sellar, y decide `requireSealed` igual que con el texto.
|
|
1005
|
+
if (payload instanceof Uint8Array) {
|
|
1006
|
+
let env
|
|
1007
|
+
try { env = unpack(payload) } catch (e) {
|
|
1008
|
+
this._emit('error', { type: 'bad_pack', from, code: e.code || 'bad-pack', error: e })
|
|
1009
|
+
return
|
|
1010
|
+
}
|
|
1011
|
+
if (isBinSealed(env)) {
|
|
1012
|
+
if (!this.sealing && !this.myEncPrivateKey) {
|
|
1013
|
+
this._emit('error', { type: 'unsealed', reason: 'no_encryption_key', from })
|
|
1014
|
+
return
|
|
1015
|
+
}
|
|
1016
|
+
let opened
|
|
1017
|
+
try {
|
|
1018
|
+
if (!this._isSealed(env.k)) throw errorCon('the key of a sealed message is not sealed', 'bad-pack')
|
|
1019
|
+
opened = unpack(await decryptBytes(await this._openMessageKey(env.k, meta), env))
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
this._emit('error', { type: 'undecipherable', from, error: e })
|
|
1022
|
+
return
|
|
1023
|
+
}
|
|
1024
|
+
const senderEncPub = this.sealing?.senderOf?.(env.k) || null
|
|
1025
|
+
this._handOpened(from, opened, { ...meta, sealed: true, ...(senderEncPub ? { senderEncPub } : {}) })
|
|
1026
|
+
return
|
|
1027
|
+
}
|
|
1028
|
+
payload = env
|
|
1029
|
+
}
|
|
884
1030
|
if (this._isSealed(payload)) {
|
|
885
1031
|
if (!this.sealing && !this.myEncPrivateKey) {
|
|
886
1032
|
this._emit('error', { type: 'unsealed', reason: 'no_encryption_key', from })
|
|
@@ -893,16 +1039,7 @@ export class WebSocketProxyClient {
|
|
|
893
1039
|
// Quién selló (la llave de cifrado), si el sobre lo dice: es lo que autentica al
|
|
894
1040
|
// remitente, porque solo quien tiene esa privada pudo armarlo.
|
|
895
1041
|
const senderEncPub = this.sealing?.senderOf?.(payload) || null
|
|
896
|
-
|
|
897
|
-
if (isPart(opened)) {
|
|
898
|
-
// Se agrupa por quien selló: solo esa llave pudo armar cada parte.
|
|
899
|
-
let whole
|
|
900
|
-
try { whole = this._parts.add(senderEncPub || meta.fromPubkey || from || '', opened) }
|
|
901
|
-
catch (e) { this._emit('error', { type: 'bad_part', from, code: e.code || 'bad-part', error: e }); return }
|
|
902
|
-
if (whole !== null) this._emit('message', from, whole, { ...fullMeta, parts: opened.__dotrino_part.n })
|
|
903
|
-
return
|
|
904
|
-
}
|
|
905
|
-
this._emit('message', from, opened, fullMeta)
|
|
1042
|
+
this._handOpened(from, opened, { ...meta, sealed: true, ...(senderEncPub ? { senderEncPub } : {}) })
|
|
906
1043
|
} catch (e) {
|
|
907
1044
|
// Sealed to somebody else, or tampered with. Staying quiet is the point.
|
|
908
1045
|
this._emit('error', { type: 'undecipherable', from, error: e })
|
|
@@ -917,11 +1054,24 @@ export class WebSocketProxyClient {
|
|
|
917
1054
|
this._emit('message', from, payload, { ...meta, sealed: false })
|
|
918
1055
|
}
|
|
919
1056
|
|
|
1057
|
+
/** Lo ya abierto a la app: entero, o a rearmar si es una parte (parts.js). */
|
|
1058
|
+
_handOpened (from, opened, fullMeta) {
|
|
1059
|
+
if (isPart(opened)) {
|
|
1060
|
+
// Se agrupa por quien selló: solo esa llave pudo armar cada parte.
|
|
1061
|
+
let whole
|
|
1062
|
+
try { whole = this._parts.add(fullMeta.senderEncPub || fullMeta.fromPubkey || from || '', opened) }
|
|
1063
|
+
catch (e) { this._emit('error', { type: 'bad_part', from, code: e.code || 'bad-part', error: e }); return }
|
|
1064
|
+
if (whole !== null) this._emit('message', from, whole, { ...fullMeta, parts: opened.__dotrino_part.n })
|
|
1065
|
+
return
|
|
1066
|
+
}
|
|
1067
|
+
this._emit('message', from, opened, fullMeta)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
920
1070
|
_sendByPubkeyRaw (toPubkeys, payload, opts = {}) {
|
|
921
1071
|
const list = Array.isArray(toPubkeys) ? toPubkeys : [toPubkeys]
|
|
922
1072
|
const msg = {
|
|
923
1073
|
to_publickey: list,
|
|
924
|
-
message:
|
|
1074
|
+
message: this._wire(payload)
|
|
925
1075
|
}
|
|
926
1076
|
if (opts.ephemeral) msg.ephemeral = true
|
|
927
1077
|
if (opts.quiet) msg.quiet = true
|
|
@@ -1347,6 +1497,9 @@ export class WebSocketProxyClient {
|
|
|
1347
1497
|
try { prev.close() } catch (_) {}
|
|
1348
1498
|
}
|
|
1349
1499
|
const ws = new WebSocket(this.url)
|
|
1500
|
+
// Las tramas binarias (binpack.js) llegan como ArrayBuffer: un Blob obligaría a leerlas
|
|
1501
|
+
// de forma asíncrona y desordenaría lo que llega detrás.
|
|
1502
|
+
try { ws.binaryType = 'arraybuffer' } catch (_) {}
|
|
1350
1503
|
this.ws = ws
|
|
1351
1504
|
const openTimer = this.connectTimeout > 0 ? setTimeout(() => this._onConnectTimeout(ws), this.connectTimeout) : null
|
|
1352
1505
|
openTimer?.unref?.()
|
|
@@ -1484,9 +1637,20 @@ export class WebSocketProxyClient {
|
|
|
1484
1637
|
|
|
1485
1638
|
_handleFrame (raw) {
|
|
1486
1639
|
let data
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1640
|
+
if (typeof raw === 'string') {
|
|
1641
|
+
try { data = JSON.parse(raw) } catch (e) {
|
|
1642
|
+
this._emit('error', { type: 'parse_error', error: e })
|
|
1643
|
+
return
|
|
1644
|
+
}
|
|
1645
|
+
} else {
|
|
1646
|
+
// Trama binaria: la cabecera de siempre y el `message` en crudo.
|
|
1647
|
+
try {
|
|
1648
|
+
const { header, body } = decodeFrame(raw)
|
|
1649
|
+
data = { ...header, message: body }
|
|
1650
|
+
} catch (e) {
|
|
1651
|
+
this._emit('error', { type: 'parse_error', code: e.code || 'bad-frame', error: e })
|
|
1652
|
+
return
|
|
1653
|
+
}
|
|
1490
1654
|
}
|
|
1491
1655
|
const { type } = data
|
|
1492
1656
|
switch (type) {
|
|
@@ -1619,6 +1783,14 @@ export class WebSocketProxyClient {
|
|
|
1619
1783
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1620
1784
|
throw errorCon('WebSocket not connected', 'NOT_CONNECTED')
|
|
1621
1785
|
}
|
|
1786
|
+
if (frame.message instanceof Uint8Array) {
|
|
1787
|
+
this._needBinaryProxy()
|
|
1788
|
+
const { message, ...header } = frame
|
|
1789
|
+
const bytes = encodeFrame(header, message)
|
|
1790
|
+
this.ws.send(bytes)
|
|
1791
|
+
this._countOut(frame, bytes)
|
|
1792
|
+
return
|
|
1793
|
+
}
|
|
1622
1794
|
const texto = JSON.stringify(frame)
|
|
1623
1795
|
this.ws.send(texto)
|
|
1624
1796
|
this._countOut(frame, texto)
|
|
@@ -1627,7 +1799,7 @@ export class WebSocketProxyClient {
|
|
|
1627
1799
|
/** Cuenta un frame que sale por el proxio, y a quién iba. */
|
|
1628
1800
|
_countOut (frame, texto) {
|
|
1629
1801
|
this._traffic.frame('out', utf8Length(texto))
|
|
1630
|
-
if (typeof frame.message !== 'string') return
|
|
1802
|
+
if (typeof frame.message !== 'string' && !(frame.message instanceof Uint8Array)) return
|
|
1631
1803
|
const bytes = utf8Length(frame.message)
|
|
1632
1804
|
for (const token of Array.isArray(frame.to) ? frame.to : []) {
|
|
1633
1805
|
this._traffic.peer('out', 'proxy', { token, pubkey: this._tokenPubkeys.get(token) || null }, bytes)
|
|
@@ -11,7 +11,8 @@ export {
|
|
|
11
11
|
} from './encpub.js'
|
|
12
12
|
|
|
13
13
|
export { listTransports, routeOf } from './stats.js'
|
|
14
|
-
export { PART_TAG, DIRECT_PART_CHARS, PROXY_PART_CHARS, QUEUE_MAX_CHARS, RELAY_MAX_CHARS } from './parts.js'
|
|
14
|
+
export { PART_TAG, DIRECT_PART_CHARS, PROXY_PART_CHARS, QUEUE_MAX_CHARS, RELAY_MAX_CHARS, DIRECT_PART_BYTES, PROXY_PART_BYTES, QUEUE_MAX_BYTES, RELAY_MAX_BYTES } from './parts.js'
|
|
15
|
+
export { pack, unpack, hasBinary, encodeFrame, decodeFrame } from './binpack.js'
|
|
15
16
|
export { logStats, formatStats, netReport, splitPeers } from './stats-log.js'
|
|
16
17
|
|
|
17
18
|
import { WebSocketProxyClient } from './client.js'
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* llave que selló: nadie más puede colar una parte en el mensaje de otro.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { pack, unpack, concatBytes } from './binpack.js'
|
|
26
|
+
|
|
25
27
|
export const PART_TAG = '__dotrino_part'
|
|
26
28
|
|
|
27
29
|
/** Parte por el canal directo: por debajo de los 64 KiB que acepta libwebrtc, ya sellada. */
|
|
@@ -36,6 +38,21 @@ export const QUEUE_MAX_CHARS = 11 * 1024 * 1024
|
|
|
36
38
|
/** Lo más que pasa por el proxio EN VIVO: 25 MB de archivo son ~33,4 MB en base64. */
|
|
37
39
|
export const RELAY_MAX_CHARS = 34 * 1024 * 1024
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* LOS MISMOS TOPES PARA UN MENSAJE CON BYTES (0.35.0), medidos sobre su paquete (binpack.js),
|
|
43
|
+
* que ocupa lo que el archivo más unos pocos bytes: 8 MB y 25 MB de archivo, con 64 KB de
|
|
44
|
+
* margen para el texto que va alrededor (nombre, tipo, pie de foto).
|
|
45
|
+
*/
|
|
46
|
+
export const QUEUE_MAX_BYTES = 8 * 1024 * 1024 + 64 * 1024
|
|
47
|
+
export const RELAY_MAX_BYTES = 25 * 1024 * 1024 + 64 * 1024
|
|
48
|
+
/**
|
|
49
|
+
* Parte binaria por el canal directo: bajo los 64 KiB de libwebrtc, con sitio para el sobre
|
|
50
|
+
* de la llave, que la bóveda envuelve para cada aparato del destinatario.
|
|
51
|
+
*/
|
|
52
|
+
export const DIRECT_PART_BYTES = 48 * 1024
|
|
53
|
+
/** Parte binaria por el proxio: cabe en su trama de 1 MB con la cabecera y el sobre. */
|
|
54
|
+
export const PROXY_PART_BYTES = 768 * 1024
|
|
55
|
+
|
|
39
56
|
/** Rearmado: lo que se acepta guardar a medias, y cuánto se espera a la parte siguiente. */
|
|
40
57
|
const RECV_MAX_CHARS = 512 * 1024 * 1024
|
|
41
58
|
const RECV_IDLE_MS = 5 * 60 * 1000
|
|
@@ -57,6 +74,18 @@ export function splitParts (json, size) {
|
|
|
57
74
|
return out
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Las partes de un paquete de `bytes.length` > `size`, cada una ya empaquetada (lista para
|
|
79
|
+
* cifrar como bytes). El rearmado une los trozos y desempaqueta el resultado.
|
|
80
|
+
*/
|
|
81
|
+
export function splitBinParts (bytes, size) {
|
|
82
|
+
const n = Math.ceil(bytes.byteLength / size)
|
|
83
|
+
const id = partId()
|
|
84
|
+
const out = []
|
|
85
|
+
for (let i = 0; i < n; i++) out.push(pack({ [PART_TAG]: { id, i, n }, d: bytes.subarray(i * size, (i + 1) * size) }))
|
|
86
|
+
return out
|
|
87
|
+
}
|
|
88
|
+
|
|
60
89
|
export const isPart = (o) => !!(o && typeof o === 'object' && o[PART_TAG])
|
|
61
90
|
|
|
62
91
|
/**
|
|
@@ -77,25 +106,26 @@ export class Reassembler {
|
|
|
77
106
|
const d = part.d
|
|
78
107
|
if (!p || typeof p.id !== 'string' || p.id.length > 64 ||
|
|
79
108
|
!Number.isInteger(p.n) || !Number.isInteger(p.i) || p.n < 2 || p.n > MAX_PARTS ||
|
|
80
|
-
p.i < 0 || p.i >= p.n || typeof d !== 'string') {
|
|
109
|
+
p.i < 0 || p.i >= p.n || (typeof d !== 'string' && !(d instanceof Uint8Array))) {
|
|
81
110
|
throw Object.assign(new Error('malformed message part'), { code: 'bad-part' })
|
|
82
111
|
}
|
|
83
112
|
const key = `${sender}|${p.id}`
|
|
84
113
|
let a = this._open.get(key)
|
|
85
114
|
if (!a) {
|
|
86
|
-
a = { n: p.n, got: new Array(p.n), count: 0, chars: 0, timer: null }
|
|
115
|
+
a = { n: p.n, bin: d instanceof Uint8Array, got: new Array(p.n), count: 0, chars: 0, timer: null }
|
|
87
116
|
this._open.set(key, a)
|
|
88
117
|
}
|
|
89
|
-
if (a.n !== p.n) {
|
|
118
|
+
if (a.n !== p.n || a.bin !== (d instanceof Uint8Array)) {
|
|
90
119
|
this._drop(key)
|
|
91
|
-
throw Object.assign(new Error('message parts disagree on their count'), { code: 'bad-part' })
|
|
120
|
+
throw Object.assign(new Error('message parts disagree on their count or kind'), { code: 'bad-part' })
|
|
92
121
|
}
|
|
93
122
|
if (a.got[p.i] === undefined) {
|
|
94
123
|
if (this._chars + d.length > RECV_MAX_CHARS) {
|
|
95
124
|
this._drop(key)
|
|
96
125
|
throw Object.assign(new Error('too much of a message in parts is pending'), { code: 'parts-too-big' })
|
|
97
126
|
}
|
|
98
|
-
|
|
127
|
+
// Una copia: `d` es una vista sobre la trama entera, y guardarla retendría la trama.
|
|
128
|
+
a.got[p.i] = a.bin ? d.slice() : d
|
|
99
129
|
a.count++
|
|
100
130
|
a.chars += d.length
|
|
101
131
|
this._chars += d.length
|
|
@@ -103,8 +133,8 @@ export class Reassembler {
|
|
|
103
133
|
if (a.timer) clearTimeout(a.timer)
|
|
104
134
|
if (a.count === a.n) {
|
|
105
135
|
this._drop(key)
|
|
106
|
-
try { return JSON.parse(a.got.join('')) }
|
|
107
|
-
catch (_) { throw Object.assign(new Error('a message in parts did not join into
|
|
136
|
+
try { return a.bin ? unpack(concatBytes(a.got)) : JSON.parse(a.got.join('')) }
|
|
137
|
+
catch (_) { throw Object.assign(new Error('a message in parts did not join into a message'), { code: 'bad-part' }) }
|
|
108
138
|
}
|
|
109
139
|
a.timer = setTimeout(() => {
|
|
110
140
|
const missing = a.n - a.count
|
|
@@ -164,3 +164,45 @@ export function identitySealing (identity, { app = 'dotrino' } = {}) {
|
|
|
164
164
|
isSealed: (m) => !!m && m.app === app && !!m.sealed,
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* SELLAR BYTES (0.35.0). Un archivo no pasa por el sellado de texto —que lo volvería a
|
|
170
|
+
* base64—: se cifra con una CEK nueva (`encryptBytesWithCek` de `@dotrino/identity/content`,
|
|
171
|
+
* AES-GCM) y lo que se sella con el sellado de siempre es SOLO la CEK. Así la bóveda del
|
|
172
|
+
* navegador ve una llave de 44 caracteres y no los megas, y no hay cripto nueva: la misma
|
|
173
|
+
* envoltura de siempre más el AES-GCM del pilar de identidad.
|
|
174
|
+
*
|
|
175
|
+
* El sobre binario es un paquete (`binpack.js`) con `{ bv: 1, k, iv, ct }`:
|
|
176
|
+
* · `k` el sobre de siempre (del sellado propio o de la bóveda) con `{ cek }` dentro
|
|
177
|
+
* · `iv`, `ct` los bytes cifrados, como bytes
|
|
178
|
+
*
|
|
179
|
+
* Una CEK vale para todas las partes de un mensaje (cada parte con su `iv`, que es lo que
|
|
180
|
+
* AES-GCM exige), así que la bóveda sella una vez por mensaje y no una por parte.
|
|
181
|
+
*/
|
|
182
|
+
export const BIN_SEALED = 1
|
|
183
|
+
|
|
184
|
+
export function isBinSealed (env) {
|
|
185
|
+
return !!env && env.bv === BIN_SEALED && !!env.k && typeof env.k === 'object' &&
|
|
186
|
+
env.iv instanceof Uint8Array && env.ct instanceof Uint8Array
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Una CEK nueva para un mensaje con bytes. */
|
|
190
|
+
export async function makeMessageKey () {
|
|
191
|
+
const { makeContentKey } = await crypto_()
|
|
192
|
+
if (typeof makeContentKey !== 'function') throw errorCon('sealing bytes needs @dotrino/identity >= 0.117.0', 'no-bytes-sealing')
|
|
193
|
+
return makeContentKey()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Cifra `bytes` con la CEK y devuelve los campos del sobre (`iv`, `ct`). */
|
|
197
|
+
export async function encryptBytes (cek, bytes) {
|
|
198
|
+
const { encryptBytesWithCek } = await crypto_()
|
|
199
|
+
if (typeof encryptBytesWithCek !== 'function') throw errorCon('sealing bytes needs @dotrino/identity >= 0.117.0', 'no-bytes-sealing')
|
|
200
|
+
return encryptBytesWithCek({ cek, bytes })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Descifra el `ct` de un sobre binario con su CEK ya abierta. */
|
|
204
|
+
export async function decryptBytes (cek, env) {
|
|
205
|
+
const { decryptBytesWithCek } = await crypto_()
|
|
206
|
+
if (typeof decryptBytesWithCek !== 'function') throw errorCon('opening bytes needs @dotrino/identity >= 0.117.0', 'no-bytes-sealing')
|
|
207
|
+
return decryptBytesWithCek({ cek, iv: env.iv, ct: env.ct })
|
|
208
|
+
}
|
|
@@ -351,6 +351,8 @@ export class WebRTCManager {
|
|
|
351
351
|
|
|
352
352
|
_attachDC (peer, dc) {
|
|
353
353
|
peer.dc = dc
|
|
354
|
+
// Los bytes como ArrayBuffer: un Blob se lee de forma asíncrona y desordenaría las partes.
|
|
355
|
+
try { dc.binaryType = 'arraybuffer' } catch (_) {}
|
|
354
356
|
dc.onopen = () => {
|
|
355
357
|
// Por dónde salió: directo o por TURN. Por detrás, sin retrasar a nadie.
|
|
356
358
|
this.probeRoute(peer).catch(() => {})
|
|
@@ -367,10 +369,13 @@ export class WebRTCManager {
|
|
|
367
369
|
}
|
|
368
370
|
dc.onmessage = (ev) => {
|
|
369
371
|
let parsed = null
|
|
370
|
-
|
|
372
|
+
let raw = ev.data
|
|
371
373
|
this.count?.('in', peer.remote, utf8Length(raw), peer.route || 'webrtc')
|
|
372
374
|
if (typeof raw === 'string') {
|
|
373
375
|
try { parsed = JSON.parse(raw) } catch (_) { parsed = null }
|
|
376
|
+
} else if (raw instanceof ArrayBuffer) {
|
|
377
|
+
// Un mensaje con bytes (binpack.js). En Node llega ya como Buffer, que es un Uint8Array.
|
|
378
|
+
raw = new Uint8Array(raw)
|
|
374
379
|
}
|
|
375
380
|
this.deliverMessage(peer.remote, parsed ?? raw, { raw, timestamp: Date.now(), via: 'webrtc' })
|
|
376
381
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/roadmap@0.4.
|
|
1
|
+
Copia vendorizada de @dotrino/roadmap@0.4.18 (dotrino-roadmap/src/{index,manifests}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
3
|
El registro de versiones rotas que viaja con cada versión. Importa @dotrino/compat/ranges.
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const MANIFESTS = Object.freeze({
|
|
9
9
|
"dotrino": {
|
|
10
10
|
"manifest": "dotrino",
|
|
11
|
-
"seq":
|
|
11
|
+
"seq": 28,
|
|
12
12
|
"issued": "2026-10-15",
|
|
13
13
|
"note": "Registro comun de compatibilidad del ecosistema. Se edita a mano en este repo y se consulta con @dotrino/roadmap. Rangos: ver @dotrino/compat/ranges (x.y.z, >= > <= <, «a - b», «*», o una lista que es un O).",
|
|
14
14
|
"products": {
|
|
@@ -31,7 +31,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
31
31
|
"identity": {
|
|
32
32
|
"repo": "imdotrino/dotrino-identity",
|
|
33
33
|
"npm": "@dotrino/identity",
|
|
34
|
-
"current": "0.
|
|
34
|
+
"current": "0.117.1",
|
|
35
35
|
"protocol": 1,
|
|
36
36
|
"requires": {}
|
|
37
37
|
},
|
|
@@ -47,13 +47,13 @@ export const MANIFESTS = Object.freeze({
|
|
|
47
47
|
},
|
|
48
48
|
"messenger": {
|
|
49
49
|
"repo": "imdotrino/dotrino-messenger",
|
|
50
|
-
"current": "0.
|
|
50
|
+
"current": "0.8.0",
|
|
51
51
|
"protocol": 2,
|
|
52
52
|
"requires": {
|
|
53
|
-
"identity": ">=0.
|
|
54
|
-
"proxy-client": ">=0.
|
|
53
|
+
"identity": ">=0.117.0",
|
|
54
|
+
"proxy-client": ">=0.35.0",
|
|
55
55
|
"store": ">=0.12.3",
|
|
56
|
-
"proxy": "1.
|
|
56
|
+
"proxy": "1.6.1+"
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"messenger-android": {
|
|
@@ -107,7 +107,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
107
107
|
},
|
|
108
108
|
"proxy": {
|
|
109
109
|
"repo": "imdotrino/dotrino-proxy",
|
|
110
|
-
"current": "1.
|
|
110
|
+
"current": "1.6.1",
|
|
111
111
|
"protocol": 2,
|
|
112
112
|
"requires": {
|
|
113
113
|
"vault": ">=0.86.0",
|
|
@@ -119,7 +119,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
119
119
|
"proxy-client": {
|
|
120
120
|
"repo": "imdotrino/dotrino-proxy-client",
|
|
121
121
|
"npm": "@dotrino/proxy-client",
|
|
122
|
-
"current": "0.
|
|
122
|
+
"current": "0.35.0",
|
|
123
123
|
"protocol": 2,
|
|
124
124
|
"requires": {
|
|
125
125
|
"identity": ">=0.53.0",
|
|
@@ -796,6 +796,12 @@ export const MANIFESTS = Object.freeze({
|
|
|
796
796
|
"versions": "1.9.0",
|
|
797
797
|
"why": "no tiene UI propia: sus tres superficies son iframes a messenger.dotrino.com con ?embed=, y el soporte de ese modo (puente de identidad, espejo de conversaciones, cola de salida, aviso de DM) se quitó del messenger después de la 0.1.0; la PWA servida siempre es la última, así que el panel se inventa una cuenta en cada sitio y no recibe nada",
|
|
798
798
|
"fix": "no hay versión que funcione: hay que elegir el camino del README de dotrino-messenger-extension (extensión autónoma con los pilares, o un modo embed del ecosistema)"
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
"product": "proxy",
|
|
802
|
+
"versions": "1.6.0",
|
|
803
|
+
"why": "pasa a base64 los mensajes binarios para cruzar la malla, y una parte de 768 KB así pasa de la trama de 1 MB del otro nodo: el enlace se cae en cada reconexión al reenviarla y todo lo que va detrás entre los dos nodos se queda atascado",
|
|
804
|
+
"fix": "websocket-proxy 1.6.1"
|
|
799
805
|
}
|
|
800
806
|
]
|
|
801
807
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.86.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.86.1 (dotrino-vault/lib/src/{admin,b64,browser,core/approvals,core/blocked,core/browserDisk,core/commKey,core/disk,core/libVersion,core/machines,core/notices,core/notifiable,core/passwords,core/sealKey,core/sealer,core/sealers,core/secretsStore,core/sha256,core/store,core/subacta,core/threadStore,core/transport,core/vault,enroll,index,loginClient,passwordLogins,procContext,protocol,revocation,sealed}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
3
|
browser.js es la bóveda de esta pestaña: el MISMO núcleo que el binario (core/), con
|
|
4
4
|
el disco en IndexedDB. Importa @dotrino/{compat,roadmap,passmanager,store/core} y
|