@dotrino/identity 0.119.0 → 0.120.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.md +16 -0
- package/package.json +4 -1
- package/vault/acta.js +3 -3
- package/vault/channel.js +77 -0
- package/vault/core.js +2 -2
- package/vault/keyid.js +3 -4
- package/vault/remote.js +2 -2
- package/vault/vault.js +2 -2
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +23 -38
- package/vault/vendor/proxy-client/encpub.js +12 -22
- package/vault/vendor/proxy-client/errors.js +15 -0
- package/vault/vendor/proxy-client/sealing.js +7 -12
- package/vault/vendor/roadmap/VERSION.txt +1 -1
- package/vault/vendor/roadmap/manifests.js +15 -13
- package/vault/vendor/store/VERSION.txt +1 -1
- package/vault/vendor/vault/VERSION.txt +1 -1
- package/vault/vendor/vault/core/libVersion.js +1 -1
- package/vault/vendor/vault/sealed.js +7 -36
package/README.md
CHANGED
|
@@ -246,6 +246,22 @@ Lo que hace que esto no sea una llave maestra de repuesto:
|
|
|
246
246
|
Diseño y hacia dónde va (inicio de sesión y federación):
|
|
247
247
|
[`dotrino-vault/docs/inicio-de-sesion.md`](../dotrino-vault/docs/inicio-de-sesion.md).
|
|
248
248
|
|
|
249
|
+
### Canal de sesión de los agentes remotos (0.120.0+)
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
import { makeEphemeral, deriveKey, seal, open } from '@dotrino/identity/channel'
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
ECDH P-256 efímero → HKDF → AES-256-GCM: las dos puntas acuerdan una llave de sesión y la
|
|
256
|
+
usan para muchos mensajes. Lo usa `@dotrino/remote-agent` (antes vivía allí). Para un sobre
|
|
257
|
+
suelto hacia una llave, `wrapForMember`/`openWrap` de `@dotrino/identity/content`.
|
|
258
|
+
|
|
259
|
+
### Huella de una llave
|
|
260
|
+
|
|
261
|
+
`keyLabel(pub)` de `@dotrino/identity/keyid` da `AB12-CD34`, la que se enseña en todo el
|
|
262
|
+
ecosistema. Úsala en vez de recortar `pubkeyId` a mano. Desde 0.120.0 una llave ilegible
|
|
263
|
+
LANZA (antes devolvía `''`); sin llave sigue siendo `''`.
|
|
264
|
+
|
|
249
265
|
### Backup / migración
|
|
250
266
|
|
|
251
267
|
- `id.exportIdentity()` → blob JSON con `privateJwk` (ECDSA), `encPrivateJwk` (ECDH), `me`, `peers`. **Sensible** — el host app es responsable de guardarlo de manera segura.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/identity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.120.0",
|
|
4
4
|
"description": "Identidad y rating de usuarios compartidos entre apps de Dotrino (vault iframe + postMessage)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
"./content": {
|
|
35
35
|
"import": "./vault/content.js"
|
|
36
36
|
},
|
|
37
|
+
"./channel": {
|
|
38
|
+
"import": "./vault/channel.js"
|
|
39
|
+
},
|
|
37
40
|
"./assertion": {
|
|
38
41
|
"import": "./vault/assertion.js"
|
|
39
42
|
},
|
package/vault/acta.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* Módulo PURO: sin kv, sin red, sin disco. Cripto de `./capabilities.js`.
|
|
36
36
|
*/
|
|
37
37
|
import { canonicalStringify } from './core.js'
|
|
38
|
-
import { signWithDevice, verifyDeviceSig, verifyDelegation,
|
|
38
|
+
import { signWithDevice, verifyDeviceSig, verifyDelegation, keyLabel } from './capabilities.js'
|
|
39
39
|
import { cleanMachine } from './machine.js'
|
|
40
40
|
export { cleanMachine }
|
|
41
41
|
|
|
@@ -368,8 +368,8 @@ export async function verifySealerLinkChain (chain, { expectedProfileId = null }
|
|
|
368
368
|
|
|
369
369
|
/** Id legible de un miembro (mismo formato que el deviceId del emparejamiento). */
|
|
370
370
|
export async function memberId (pub) {
|
|
371
|
-
|
|
372
|
-
return
|
|
371
|
+
if (!pub) throw new Error('memberId: missing key')
|
|
372
|
+
return keyLabel(pub)
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
/** ¿Esta acta es un traspaso? (la firmó uno y nombra master a otro) */
|
package/vault/channel.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* channel.js — canal cifrado punto a punto entre el dispositivo enlazado y un agente
|
|
3
|
+
* remoto del ecosistema Dotrino (lo usa `@dotrino/remote-agent`; antes vivía allí). Isomórfico: usa WebCrypto
|
|
4
|
+
* (`globalThis.crypto.subtle`), disponible tanto en el navegador (PWA) como en
|
|
5
|
+
* Node ≥ 18.
|
|
6
|
+
*
|
|
7
|
+
* NO reimplementa la identidad ni la firma del ecosistema: la AUTORIZACIÓN la da
|
|
8
|
+
* el vault (cert de dispositivo verificado con `@dotrino/identity/capabilities`
|
|
9
|
+
* → cadena hasta la maestra). Este módulo solo levanta un secreto de sesión
|
|
10
|
+
* efímero (ECDH P-256 → HKDF → AES-256-GCM) para que el relay de transporte
|
|
11
|
+
* (`proxy.dotrino.com`) vea únicamente texto cifrado. La identidad de las dos
|
|
12
|
+
* puntas ya la garantizan las firmas del vault que envuelven las claves efímeras.
|
|
13
|
+
*
|
|
14
|
+
* Distinto de `content.js` a propósito: aquí la llave es de SESIÓN (se acuerda una vez
|
|
15
|
+
* y cifra muchos mensajes, con HKDF y la pública en raw), allí cada sobre lleva su
|
|
16
|
+
* efímera. El `info` de HKDF conserva su nombre: es parte del cable, y lo hablan también
|
|
17
|
+
* los puertos nativos de `dotrino-native`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const subtle = () => globalThis.crypto.subtle
|
|
21
|
+
|
|
22
|
+
const b64 = {
|
|
23
|
+
enc (buf) {
|
|
24
|
+
const b = new Uint8Array(buf)
|
|
25
|
+
let s = ''
|
|
26
|
+
for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i])
|
|
27
|
+
return btoa(s)
|
|
28
|
+
},
|
|
29
|
+
dec (str) {
|
|
30
|
+
const bin = atob(str)
|
|
31
|
+
const out = new Uint8Array(bin.length)
|
|
32
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Genera un par ECDH P-256 efímero. Devuelve la pub en raw (base64) + privada. */
|
|
38
|
+
export async function makeEphemeral () {
|
|
39
|
+
const kp = await subtle().generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits'])
|
|
40
|
+
const raw = await subtle().exportKey('raw', kp.publicKey)
|
|
41
|
+
return { privateKey: kp.privateKey, pub: b64.enc(raw) }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Deriva la clave AES-256-GCM de la sesión a partir de la privada propia y la
|
|
46
|
+
* pub efímera (base64 raw) del otro extremo. `salt` liga la clave a la sesión.
|
|
47
|
+
*/
|
|
48
|
+
export async function deriveKey (privateKey, otherPubB64, salt) {
|
|
49
|
+
const otherPub = await subtle().importKey(
|
|
50
|
+
'raw', b64.dec(otherPubB64), { name: 'ECDH', namedCurve: 'P-256' }, false, []
|
|
51
|
+
)
|
|
52
|
+
const bits = await subtle().deriveBits({ name: 'ECDH', public: otherPub }, privateKey, 256)
|
|
53
|
+
const hkdfKey = await subtle().importKey('raw', bits, 'HKDF', false, ['deriveKey'])
|
|
54
|
+
return subtle().deriveKey(
|
|
55
|
+
{ name: 'HKDF', hash: 'SHA-256', salt: new TextEncoder().encode(salt), info: new TextEncoder().encode('dotrino-remote-agent-e2e') },
|
|
56
|
+
hkdfKey,
|
|
57
|
+
{ name: 'AES-GCM', length: 256 },
|
|
58
|
+
false,
|
|
59
|
+
['encrypt', 'decrypt']
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Cifra un objeto JSON → sobre { iv, ct } (ambos base64). */
|
|
64
|
+
export async function seal (key, obj) {
|
|
65
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))
|
|
66
|
+
const pt = new TextEncoder().encode(JSON.stringify(obj))
|
|
67
|
+
const ct = await subtle().encrypt({ name: 'AES-GCM', iv }, key, pt)
|
|
68
|
+
return { iv: b64.enc(iv), ct: b64.enc(ct) }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Descifra un sobre { iv, ct } → objeto JSON. Lanza si el tag GCM no valida. */
|
|
72
|
+
export async function open (key, env) {
|
|
73
|
+
const pt = await subtle().decrypt({ name: 'AES-GCM', iv: b64.dec(env.iv) }, key, b64.dec(env.ct))
|
|
74
|
+
return JSON.parse(new TextDecoder().decode(pt))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { b64 }
|
package/vault/core.js
CHANGED
|
@@ -23,7 +23,7 @@ import * as Acta from './acta.js'
|
|
|
23
23
|
import * as Content from './content.js'
|
|
24
24
|
import { syncPeers, stampOf } from './peerSync.js'
|
|
25
25
|
import { assertionBody, cleanScopes, claimsAllowed, ASSERTION_DEFAULT_TTL_MS, ASSERTION_MAX_TTL_MS } from './assertion.js'
|
|
26
|
-
import { pubkeyId as pubkeyIdOf, signWithDevice } from './capabilities.js'
|
|
26
|
+
import { pubkeyId as pubkeyIdOf, signWithDevice, keyLabel } from './capabilities.js'
|
|
27
27
|
import { enrollDevice as remoteEnroll, requestSign as remoteSign, requestStore as remoteStore, requestDevices as remoteDevices, requestRenew as remoteRenew, requestAdmin as remoteAdmin, requestApproval as remoteApproval, requestRenounce as remoteRenounce, checkMembership as remoteCheck, registerLocalVault } from './remote.js'
|
|
28
28
|
|
|
29
29
|
export const KEY_STORAGE = 'dotrino.identity.keypair'
|
|
@@ -1128,7 +1128,7 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
1128
1128
|
} catch (_) { return null }
|
|
1129
1129
|
}
|
|
1130
1130
|
|
|
1131
|
-
const deviceIdDe =
|
|
1131
|
+
const deviceIdDe = keyLabel
|
|
1132
1132
|
|
|
1133
1133
|
/**
|
|
1134
1134
|
* @param {object} entrada lo que devuelve `loginWithPassword` del pilar
|
package/vault/keyid.js
CHANGED
|
@@ -32,10 +32,9 @@ export async function pubkeyId (publicJwkStr) {
|
|
|
32
32
|
*/
|
|
33
33
|
export async function keyLabel (publicJwkStr) {
|
|
34
34
|
if (!publicJwkStr) return ''
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
} catch (_) { return '' }
|
|
35
|
+
// Una llave que no se puede leer LANZA: un '' callado se pintaba como «sin aparato».
|
|
36
|
+
const id = (await pubkeyId(publicJwkStr)).slice(0, 8).toUpperCase()
|
|
37
|
+
return id.slice(0, 4) + '-' + id.slice(4, 8)
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
export default { pubkeyId, keyLabel }
|
package/vault/remote.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* No reimplementa cripto: usa `@dotrino/identity/capabilities`. Transporte:
|
|
14
14
|
* `@dotrino/proxy-client` (importado perezosamente; solo se carga al emparejar).
|
|
15
15
|
*/
|
|
16
|
-
import { makeDeviceKey, signWithDevice, verifyDeviceSig, makePairingCode, commitCode,
|
|
16
|
+
import { makeDeviceKey, signWithDevice, verifyDeviceSig, makePairingCode, commitCode, keyLabel } from './capabilities.js'
|
|
17
17
|
import { checkVaultReply } from './acta.js'
|
|
18
18
|
import { cleanMachine, browserMachine } from './machine.js'
|
|
19
19
|
|
|
@@ -146,7 +146,7 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', conti
|
|
|
146
146
|
// identidad (P) como `device` → el cert delega tu identidad y hay UNA sola (signData/
|
|
147
147
|
// identify/cert son la misma P).
|
|
148
148
|
const dev = device || await makeDeviceKey({ label })
|
|
149
|
-
const deviceId =
|
|
149
|
+
const deviceId = await keyLabel(dev.publickey)
|
|
150
150
|
// El DISPOSITIVO genera el código y manda solo su COMPROMISO (no el código). El vault
|
|
151
151
|
// aprende el código únicamente cuando vos lo tipeás en el PC → aprobar exige tener el dispositivo.
|
|
152
152
|
const code = makePairingCode()
|
package/vault/vault.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
initPeerStorage, loadPeers, savePeers, setPeersDirect, upsertPeer, onDirty
|
|
14
14
|
} from './peerStore.js'
|
|
15
15
|
import { createIdentityCore } from './core.js'
|
|
16
|
-
import {
|
|
16
|
+
import { keyLabel } from './capabilities.js'
|
|
17
17
|
import { withExternalKeys, appBridge } from './externalKeys.js'
|
|
18
18
|
import { nativeBackends } from './nativeStore.js'
|
|
19
19
|
import { useBackend as usePeerBackend } from './peerStore.js'
|
|
@@ -320,7 +320,7 @@ import { useBackend as usePeerBackend } from './peerStore.js'
|
|
|
320
320
|
if (!bySub.has(x.sub) || (x.exp || 0) > (bySub.get(x.sub).exp || 0)) bySub.set(x.sub, x)
|
|
321
321
|
}
|
|
322
322
|
const devices = await Promise.all([...bySub.values()].map(async (x) => ({
|
|
323
|
-
deviceId:
|
|
323
|
+
deviceId: await keyLabel(x.sub),
|
|
324
324
|
sub: x.sub, label: x.label || '', scope: x.scope, exp: x.exp, nonce: x.nonce
|
|
325
325
|
})))
|
|
326
326
|
return { devices, revoked: (revoked || []).map((r) => r.nonce || r) }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/proxy-client@0.36.
|
|
1
|
+
Copia vendorizada de @dotrino/proxy-client@0.36.2 (dotrino-proxy-client/src/{binpack,canonical,client,encpub,errors,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.
|
|
@@ -5,17 +5,8 @@ import { WebRTCManager, RTC_TAG, DEFAULT_ICE_SERVERS, loadNodePeerConnection, re
|
|
|
5
5
|
import { TrafficStats, utf8Length, registerTransport, unregisterTransport } from './stats.js'
|
|
6
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
7
|
import { pack, unpack, hasBinary, encodeFrame, decodeFrame } from './binpack.js'
|
|
8
|
+
import { errorWithCode } from './errors.js'
|
|
8
9
|
|
|
9
|
-
/**
|
|
10
|
-
* Error con un `code` estable.
|
|
11
|
-
*
|
|
12
|
-
* El contrato es el CODE, no la frase: comprobar por el texto cruza procesos y
|
|
13
|
-
* se rompe en silencio en cuanto alguien lo reescribe o lo traduce.
|
|
14
|
-
*
|
|
15
|
-
* @param {string} mensaje
|
|
16
|
-
* @param {string} code
|
|
17
|
-
* @returns {Error & { code: string }}
|
|
18
|
-
*/
|
|
19
10
|
/**
|
|
20
11
|
* EL SALUDO: «este token es de esta identidad».
|
|
21
12
|
*
|
|
@@ -32,12 +23,6 @@ const HELLO_TAG = '__cc_hello__'
|
|
|
32
23
|
*/
|
|
33
24
|
const SPEAKS = ['binary']
|
|
34
25
|
|
|
35
|
-
function errorCon (mensaje, code) {
|
|
36
|
-
const e = /** @type {Error & { code: string }} */ (new Error(mensaje))
|
|
37
|
-
e.code = code
|
|
38
|
-
return e
|
|
39
|
-
}
|
|
40
|
-
|
|
41
26
|
/**
|
|
42
27
|
* Dotrino WebSocket proxy client.
|
|
43
28
|
* Minimal API: connection + token + messages + channels (publish/list/count/disconnect)
|
|
@@ -280,7 +265,7 @@ export class WebSocketProxyClient {
|
|
|
280
265
|
// las peticiones pendientes). Sin esto, quien esperaba un `publish` se comía
|
|
281
266
|
// los 10 s completos del timeout para enterarse de algo que ya se sabía, y
|
|
282
267
|
// el temporizador mantenía vivo el proceso en Node ese rato.
|
|
283
|
-
this._rejectAllPending(
|
|
268
|
+
this._rejectAllPending(errorWithCode('Connection closed', 'CONNECTION_CLOSED'))
|
|
284
269
|
}
|
|
285
270
|
|
|
286
271
|
/** Corta en seco todo lo que esperaba respuesta, con su motivo. */
|
|
@@ -351,7 +336,7 @@ export class WebSocketProxyClient {
|
|
|
351
336
|
// abierto es no guardar ninguno: la app manda por donde le sale y el proxio lo lee
|
|
352
337
|
// igual. Para sellar por token está `sendSealedTo`.
|
|
353
338
|
if (this.requireSealed && !this._isSealed(payload)) {
|
|
354
|
-
throw
|
|
339
|
+
throw errorWithCode(
|
|
355
340
|
'requireSealed: refusing to send a directed message in the clear — use sendSealedTo()',
|
|
356
341
|
'unsealed')
|
|
357
342
|
}
|
|
@@ -527,7 +512,7 @@ export class WebSocketProxyClient {
|
|
|
527
512
|
const m = this._measure(payload)
|
|
528
513
|
const queueMax = m.bin ? QUEUE_MAX_BYTES : QUEUE_MAX_CHARS
|
|
529
514
|
if (m.len > queueMax) {
|
|
530
|
-
throw
|
|
515
|
+
throw errorWithCode(`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')
|
|
531
516
|
}
|
|
532
517
|
// Por pubkey todo va por el proxio: si no admite bytes, se dice antes de mandar nada. Y
|
|
533
518
|
// si alguno de los destinatarios dijo que no los entiende, tampoco.
|
|
@@ -576,7 +561,7 @@ export class WebSocketProxyClient {
|
|
|
576
561
|
// solo sobre solo lo abriría una—, así que ahí se exige decirlo.
|
|
577
562
|
if (!peerPubkey && tokens.length === 1) peerPubkey = this.pubkeyOfToken(tokens[0])
|
|
578
563
|
if (!peerPubkey) {
|
|
579
|
-
throw
|
|
564
|
+
throw errorWithCode(
|
|
580
565
|
'sendSealedTo: nobody has said whose this token is — greet it (helloTo) or pass peerPubkey',
|
|
581
566
|
'no-peer-identity')
|
|
582
567
|
}
|
|
@@ -607,7 +592,7 @@ export class WebSocketProxyClient {
|
|
|
607
592
|
// directo. Se pide el canal para que el siguiente intento lo encuentre abierto.
|
|
608
593
|
if (!direct && m.len > relayMax) {
|
|
609
594
|
this._upgradeDirect(tokens)
|
|
610
|
-
throw
|
|
595
|
+
throw errorWithCode(`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')
|
|
611
596
|
}
|
|
612
597
|
// Si el token muere a mitad, lo que quede se iría a la cola por pubkey; solo se permite
|
|
613
598
|
// si el mensaje entero cabe ahí. Si no, se dice (`too-big-for-queue`), no se deja.
|
|
@@ -650,7 +635,7 @@ export class WebSocketProxyClient {
|
|
|
650
635
|
async _sendPiece (token, sobre, peerPubkey, queueFallback, relayFallback = true) {
|
|
651
636
|
if (this._rtc && await this._rtc.sendDirect(token, this._wire(sobre))) return
|
|
652
637
|
if (!relayFallback) {
|
|
653
|
-
throw
|
|
638
|
+
throw errorWithCode('the direct channel closed in the middle of a message too big for the proxy', 'direct-lost')
|
|
654
639
|
}
|
|
655
640
|
if (sobre instanceof Uint8Array) this._needBinaryProxy()
|
|
656
641
|
this._sendToTokenOrQueue(token, sobre, peerPubkey, { queueFallback, skipDirect: true })
|
|
@@ -673,7 +658,7 @@ export class WebSocketProxyClient {
|
|
|
673
658
|
*/
|
|
674
659
|
sendToOrQueue (token, payload, { peerPubkey } = /** @type {any} */ ({})) {
|
|
675
660
|
if (!token || !peerPubkey) {
|
|
676
|
-
throw
|
|
661
|
+
throw errorWithCode('sendToOrQueue: needs the token and whose it is (peerPubkey)', 'no-peer-identity')
|
|
677
662
|
}
|
|
678
663
|
this._sendToTokenOrQueue(token, payload, peerPubkey)
|
|
679
664
|
}
|
|
@@ -691,7 +676,7 @@ export class WebSocketProxyClient {
|
|
|
691
676
|
*/
|
|
692
677
|
_sendToTokenOrQueue (token, sobre, peerPubkey, { queueFallback = true, skipDirect = false } = {}) {
|
|
693
678
|
if (this.requireSealed && !this._isSealed(sobre)) {
|
|
694
|
-
throw
|
|
679
|
+
throw errorWithCode('requireSealed: refusing to send a directed message in the clear — use sendSealedTo()', 'unsealed')
|
|
695
680
|
}
|
|
696
681
|
const messageStr = this._wire(sobre)
|
|
697
682
|
if (!skipDirect && this._rtc && this._rtc.trySend(token, messageStr)) return
|
|
@@ -747,7 +732,7 @@ export class WebSocketProxyClient {
|
|
|
747
732
|
*/
|
|
748
733
|
helloTo (to) {
|
|
749
734
|
if (!this.myPublickey) {
|
|
750
|
-
throw
|
|
735
|
+
throw errorWithCode('helloTo: identify first — a greeting with no identity says nothing', 'not-identified')
|
|
751
736
|
}
|
|
752
737
|
const tokens = Array.isArray(to) ? to : [to]
|
|
753
738
|
for (const t of tokens) {
|
|
@@ -789,7 +774,7 @@ export class WebSocketProxyClient {
|
|
|
789
774
|
_needPeerBinary (targets) {
|
|
790
775
|
for (const t of targets) {
|
|
791
776
|
if (t && this.speaks(t, 'binary') === false) {
|
|
792
|
-
throw
|
|
777
|
+
throw errorWithCode('the other side does not take binary messages (it needs @dotrino/proxy-client >= 0.36.0)', 'peer-no-binary')
|
|
793
778
|
}
|
|
794
779
|
}
|
|
795
780
|
}
|
|
@@ -815,7 +800,7 @@ export class WebSocketProxyClient {
|
|
|
815
800
|
|
|
816
801
|
/** Sella con lo que haya: la bóveda de la app (`sealing`) o las primitivas del pilar. */
|
|
817
802
|
async _seal (payload, peerEncPub, peerPubkey) {
|
|
818
|
-
if (!peerEncPub) throw
|
|
803
|
+
if (!peerEncPub) throw errorWithCode('seal: missing peerEncPub', 'unsealed')
|
|
819
804
|
return this.sealing
|
|
820
805
|
? this.sealing.seal(payload, peerEncPub, { publickey: peerPubkey })
|
|
821
806
|
: seal(payload, peerEncPub)
|
|
@@ -856,7 +841,7 @@ export class WebSocketProxyClient {
|
|
|
856
841
|
/** Un mensaje con bytes por el proxio necesita uno que admita tramas binarias (websocket-proxy ≥ 1.6.0). */
|
|
857
842
|
_needBinaryProxy () {
|
|
858
843
|
if (!Array.isArray(this.caps) || !this.caps.includes('binary')) {
|
|
859
|
-
throw
|
|
844
|
+
throw errorWithCode('this proxy does not take binary frames (it needs websocket-proxy >= 1.6.0)', 'no-binary-support')
|
|
860
845
|
}
|
|
861
846
|
}
|
|
862
847
|
|
|
@@ -867,7 +852,7 @@ export class WebSocketProxyClient {
|
|
|
867
852
|
if (!p) {
|
|
868
853
|
p = (async () => {
|
|
869
854
|
const o = this.sealing ? await this.sealing.open(k, meta) : await open(k, this.myEncPrivateKey)
|
|
870
|
-
if (!o || typeof o.cek !== 'string') throw
|
|
855
|
+
if (!o || typeof o.cek !== 'string') throw errorWithCode('the sealed key of a message has no key inside', 'bad-pack')
|
|
871
856
|
return o.cek
|
|
872
857
|
})()
|
|
873
858
|
this._msgKeys.set(id, p)
|
|
@@ -909,7 +894,7 @@ export class WebSocketProxyClient {
|
|
|
909
894
|
*/
|
|
910
895
|
async encPubOf (publickey) {
|
|
911
896
|
if (typeof publickey !== 'string' || !publickey) {
|
|
912
|
-
throw
|
|
897
|
+
throw errorWithCode('encPubOf: missing publickey', 'encpub-shape')
|
|
913
898
|
}
|
|
914
899
|
const cacheada = this._encPubs.get(publickey)
|
|
915
900
|
if (cacheada) return cacheada
|
|
@@ -931,7 +916,7 @@ export class WebSocketProxyClient {
|
|
|
931
916
|
const propia = await this.encPubResolver(publickey)
|
|
932
917
|
if (propia) {
|
|
933
918
|
if (!isEncPub(propia)) {
|
|
934
|
-
throw
|
|
919
|
+
throw errorWithCode('encPubResolver returned something that is not a P-256 public JWK', 'encpub-unverified')
|
|
935
920
|
}
|
|
936
921
|
this._encPubs.set(publickey, propia)
|
|
937
922
|
return propia
|
|
@@ -941,7 +926,7 @@ export class WebSocketProxyClient {
|
|
|
941
926
|
// esperar diez segundos a un timeout. Un proxio que no manda `caps` es de antes de
|
|
942
927
|
// que esto existiera y no se sabe: se pregunta igual, y contesta la pregunta.
|
|
943
928
|
if (Array.isArray(this.caps) && !this.caps.includes('encpub')) {
|
|
944
|
-
throw
|
|
929
|
+
throw errorWithCode(
|
|
945
930
|
`este proxio (${this.url}) no sirve llaves de cifrado: hace falta websocket-proxy >= 1.1.0`,
|
|
946
931
|
'no-encpub-support')
|
|
947
932
|
}
|
|
@@ -950,7 +935,7 @@ export class WebSocketProxyClient {
|
|
|
950
935
|
// la app puede tener el mismo JWK escrito de otra forma.
|
|
951
936
|
const statement = (res.keys || []).find((k) => samePubkey(k?.data?.publickey, publickey))
|
|
952
937
|
if (!statement) {
|
|
953
|
-
throw
|
|
938
|
+
throw errorWithCode('no encryption key announced for that identity — it cannot be sealed to yet', 'no-encpub')
|
|
954
939
|
}
|
|
955
940
|
// AQUÍ ES DONDE EL PROXIO DEJA DE IMPORTAR: la firma se comprueba contra la pubkey a
|
|
956
941
|
// la que vamos a escribir. Si cambió la llave por la suya, esto lanza.
|
|
@@ -1015,7 +1000,7 @@ export class WebSocketProxyClient {
|
|
|
1015
1000
|
}
|
|
1016
1001
|
let opened
|
|
1017
1002
|
try {
|
|
1018
|
-
if (!this._isSealed(env.k)) throw
|
|
1003
|
+
if (!this._isSealed(env.k)) throw errorWithCode('the key of a sealed message is not sealed', 'bad-pack')
|
|
1019
1004
|
opened = unpack(await decryptBytes(await this._openMessageKey(env.k, meta), env))
|
|
1020
1005
|
} catch (e) {
|
|
1021
1006
|
this._emit('error', { type: 'undecipherable', from, error: e })
|
|
@@ -1567,7 +1552,7 @@ export class WebSocketProxyClient {
|
|
|
1567
1552
|
if (ws !== this.ws || this._connected) return
|
|
1568
1553
|
this.ws = null // a partir de acá, los eventos tardíos de `ws` se ignoran
|
|
1569
1554
|
try { ws.close() } catch (_) {}
|
|
1570
|
-
const err =
|
|
1555
|
+
const err = errorWithCode(`could not open the connection to ${this.url} in ${Math.round(this.connectTimeout / 1000)}s`, 'CONNECT_TIMEOUT')
|
|
1571
1556
|
this._emit('error', { type: 'connect_timeout', error: err })
|
|
1572
1557
|
if (this._connectReject) {
|
|
1573
1558
|
this._connectReject(err)
|
|
@@ -1781,7 +1766,7 @@ export class WebSocketProxyClient {
|
|
|
1781
1766
|
|
|
1782
1767
|
_sendRaw (frame) {
|
|
1783
1768
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1784
|
-
throw
|
|
1769
|
+
throw errorWithCode('WebSocket not connected', 'NOT_CONNECTED')
|
|
1785
1770
|
}
|
|
1786
1771
|
if (frame.message instanceof Uint8Array) {
|
|
1787
1772
|
this._needBinaryProxy()
|
|
@@ -1860,7 +1845,7 @@ export class WebSocketProxyClient {
|
|
|
1860
1845
|
const out = { ...frame, id }
|
|
1861
1846
|
const timer = setTimeout(() => {
|
|
1862
1847
|
this._pending.delete(id)
|
|
1863
|
-
reject(
|
|
1848
|
+
reject(errorWithCode(`Timeout waiting for ${expectedType}`, 'REQUEST_TIMEOUT'))
|
|
1864
1849
|
}, 10000)
|
|
1865
1850
|
this._pending.set(id, { resolve, reject, timer, expectedType, channelKey })
|
|
1866
1851
|
try {
|
|
@@ -1891,7 +1876,7 @@ export class WebSocketProxyClient {
|
|
|
1891
1876
|
this._pending.delete(id)
|
|
1892
1877
|
// Con su `code`: quien llama distingue los casos por él, no por la frase (que el proxio
|
|
1893
1878
|
// puede cambiar o traducir). Antes se perdía aquí y solo quedaba comparar el texto.
|
|
1894
|
-
const e =
|
|
1879
|
+
const e = errorWithCode(data.error || 'Server error', data.code || null)
|
|
1895
1880
|
// «Ahora no» (429) y cuánto esperar, cuando el proxio lo dice.
|
|
1896
1881
|
if (data.status) e.status = data.status
|
|
1897
1882
|
if (data.retry_after_ms) e.retryAfterMs = data.retry_after_ms
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { canonicalStringify } from './canonical.js'
|
|
30
30
|
import { verifyData, samePubkey } from './signature.js'
|
|
31
|
+
import { errorWithCode } from './errors.js'
|
|
31
32
|
|
|
32
33
|
export const ENCPUB_V = 1
|
|
33
34
|
|
|
@@ -54,17 +55,6 @@ export function isEncPub (v) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/**
|
|
58
|
-
* @param {string} mensaje
|
|
59
|
-
* @param {string} code
|
|
60
|
-
* @returns {Error & { code: string }}
|
|
61
|
-
*/
|
|
62
|
-
function errorCon (mensaje, code) {
|
|
63
|
-
const e = /** @type {Error & { code: string }} */ (new Error(mensaje))
|
|
64
|
-
e.code = code
|
|
65
|
-
return e
|
|
66
|
-
}
|
|
67
|
-
|
|
68
58
|
/**
|
|
69
59
|
* Firma el anuncio con lo que firma la identidad (`identity.signData`, o la llave local
|
|
70
60
|
* del cliente). No genera llaves ni cifra nada: solo dice de quién es la que ya tienes.
|
|
@@ -73,15 +63,15 @@ function errorCon (mensaje, code) {
|
|
|
73
63
|
* @returns {Promise<{data:any, signature:string}>}
|
|
74
64
|
*/
|
|
75
65
|
export async function buildEncPubStatement ({ publickey, encPub, sign } = /** @type {any} */ ({})) {
|
|
76
|
-
if (typeof publickey !== 'string' || !publickey) throw
|
|
77
|
-
if (!isEncPub(encPub)) throw
|
|
78
|
-
if (typeof sign !== 'function') throw
|
|
66
|
+
if (typeof publickey !== 'string' || !publickey) throw errorWithCode('buildEncPubStatement: missing publickey', 'encpub-shape')
|
|
67
|
+
if (!isEncPub(encPub)) throw errorWithCode('buildEncPubStatement: encPub is not a P-256 public JWK', 'encpub-shape')
|
|
68
|
+
if (typeof sign !== 'function') throw errorWithCode('buildEncPubStatement: missing sign(data)', 'encpub-shape')
|
|
79
69
|
const data = encPubBody({ publickey, encPub })
|
|
80
70
|
const firmado = await sign(data)
|
|
81
71
|
const signature = typeof firmado === 'string' ? firmado : firmado?.signature
|
|
82
72
|
// «No pude firmar» no es «se cayó la red», y aquí es donde se separan: quien llama
|
|
83
73
|
// decide una cosa u otra según el `code`, nunca según la frase.
|
|
84
|
-
if (typeof signature !== 'string') throw
|
|
74
|
+
if (typeof signature !== 'string') throw errorWithCode('buildEncPubStatement: sign() returned no signature', 'no-signature')
|
|
85
75
|
return { data, signature }
|
|
86
76
|
}
|
|
87
77
|
|
|
@@ -98,21 +88,21 @@ export async function buildEncPubStatement ({ publickey, encPub, sign } = /** @t
|
|
|
98
88
|
*/
|
|
99
89
|
export async function readEncPubStatement (statement, { publickey } = /** @type {any} */ ({})) {
|
|
100
90
|
if (typeof publickey !== 'string' || !publickey) {
|
|
101
|
-
throw
|
|
91
|
+
throw errorWithCode('readEncPubStatement: missing the publickey to check against', 'encpub-shape')
|
|
102
92
|
}
|
|
103
93
|
const data = statement?.data
|
|
104
94
|
const signature = statement?.signature
|
|
105
|
-
if (!data || typeof signature !== 'string') throw
|
|
106
|
-
if (data.v !== ENCPUB_V || data.op !== 'encpub') throw
|
|
107
|
-
if (data.aud !== ENCPUB_AUD) throw
|
|
95
|
+
if (!data || typeof signature !== 'string') throw errorWithCode('encpub statement: malformed', 'encpub-unverified')
|
|
96
|
+
if (data.v !== ENCPUB_V || data.op !== 'encpub') throw errorWithCode('encpub statement: not an encpub announcement', 'encpub-unverified')
|
|
97
|
+
if (data.aud !== ENCPUB_AUD) throw errorWithCode('encpub statement: wrong audience', 'encpub-unverified')
|
|
108
98
|
// `samePubkey` y no `===`: un JWK serializado no es canónico y la misma llave escrita
|
|
109
99
|
// por dos piezas distintas da dos strings distintos.
|
|
110
|
-
if (!samePubkey(data.publickey, publickey)) throw
|
|
111
|
-
if (!isEncPub(data.encpub)) throw
|
|
100
|
+
if (!samePubkey(data.publickey, publickey)) throw errorWithCode('encpub statement: announces another identity', 'encpub-unverified')
|
|
101
|
+
if (!isEncPub(data.encpub)) throw errorWithCode('encpub statement: not a P-256 public JWK', 'encpub-unverified')
|
|
112
102
|
// Se verifica con la llave que ANUNCIA el sobre, que ya se comprobó que es la misma que
|
|
113
103
|
// la pedida: así el texto firmado y la llave que lo comprueba salen del mismo sitio.
|
|
114
104
|
if (!(await verifyData(data.publickey, data, signature))) {
|
|
115
|
-
throw
|
|
105
|
+
throw errorWithCode('encpub statement: bad signature — the key is not bound to that identity', 'encpub-unverified')
|
|
116
106
|
}
|
|
117
107
|
return data.encpub
|
|
118
108
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error con un `code` estable.
|
|
3
|
+
*
|
|
4
|
+
* El contrato es el CODE, no la frase: comprobar por el texto cruza procesos y
|
|
5
|
+
* se rompe en silencio en cuanto alguien lo reescribe o lo traduce.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} message
|
|
8
|
+
* @param {string} code
|
|
9
|
+
* @returns {Error & { code: string }}
|
|
10
|
+
*/
|
|
11
|
+
export function errorWithCode (message, code) {
|
|
12
|
+
const e = /** @type {Error & { code: string }} */ (new Error(message))
|
|
13
|
+
e.code = code
|
|
14
|
+
return e
|
|
15
|
+
}
|
|
@@ -15,14 +15,9 @@
|
|
|
15
15
|
* second, older copy of a pillar inside every consumer.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
import { errorWithCode } from './errors.js'
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
function errorCon (mensaje, code) {
|
|
22
|
-
const e = /** @type {Error & { code: string }} */ (new Error(mensaje))
|
|
23
|
-
e.code = code
|
|
24
|
-
return e
|
|
25
|
-
}
|
|
20
|
+
const ECDH = { name: 'ECDH', namedCurve: 'P-256' }
|
|
26
21
|
|
|
27
22
|
const VERSION = 1
|
|
28
23
|
|
|
@@ -137,7 +132,7 @@ export function identitySealing (identity, { app = 'dotrino' } = {}) {
|
|
|
137
132
|
|
|
138
133
|
return {
|
|
139
134
|
async seal (msg, peerEncPub, { publickey } = {}) {
|
|
140
|
-
if (!peerEncPub) throw
|
|
135
|
+
if (!peerEncPub) throw errorWithCode('no encryption key for the other side', 'unsealed')
|
|
141
136
|
// Destinatarios como OBJETOS, y CON SU `publickey`: `encrypt` expande cada uno a
|
|
142
137
|
// todos los aparatos de esa persona por su tarjeta de perfil, y la tarjeta se busca
|
|
143
138
|
// por la pubkey. Sin ella el sobre solo lo abría el aparato que anunció la llave, y
|
|
@@ -149,7 +144,7 @@ export function identitySealing (identity, { app = 'dotrino' } = {}) {
|
|
|
149
144
|
// VACÍO: cifrado de verdad, y que no abre nadie. Eso no es un sobre, es un mensaje
|
|
150
145
|
// perdido con cara de enviado.
|
|
151
146
|
if (!sealed || !sealed.wrap || Object.keys(sealed.wrap).length === 0) {
|
|
152
|
-
throw
|
|
147
|
+
throw errorWithCode('identitySealing: the vault wrapped the message for nobody', 'unsealed')
|
|
153
148
|
}
|
|
154
149
|
return { app, sealed, from: await myEncPub() }
|
|
155
150
|
},
|
|
@@ -189,20 +184,20 @@ export function isBinSealed (env) {
|
|
|
189
184
|
/** Una CEK nueva para un mensaje con bytes. */
|
|
190
185
|
export async function makeMessageKey () {
|
|
191
186
|
const { makeContentKey } = await crypto_()
|
|
192
|
-
if (typeof makeContentKey !== 'function') throw
|
|
187
|
+
if (typeof makeContentKey !== 'function') throw errorWithCode('sealing bytes needs @dotrino/identity >= 0.117.0', 'no-bytes-sealing')
|
|
193
188
|
return makeContentKey()
|
|
194
189
|
}
|
|
195
190
|
|
|
196
191
|
/** Cifra `bytes` con la CEK y devuelve los campos del sobre (`iv`, `ct`). */
|
|
197
192
|
export async function encryptBytes (cek, bytes) {
|
|
198
193
|
const { encryptBytesWithCek } = await crypto_()
|
|
199
|
-
if (typeof encryptBytesWithCek !== 'function') throw
|
|
194
|
+
if (typeof encryptBytesWithCek !== 'function') throw errorWithCode('sealing bytes needs @dotrino/identity >= 0.117.0', 'no-bytes-sealing')
|
|
200
195
|
return encryptBytesWithCek({ cek, bytes })
|
|
201
196
|
}
|
|
202
197
|
|
|
203
198
|
/** Descifra el `ct` de un sobre binario con su CEK ya abierta. */
|
|
204
199
|
export async function decryptBytes (cek, env) {
|
|
205
200
|
const { decryptBytesWithCek } = await crypto_()
|
|
206
|
-
if (typeof decryptBytesWithCek !== 'function') throw
|
|
201
|
+
if (typeof decryptBytesWithCek !== 'function') throw errorWithCode('opening bytes needs @dotrino/identity >= 0.117.0', 'no-bytes-sealing')
|
|
207
202
|
return decryptBytesWithCek({ cek, iv: env.iv, ct: env.ct })
|
|
208
203
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/roadmap@0.4.
|
|
1
|
+
Copia vendorizada de @dotrino/roadmap@0.4.30 (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,8 +8,8 @@
|
|
|
8
8
|
export const MANIFESTS = Object.freeze({
|
|
9
9
|
"dotrino": {
|
|
10
10
|
"manifest": "dotrino",
|
|
11
|
-
"seq":
|
|
12
|
-
"issued": "2026-10-
|
|
11
|
+
"seq": 30,
|
|
12
|
+
"issued": "2026-10-16",
|
|
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": {
|
|
15
15
|
"compat": {
|
|
@@ -22,18 +22,20 @@ export const MANIFESTS = Object.freeze({
|
|
|
22
22
|
"env": {
|
|
23
23
|
"repo": "imdotrino/dotrino-vault",
|
|
24
24
|
"npm": "@dotrino/env",
|
|
25
|
-
"current": "0.
|
|
25
|
+
"current": "0.90.1",
|
|
26
26
|
"protocol": 1,
|
|
27
27
|
"requires": {
|
|
28
|
-
"vault": ">=0.
|
|
28
|
+
"vault": ">=0.90.1"
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"identity": {
|
|
32
32
|
"repo": "imdotrino/dotrino-identity",
|
|
33
33
|
"npm": "@dotrino/identity",
|
|
34
|
-
"current": "0.
|
|
34
|
+
"current": "0.119.0",
|
|
35
35
|
"protocol": 1,
|
|
36
|
-
"requires": {
|
|
36
|
+
"requires": {
|
|
37
|
+
"proxy-client": ">=0.13.1"
|
|
38
|
+
}
|
|
37
39
|
},
|
|
38
40
|
"lobby": {
|
|
39
41
|
"repo": "imdotrino/dotrino-lobby",
|
|
@@ -107,7 +109,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
107
109
|
},
|
|
108
110
|
"proxy": {
|
|
109
111
|
"repo": "imdotrino/dotrino-proxy",
|
|
110
|
-
"current": "1.6.
|
|
112
|
+
"current": "1.6.4",
|
|
111
113
|
"protocol": 2,
|
|
112
114
|
"requires": {
|
|
113
115
|
"vault": ">=0.87.0",
|
|
@@ -215,7 +217,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
215
217
|
"roadmap": {
|
|
216
218
|
"repo": "imdotrino/dotrino-roadmap",
|
|
217
219
|
"npm": "@dotrino/roadmap",
|
|
218
|
-
"current": "0.4.
|
|
220
|
+
"current": "0.4.30",
|
|
219
221
|
"protocol": 1,
|
|
220
222
|
"requires": {
|
|
221
223
|
"compat": "0.4.0+"
|
|
@@ -251,7 +253,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
251
253
|
"vault": {
|
|
252
254
|
"repo": "imdotrino/dotrino-vault",
|
|
253
255
|
"npm": "@dotrino/vault",
|
|
254
|
-
"current": "0.
|
|
256
|
+
"current": "0.90.1",
|
|
255
257
|
"protocol": 1,
|
|
256
258
|
"requires": {
|
|
257
259
|
"vaultd": ">=0.105.0",
|
|
@@ -261,13 +263,13 @@ export const MANIFESTS = Object.freeze({
|
|
|
261
263
|
"compat": ">=0.4.0",
|
|
262
264
|
"roadmap": ">=0.4.26",
|
|
263
265
|
"passmanager": ">=0.18.4",
|
|
264
|
-
"store": ">=0.
|
|
266
|
+
"store": ">=0.12.3"
|
|
265
267
|
}
|
|
266
268
|
},
|
|
267
269
|
"vaultd": {
|
|
268
270
|
"repo": "imdotrino/dotrino-vault",
|
|
269
271
|
"npm": "@dotrino/vaultd",
|
|
270
|
-
"current": "0.
|
|
272
|
+
"current": "0.162.1",
|
|
271
273
|
"protocol": 1,
|
|
272
274
|
"requires": {
|
|
273
275
|
"identity": ">=0.118.0",
|
|
@@ -318,12 +320,12 @@ export const MANIFESTS = Object.freeze({
|
|
|
318
320
|
},
|
|
319
321
|
"passmanager-extension": {
|
|
320
322
|
"repo": "imdotrino/dotrino-passmanager",
|
|
321
|
-
"current": "0.
|
|
323
|
+
"current": "0.54.0",
|
|
322
324
|
"protocol": 1,
|
|
323
325
|
"requires": {
|
|
324
326
|
"identity": ">=0.118.0",
|
|
325
327
|
"proxy-client": ">=0.36.1",
|
|
326
|
-
"vault": ">=0.
|
|
328
|
+
"vault": ">=0.90.0",
|
|
327
329
|
"proxy": "1.1.0+",
|
|
328
330
|
"vaultd": ">=0.160.2"
|
|
329
331
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/store/core@0.12.
|
|
1
|
+
Copia vendorizada de @dotrino/store/core@0.12.4 (dotrino-store/store/{core}.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
|
Las reglas del almacén (lápidas, páginas): las mismas en el store y en la bóveda.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.90.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.90.2 (dotrino-vault/lib/src/{admin,b64,browser,core/approvals,core/backup,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
|
|
@@ -16,30 +16,13 @@
|
|
|
16
16
|
* P-256 + AES-GCM, llaves como JWK string.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
function b64 (buf) {
|
|
22
|
-
const bytes = new Uint8Array(buf)
|
|
23
|
-
let s = ''
|
|
24
|
-
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i])
|
|
25
|
-
return btoa(s)
|
|
26
|
-
}
|
|
19
|
+
import { wrapForMember, openWrap } from '@dotrino/identity/content'
|
|
27
20
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)
|
|
32
|
-
return out
|
|
33
|
-
}
|
|
21
|
+
// El sobre es el MISMO que el de las llaves de contenido de identity (ECDH P-256 efímero
|
|
22
|
+
// → AES-256-GCM, `epk` en JWK): aquí solo se le mete un JSON en vez de una CEK. Antes
|
|
23
|
+
// estaba escrito otra vez en este archivo, byte a byte igual.
|
|
34
24
|
|
|
35
|
-
|
|
36
|
-
const pub = await subtle.importKey(
|
|
37
|
-
'jwk', JSON.parse(publicJwkStr),
|
|
38
|
-
{ name: 'ECDH', namedCurve: 'P-256' }, false, []
|
|
39
|
-
)
|
|
40
|
-
const bits = await subtle.deriveBits({ name: 'ECDH', public: pub }, privateKey, 256)
|
|
41
|
-
return subtle.importKey('raw', bits, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt'])
|
|
42
|
-
}
|
|
25
|
+
const subtle = globalThis.crypto.subtle
|
|
43
26
|
|
|
44
27
|
/**
|
|
45
28
|
* Genera la llave ECDH EFÍMERA del solicitante (una por petición).
|
|
@@ -57,17 +40,7 @@ export async function makeEphemeralKey () {
|
|
|
57
40
|
* @returns {Promise<{epk:string, iv:string, ct:string}>}
|
|
58
41
|
*/
|
|
59
42
|
export async function seal ({ ek, payload }) {
|
|
60
|
-
|
|
61
|
-
const key = await deriveAesKey(eph.privateKey, ek)
|
|
62
|
-
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))
|
|
63
|
-
const pt = new TextEncoder().encode(JSON.stringify(payload))
|
|
64
|
-
const ct = await subtle.encrypt({ name: 'AES-GCM', iv }, key, pt)
|
|
65
|
-
const epkJwk = await subtle.exportKey('jwk', eph.publicKey)
|
|
66
|
-
return {
|
|
67
|
-
epk: JSON.stringify({ kty: epkJwk.kty, crv: epkJwk.crv, x: epkJwk.x, y: epkJwk.y }),
|
|
68
|
-
iv: b64(iv),
|
|
69
|
-
ct: b64(ct)
|
|
70
|
-
}
|
|
43
|
+
return wrapForMember({ cek: JSON.stringify(payload), memberEncPub: ek })
|
|
71
44
|
}
|
|
72
45
|
|
|
73
46
|
/**
|
|
@@ -78,7 +51,5 @@ export async function openSealed ({ privateKey, enc }) {
|
|
|
78
51
|
if (!enc || typeof enc.epk !== 'string' || typeof enc.iv !== 'string' || typeof enc.ct !== 'string') {
|
|
79
52
|
throw new Error('invalid sealed envelope')
|
|
80
53
|
}
|
|
81
|
-
|
|
82
|
-
const pt = await subtle.decrypt({ name: 'AES-GCM', iv: fromB64(enc.iv) }, key, fromB64(enc.ct))
|
|
83
|
-
return JSON.parse(new TextDecoder().decode(pt))
|
|
54
|
+
return JSON.parse(await openWrap({ wrap: enc, myEncPrivateKey: privateKey }))
|
|
84
55
|
}
|