@dotrino/identity 0.115.0 → 0.117.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/package.json +4 -1
- package/src/node.js +4 -180
- package/vault/content.js +28 -1
- package/vault/core.js +66 -7
- package/vault/facade.js +200 -0
- package/vault/index.html +10 -0
- package/vault/remote.js +45 -5
- package/vault/vault.js +29 -58
- package/vault/vendor/compat/VERSION.txt +3 -0
- package/vault/vendor/compat/advisory.js +141 -0
- package/vault/vendor/compat/index.js +188 -0
- package/vault/vendor/compat/ranges.js +93 -0
- package/vault/vendor/opaque/VERSION.txt +1 -1
- package/vault/vendor/passmanager/VERSION.txt +3 -0
- package/vault/vendor/passmanager/crypto.js +167 -0
- package/vault/vendor/passmanager/fields.js +183 -0
- package/vault/vendor/passmanager/generate.js +74 -0
- package/vault/vendor/passmanager/import.js +156 -0
- package/vault/vendor/passmanager/index.js +27 -0
- package/vault/vendor/passmanager/match.js +137 -0
- package/vault/vendor/passmanager/model.js +332 -0
- package/vault/vendor/passmanager/pubkey.js +33 -0
- package/vault/vendor/passmanager/sealed/convert.js +72 -0
- package/vault/vendor/passmanager/sealed/device.js +211 -0
- package/vault/vendor/passmanager/sealed/entry.js +209 -0
- package/vault/vendor/passmanager/sealed/index.js +20 -0
- package/vault/vendor/passmanager/sealed/keys.js +120 -0
- package/vault/vendor/passmanager/sealed/recovery.js +136 -0
- package/vault/vendor/passmanager/sealed/store.js +508 -0
- package/vault/vendor/passmanager/session-cache.js +60 -0
- package/vault/vendor/passmanager/totp.js +111 -0
- package/vault/vendor/passmanager/transport/protocol.js +83 -0
- package/vault/vendor/passmanager/transport/proxy.js +112 -0
- package/vault/vendor/passmanager/transport/sealed.js +74 -0
- package/vault/vendor/passmanager/vault/approval.js +63 -0
- package/vault/vendor/passmanager/vault/errors.js +34 -0
- package/vault/vendor/passmanager/vault/guard.js +114 -0
- package/vault/vendor/passmanager/vault/interface.js +34 -0
- package/vault/vendor/passmanager/vault/local.js +213 -0
- package/vault/vendor/passmanager/vault/remote.js +85 -0
- package/vault/vendor/passmanager/vault/responder.js +192 -0
- package/vault/vendor/passmanager/vault/sealed-local.js +199 -0
- package/vault/vendor/passmanager/vault/sealed-responder.js +159 -0
- package/vault/vendor/passmanager/vault/sealed.js +362 -0
- package/vault/vendor/passmanager/webauthn.js +221 -0
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +136 -11
- package/vault/vendor/proxy-client/index.js +1 -0
- package/vault/vendor/proxy-client/parts.js +127 -0
- package/vault/vendor/proxy-client/webrtc.js +23 -0
- package/vault/vendor/roadmap/VERSION.txt +3 -0
- package/vault/vendor/roadmap/index.js +108 -0
- package/vault/vendor/roadmap/manifests.js +802 -0
- package/vault/vendor/store/VERSION.txt +3 -0
- package/vault/vendor/store/core.js +309 -0
- package/vault/vendor/vault/VERSION.txt +4 -1
- package/vault/vendor/vault/admin.js +437 -0
- package/vault/vendor/vault/browser.js +167 -0
- package/vault/vendor/vault/core/approvals.js +204 -0
- package/vault/vendor/vault/core/blocked.js +68 -0
- package/vault/vendor/vault/core/browserDisk.js +109 -0
- package/vault/vendor/vault/core/commKey.js +68 -0
- package/vault/vendor/vault/core/disk.js +65 -0
- package/vault/vendor/vault/core/libVersion.js +8 -0
- package/vault/vendor/vault/core/machines.js +66 -0
- package/vault/vendor/vault/core/notices.js +71 -0
- package/vault/vendor/vault/core/notifiable.js +73 -0
- package/vault/vendor/vault/core/passwords.js +125 -0
- package/vault/vendor/vault/core/sealKey.js +77 -0
- package/vault/vendor/vault/core/sealer.js +172 -0
- package/vault/vendor/vault/core/sealers.js +159 -0
- package/vault/vendor/vault/core/secretsStore.js +1326 -0
- package/vault/vendor/vault/core/sha256.js +69 -0
- package/vault/vendor/vault/core/store.js +68 -0
- package/vault/vendor/vault/core/subacta.js +101 -0
- package/vault/vendor/vault/core/threadStore.js +273 -0
- package/vault/vendor/vault/core/transport.js +270 -0
- package/vault/vendor/vault/core/vault.js +4414 -0
- package/vault/vendor/vault/enroll.js +8 -4
- package/vault/vendor/vault/procContext.js +97 -0
- package/vault/vendor/vault/revocation.js +67 -0
- package/vault/vendor/vault/sealed.js +84 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// LA BÓVEDA SELLADA CUANDO QUIEN GUARDA Y QUIEN LEE SON EL MISMO PROCESO.
|
|
2
|
+
//
|
|
3
|
+
// `SealedVault` es el aparato hablando con una bóveda que está al otro lado de un
|
|
4
|
+
// transporte. Esto es el otro caso, y existe de verdad en dos sitios del ecosistema:
|
|
5
|
+
//
|
|
6
|
+
// · **`dotrino-passmanager serve`** — el mismo proceso atiende por el proxio y es
|
|
7
|
+
// también la línea de comandos con la que el dueño mira lo suyo;
|
|
8
|
+
// · **la bóveda de dentro de la extensión** — no hay nadie más a quien preguntar.
|
|
9
|
+
//
|
|
10
|
+
// En los dos, quien lee lo hace con UNA envoltura: la de `#recovery` (el dueño con su
|
|
11
|
+
// contraseña) o la del propio aparato. Se le dice cuál al construirla y no se adivina:
|
|
12
|
+
// adivinarlo sería leer con la que hubiera, y entonces «no puedo abrir esto» y «esto no es
|
|
13
|
+
// para mí» se verían igual.
|
|
14
|
+
//
|
|
15
|
+
// Cumple el contrato de siempre (`./interface.js`) para que nada de lo que hay encima
|
|
16
|
+
// tenga que aprender otra forma de pedir las cosas.
|
|
17
|
+
|
|
18
|
+
import { VaultError, CODES } from './errors.js'
|
|
19
|
+
import { findForUrl } from '../match.js'
|
|
20
|
+
import { siteIndex, fieldIndex, valueDigest } from '../sealed/keys.js'
|
|
21
|
+
import { buildSealedEntry, openSealedEntry, openSealedView } from '../sealed/device.js'
|
|
22
|
+
import { mergePatch } from '../model.js'
|
|
23
|
+
|
|
24
|
+
/** Los nombres FIJOS, que viajan tal cual: vocabulario cerrado e igual para todos. */
|
|
25
|
+
const FIXED = ['name', 'username', 'secret', 'totp', 'notes']
|
|
26
|
+
|
|
27
|
+
export class SealedLocalVault {
|
|
28
|
+
/**
|
|
29
|
+
* @param {SealedStore} sealed el almacén de sobres
|
|
30
|
+
* @param {object} opts
|
|
31
|
+
* `readerPub` con qué envoltura se lee: `RECOVERY` o la pública de este aparato
|
|
32
|
+
* `openSealed({ wrap, envelope })` abre un sobre. La privada no sale de donde viva.
|
|
33
|
+
* `author` `{ publickey, sign(body) }` quien firma lo que se escribe
|
|
34
|
+
* `recipients()` → `{ recoveryPub, main, passkeys }` a quién se le envuelve
|
|
35
|
+
* `keys()` → las llaves del perfil (`profileKeys`), para huellas y resúmenes
|
|
36
|
+
*/
|
|
37
|
+
constructor (sealed, { readerPub, openSealed, author, recipients, keys } = {}) {
|
|
38
|
+
if (!sealed) throw new Error('SealedLocalVault: missing the sealed store')
|
|
39
|
+
if (!readerPub) throw new Error('SealedLocalVault: say which wrap this one reads with')
|
|
40
|
+
for (const n of ['openSealed', 'author', 'recipients', 'keys']) {
|
|
41
|
+
if (!arguments[1]?.[n]) throw new Error(`SealedLocalVault: missing ${n}`)
|
|
42
|
+
}
|
|
43
|
+
this.sealed = sealed
|
|
44
|
+
this.readerPub = readerPub
|
|
45
|
+
this.openSealed = openSealed
|
|
46
|
+
this.author = author
|
|
47
|
+
this.recipients = recipients
|
|
48
|
+
this.keys = keys
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get capabilities () {
|
|
52
|
+
// `canList` es `true` y aquí sí es verdad: quien pregunta es el dueño en su propia
|
|
53
|
+
// máquina, no un aparato pidiendo por el proxio. Lo que no existe en remoto sigue sin
|
|
54
|
+
// existir — esto no se asoma por ningún transporte.
|
|
55
|
+
return { canWrite: true, canList: true, needsApproval: false, sealed: true }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** De la clave que usa la interfaz a la que guarda la bóveda. La misma que `SealedVault`. */
|
|
59
|
+
async #sealedKey (k) {
|
|
60
|
+
if (FIXED.includes(k) || k.startsWith('wa.')) return k
|
|
61
|
+
const { kidx } = await this.keys()
|
|
62
|
+
return 'f:' + await fieldIndex(kidx, k)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Las vistas, abiertas. Es lo que se enseña en una lista: sin valores privados. */
|
|
66
|
+
async list () {
|
|
67
|
+
const vistas = await this.sealed.views({ pub: this.readerPub })
|
|
68
|
+
const out = []
|
|
69
|
+
for (const v of vistas) {
|
|
70
|
+
const abierta = await openSealedView({ ...v, openSealed: this.openSealed })
|
|
71
|
+
// `fieldHashes` con el nombre que ya usa la interfaz para comparar sin abrir nada.
|
|
72
|
+
// Los resúmenes están GUARDADOS y su clave viaja por huella, así que aquí se
|
|
73
|
+
// traducen de vuelta — la vista sí lleva los nombres de verdad, porque va sellada.
|
|
74
|
+
// Sin esto, «¿ya está guardado igual?» se contesta que no siempre, y el aviso de
|
|
75
|
+
// guardar sale en cada formulario que ya tenías.
|
|
76
|
+
const fieldHashes = {}
|
|
77
|
+
for (const k of [...(abierta.fieldKeys || []), 'notes']) {
|
|
78
|
+
const clave = await this.#sealedKey(k)
|
|
79
|
+
if (abierta.digests?.[clave] !== undefined) fieldHashes[k] = abierta.digests[clave]
|
|
80
|
+
}
|
|
81
|
+
out.push({ ...abierta, fieldHashes })
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* El resumen de un valor, para decir si ya está guardado igual **sin abrir nada** (§2.6).
|
|
88
|
+
* No hay nonce que compartir: el `id` de la entrada va dentro, que es lo que impide que
|
|
89
|
+
* un valor repetido en dos entradas se note.
|
|
90
|
+
*/
|
|
91
|
+
async digest (id, key, value) {
|
|
92
|
+
const { kcmp } = await this.keys()
|
|
93
|
+
return valueDigest(kcmp, id, key, String(value))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Lo que hay para una URL, ya ORDENADO por lo bien que casa: las entradas, no los pares
|
|
98
|
+
* `{entry, match}` de `findForUrl`. Es la misma forma que devuelve `SealedVault.find`,
|
|
99
|
+
* y tiene que serlo — quien llama no debería notar de qué bóveda le están contestando.
|
|
100
|
+
*/
|
|
101
|
+
async find (url) {
|
|
102
|
+
return findForUrl(await this.list(), url).map((x) => x.entry)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* EN QUÉ SITIOS HAY ALGO, y cuánto en cada uno. Misma forma que `LocalVault.sites()`
|
|
107
|
+
* —`[{ site, count }]`— porque es lo que pinta el gestor: devolver una lista de cadenas
|
|
108
|
+
* dejaba la fila de dominios vacía sin un solo error.
|
|
109
|
+
*
|
|
110
|
+
* El sitio vacío es «sirve en cualquier parte» y va al final, que es donde se busca.
|
|
111
|
+
*/
|
|
112
|
+
async sites () {
|
|
113
|
+
const cuenta = new Map()
|
|
114
|
+
for (const e of await this.list()) {
|
|
115
|
+
for (const s of (e.sites?.length ? e.sites : [''])) cuenta.set(s, (cuenta.get(s) || 0) + 1)
|
|
116
|
+
}
|
|
117
|
+
return [...cuenta.entries()]
|
|
118
|
+
.map(([site, count]) => ({ site, count }))
|
|
119
|
+
.sort((a, b) => (a.site === '' ? 1 : b.site === '' ? -1 : a.site.localeCompare(b.site)))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* BUSCAR por texto. Se mira lo mismo que mira `SealedVault`: el nombre, el SITIO y la
|
|
124
|
+
* **pista** —el usuario tapado que la vista lleva—, que es por lo que la gente busca de
|
|
125
|
+
* verdad («ana@…»). Dejarla fuera hacía que buscar la cuenta de otro dominio no
|
|
126
|
+
* encontrara nada, sin un solo error.
|
|
127
|
+
*
|
|
128
|
+
* Se busca sobre las VISTAS, no sobre los valores: aquí no se abre ninguna contraseña
|
|
129
|
+
* para contestar a un buscador.
|
|
130
|
+
*/
|
|
131
|
+
async search (texto, { limit = 20 } = {}) {
|
|
132
|
+
const q = String(texto || '').trim().toLowerCase()
|
|
133
|
+
if (!q) return []
|
|
134
|
+
const out = []
|
|
135
|
+
for (const v of await this.list()) {
|
|
136
|
+
if (out.length >= limit) break
|
|
137
|
+
if ([v.title, v.hint, ...(v.sites || [])].filter(Boolean).join(' ').toLowerCase().includes(q)) out.push(v)
|
|
138
|
+
}
|
|
139
|
+
return out
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Una entrada ENTERA, o solo los campos que se pidan. */
|
|
143
|
+
async get (id, { keys = null } = {}) {
|
|
144
|
+
let got
|
|
145
|
+
try { got = await this.sealed.get(id, { pub: this.readerPub, keys }) }
|
|
146
|
+
catch (e) {
|
|
147
|
+
if (e?.code === 'not-found') throw new VaultError(CODES.NOT_FOUND, 'no hay ninguna entrada con ese id')
|
|
148
|
+
// «no me la envolvieron a mí» no es «no existe», y confundirlas manda a buscar el
|
|
149
|
+
// problema al sitio equivocado.
|
|
150
|
+
if (e?.code === 'not-yours') throw new VaultError(CODES.NOT_ALLOWED, 'esta punta no tiene llave para esa entrada')
|
|
151
|
+
throw e
|
|
152
|
+
}
|
|
153
|
+
return openSealedEntry({ got, openSealed: this.openSealed })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* GUARDAR. Se cierra aquí y sale cerrado: la entrada se cifra, su llave se envuelve a
|
|
158
|
+
* los destinatarios que diga el acta AHORA y el conjunto va firmado.
|
|
159
|
+
*
|
|
160
|
+
* Los destinatarios se piden en cada escritura y no se cachean: un aparato que acaba de
|
|
161
|
+
* entrar tiene que poder leer lo siguiente que se guarde.
|
|
162
|
+
*/
|
|
163
|
+
async put (entry) {
|
|
164
|
+
const sobre = await buildSealedEntry({
|
|
165
|
+
plain: entry,
|
|
166
|
+
keys: await this.keys(),
|
|
167
|
+
recipients: await this.recipients(),
|
|
168
|
+
author: this.author
|
|
169
|
+
})
|
|
170
|
+
await this.sealed.putSealed(sobre)
|
|
171
|
+
// La VISTA, como devuelve `LocalVault.put`: quien llama enseña el nombre de lo que
|
|
172
|
+
// acaba de guardar. Devolver solo el id dejaba un «Guardada: undefined» en la CLI.
|
|
173
|
+
return { id: sobre.entry.id, title: entry.title || '', sites: entry.sites || [] }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Cambiar unos campos. Se hace leyendo y volviendo a escribir porque aquí quien lee y
|
|
178
|
+
* quien escribe son el mismo: no hay nada que ahorrar ocultándose un valor a sí mismo.
|
|
179
|
+
* (En remoto SÍ lo hay, y por eso allí existe `patchSealed`.)
|
|
180
|
+
*
|
|
181
|
+
* La fusión la hace `mergePatch`, la MISMA que `LocalVault` — y tiene que ser la misma,
|
|
182
|
+
* porque no es un `{...a, ...b}`: un campo sin `value` dice «déjale el que tenga», no
|
|
183
|
+
* «bórralo». Marcar uno como privado llega así, y fusionarlo a lo bruto le borraba el
|
|
184
|
+
* valor. Costó encontrarlo porque no falla: guarda, y el campo ya no está.
|
|
185
|
+
*/
|
|
186
|
+
async patch (id, changes = {}) {
|
|
187
|
+
return this.put(mergePatch(await this.get(id), changes))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async remove (id) { return this.sealed.remove(id) }
|
|
191
|
+
|
|
192
|
+
/** La huella de un sitio, por si quien llama quiere preguntar por ella directamente. */
|
|
193
|
+
async siteIndexOf (site) {
|
|
194
|
+
const { kidx } = await this.keys()
|
|
195
|
+
return siteIndex(kidx, site)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default { SealedLocalVault }
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// EL LADO QUE RESPONDE, con el formato sellado.
|
|
2
|
+
//
|
|
3
|
+
// Hace lo mismo que `VaultResponder` —es donde vive la política: quién puede pedir, qué
|
|
4
|
+
// exige un dedo encima y qué queda anotado— con una diferencia que lo cambia todo: **esta
|
|
5
|
+
// bóveda no puede abrir nada**. Así que lo que entrega son sobres, y lo que comprueba es lo
|
|
6
|
+
// que se puede comprobar sin ninguna llave.
|
|
7
|
+
//
|
|
8
|
+
// Va aparte del responder de siempre y no dentro de él porque el protocolo es OTRO
|
|
9
|
+
// (`pm2.*`, ver `../transport/protocol.js`): mezclarlos daría una clase con dos modos, que
|
|
10
|
+
// es como se acaba sirviendo el formato viejo «mientras tanto» — o sea, el agujero otra vez.
|
|
11
|
+
|
|
12
|
+
import { isRequest, reply, replyError, SEALED_OPS } from '../transport/protocol.js'
|
|
13
|
+
import { ApprovalGate } from './approval.js'
|
|
14
|
+
import { ALWAYS_PRIVATE } from '../fields.js'
|
|
15
|
+
import { CODES } from './errors.js'
|
|
16
|
+
import { PASSKEY_FIELD } from '../sealed/entry.js'
|
|
17
|
+
|
|
18
|
+
/** Lo que es privado POR LO QUE ES, sin mirar ninguna marca. */
|
|
19
|
+
const SIEMPRE_PRIVADO = new Set([...ALWAYS_PRIVATE.filter((k) => k !== 'webauthn'), PASSKEY_FIELD, 'wa.userHandle'])
|
|
20
|
+
|
|
21
|
+
export class SealedResponder {
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* `client` cliente de `@dotrino/proxy-client` conectado e identificado
|
|
25
|
+
* `store` una `SealedStore`
|
|
26
|
+
* `recipients()` → `{ recoveryPub, main, passkeys }`, del acta
|
|
27
|
+
* `isAllowed(pubkey)` quién puede pedir (el acta: `passwords`). Por defecto NADIE.
|
|
28
|
+
* `needsApproval(pubkey)` si ese aparato tiene que pedir permiso (el acta:
|
|
29
|
+
* `unattended`). Se compone con el criterio de abajo: **solo lo privado pregunta**.
|
|
30
|
+
* `approve({ op, payload, pubkey })` pide el visto bueno y espera
|
|
31
|
+
* `encPubOf(pubkey)` para sellarle la respuesta
|
|
32
|
+
* `onRequest(record)` la bitácora
|
|
33
|
+
*/
|
|
34
|
+
constructor ({ client, store, recipients, isAllowed, needsApproval, approve, encPubOf, onRequest } = {}) {
|
|
35
|
+
this.client = client
|
|
36
|
+
this.store = store
|
|
37
|
+
this.recipients = recipients || (async () => ({ recoveryPub: null, main: [], passkeys: [] }))
|
|
38
|
+
this.isAllowed = isAllowed || (() => false)
|
|
39
|
+
this.encPubOf = encPubOf || (() => null)
|
|
40
|
+
this.needsApproval = needsApproval || (async () => true)
|
|
41
|
+
this.approve = approve || (async () => false)
|
|
42
|
+
this.onRequest = onRequest || (() => {})
|
|
43
|
+
this._handler = null
|
|
44
|
+
this.gate = new ApprovalGate({
|
|
45
|
+
ask: ({ op, payload, pubkey }) => this.approve({ op, payload, pubkey }),
|
|
46
|
+
scope: ({ pubkey }) => pubkey
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
revokeApproval (pubkey) { this.gate.revoke(pubkey || null) }
|
|
51
|
+
isApproved (pubkey) { return this.gate.granted(pubkey) }
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* ¿LO QUE SE PIDE ES PRIVADO? Aquí se contesta SIN abrir nada, y por eso hace falta la
|
|
55
|
+
* lista en claro de la entrada (`priv`): la bóveda no ve la vista.
|
|
56
|
+
*
|
|
57
|
+
* Sin `keys` se pide la entrada entera, y eso siempre es privado: pedir todo es pedir la
|
|
58
|
+
* contraseña.
|
|
59
|
+
*/
|
|
60
|
+
async wantsPrivate (op, payload) {
|
|
61
|
+
if (op !== 'pm2.get') return false
|
|
62
|
+
const keys = payload?.keys
|
|
63
|
+
if (!Array.isArray(keys)) return true
|
|
64
|
+
const priv = new Set(await this.store.privateKeysOf(payload?.id))
|
|
65
|
+
return keys.some((k) => SIEMPRE_PRIVADO.has(k) || priv.has(k))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
start () {
|
|
69
|
+
if (this._handler) return
|
|
70
|
+
this._handler = async (from, msg, meta) => {
|
|
71
|
+
const p = typeof msg === 'string' ? (() => { try { return JSON.parse(msg) } catch (_) { return null } })() : msg
|
|
72
|
+
if (!isRequest(p)) return
|
|
73
|
+
// QUIÉN PREGUNTA lo dice el proxio (`fromPubkey`: la llave con la que se identificó esa
|
|
74
|
+
// conexión) o un saludo previo (`pubkeyOfToken`). Aquí se leía `meta.pubkey`, que no
|
|
75
|
+
// existe, y se caía al token: ninguna bóveda sabía a quién sellarle la respuesta, no
|
|
76
|
+
// contestaba, y del otro lado se veía «nadie respondió». Sin llave no hay a quién
|
|
77
|
+
// contestar sellado, así que no se contesta — el token no es una llave.
|
|
78
|
+
const pubkey = meta?.fromPubkey || this.client?.pubkeyOfToken?.(from) || null
|
|
79
|
+
if (!pubkey) { this.onRequest({ op: p.op, outcome: 'anonymous', from: null }); return }
|
|
80
|
+
await this.handle({ from, pubkey, msg: p, sealed: meta?.sealed === true })
|
|
81
|
+
}
|
|
82
|
+
this.client.on('message', this._handler)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
stop () {
|
|
86
|
+
if (!this._handler) return
|
|
87
|
+
this.client.off?.('message', this._handler)
|
|
88
|
+
this._handler = null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Contesta UNA petición. Público para que las pruebas no tengan que fingir un socket. */
|
|
92
|
+
async handle ({ from, pubkey, msg, sealed = true }) {
|
|
93
|
+
const { rid, op, payload } = msg
|
|
94
|
+
const anota = (outcome, extra = {}) => this.onRequest({ op, outcome, from: pubkey, ...extra })
|
|
95
|
+
// EL SELLADO DEL TRANSPORTE NO ES OPCIONAL (CONVENCIONES §4.1): lo que llega en claro
|
|
96
|
+
// no se atiende, porque contestarlo sería mandar sobres a quien no sabemos que es.
|
|
97
|
+
//
|
|
98
|
+
// Los códigos son los del responder de siempre (`responder.js`): «la política dice que
|
|
99
|
+
// no» es `denied` y «el humano dijo que no» es `not-approved`. Se arreglan distinto —uno
|
|
100
|
+
// dando permiso al aparato, el otro volviendo a pedir— y quien pide los distingue así.
|
|
101
|
+
if (!sealed) { anota('unsealed'); return this.#send(from, pubkey, replyError(rid, CODES.UNSEALED, 'unsealed request')) }
|
|
102
|
+
if (!SEALED_OPS.includes(op)) { anota('unknown-op'); return this.#send(from, pubkey, replyError(rid, CODES.DENIED, 'operation not allowed: ' + op)) }
|
|
103
|
+
if (!this.isAllowed(pubkey)) { anota('denied'); return this.#send(from, pubkey, replyError(rid, CODES.DENIED, 'this device cannot ask for passwords')) }
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
if (await this.#needsApproval(op, payload, pubkey)) {
|
|
107
|
+
// `allow`, no `ask`: es el que RECUERDA el sí y junta las simultáneas. `ask` es la
|
|
108
|
+
// función cruda de preguntar, y usarla directamente hacía sonar el teléfono en cada
|
|
109
|
+
// petición — que es como se enseña a aprobar sin mirar.
|
|
110
|
+
const ok = await this.gate.allow({ op, payload, pubkey })
|
|
111
|
+
if (!ok) { anota('rejected'); return this.#send(from, pubkey, replyError(rid, CODES.NOT_APPROVED, 'not approved on the trusted device')) }
|
|
112
|
+
}
|
|
113
|
+
const result = await this.#run(op, payload, pubkey)
|
|
114
|
+
anota('ok')
|
|
115
|
+
return this.#send(from, pubkey, reply(rid, result))
|
|
116
|
+
} catch (e) {
|
|
117
|
+
anota('error', { code: e?.code })
|
|
118
|
+
return this.#send(from, pubkey, replyError(rid, e?.code || CODES.UNKNOWN, e?.message || 'error'))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async #needsApproval (op, payload, pubkey) {
|
|
123
|
+
return await this.needsApproval(pubkey) && await this.wantsPrivate(op, payload)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async #run (op, payload = {}, pubkey) {
|
|
127
|
+
switch (op) {
|
|
128
|
+
case 'pm2.recipients': return this.recipients()
|
|
129
|
+
case 'pm2.profile':
|
|
130
|
+
// Con `set` se estrena (al convertir); sin él, se pide la que hay.
|
|
131
|
+
if (payload.set) return this.store.setProfile(payload.set)
|
|
132
|
+
return this.store.profile({ pub: pubkey })
|
|
133
|
+
case 'pm2.views': return this.store.views({ pub: pubkey })
|
|
134
|
+
case 'pm2.find': return this.store.find({ pub: pubkey, idx: payload.idx || [] })
|
|
135
|
+
case 'pm2.passkey': return this.store.findPasskey({ pub: pubkey, rp: payload.rp, cred: payload.cred })
|
|
136
|
+
case 'pm2.get': return this.store.get(payload.id, { pub: pubkey, keys: payload.keys })
|
|
137
|
+
case 'pm2.put': return this.store.putSealed({ ...payload, by: pubkey })
|
|
138
|
+
case 'pm2.patch': return this.store.patchSealed({ ...payload, by: pubkey })
|
|
139
|
+
case 'pm2.remove': return this.store.remove(payload.id)
|
|
140
|
+
default: throw Object.assign(new Error('operation not allowed: ' + op), { code: CODES.DENIED })
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** `to` es por dónde se contesta (el token); `pubkey`, a quién se le sella. */
|
|
145
|
+
#send (to, pubkey, msg) {
|
|
146
|
+
const encPub = this.encPubOf(pubkey)
|
|
147
|
+
// La respuesta va SELLADA: el proxio enruta pero no cifra. Si no se puede sellar, no se
|
|
148
|
+
// manda — mandarlo en claro sería entregar los sobres y las envolturas a quien mire.
|
|
149
|
+
if (encPub && typeof this.client.sendSealedTo === 'function') {
|
|
150
|
+
return this.client.sendSealedTo(to, msg, { peerEncPub: encPub })
|
|
151
|
+
}
|
|
152
|
+
if (typeof this.client.sendSealed === 'function' && encPub) {
|
|
153
|
+
return this.client.sendSealed([to], msg, { peerEncPub: encPub })
|
|
154
|
+
}
|
|
155
|
+
throw Object.assign(new Error('cannot seal the reply to that device: it has no encryption key in the record'), { code: 'no-encpub' })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default SealedResponder
|