@dotrino/vaultd 0.62.1 → 0.64.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/lib/src/enroll.js +6 -3
- package/lib/src/index.js +12 -9
- package/package.json +2 -2
- package/src/ctl.js +4 -1
- package/src/manager.js +14 -6
- package/src/vault.js +28 -5
package/lib/src/enroll.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* `docs/pairing-protocol.md`).
|
|
31
31
|
*/
|
|
32
32
|
import { verifyDeviceSig, pubkeyId, commitCode } from '@dotrino/identity/capabilities'
|
|
33
|
-
import { verifyContinuity } from '@dotrino/identity/acta'
|
|
33
|
+
import { verifyContinuity, canSeal } from '@dotrino/identity/acta'
|
|
34
34
|
|
|
35
35
|
/** Un token de emparejamiento vale 5 min. */
|
|
36
36
|
export const PAIRING_TTL_MS = 5 * 60 * 1000
|
|
@@ -388,9 +388,12 @@ export function createEnrollDesk ({
|
|
|
388
388
|
const pend = [...pending.values()].find((x) => x.state === 'AWAITING_ACTA' && (x.from === from || x.dpub))
|
|
389
389
|
if (!pend) return reply(from, { type: MSG_ERROR, error: 'no adoption awaiting a record' })
|
|
390
390
|
if (!record || typeof record !== 'object') return reply(from, { type: MSG_ERROR, error: 'record missing or unreadable' })
|
|
391
|
-
|
|
391
|
+
// Ya no hay campo `sealer`: se le pregunta al PERMISO. Es la misma comprobación —«¿me
|
|
392
|
+
// nombra a mí la que me mandan?»— dicha en el idioma nuevo, y con varios selladores la
|
|
393
|
+
// respuesta puede ser que sí para más de uno, que es lo correcto.
|
|
394
|
+
if (!canSeal(record, iss)) {
|
|
392
395
|
audit('rejected', { what: 'adopt', reason: 'not-sealer' })
|
|
393
|
-
return reply(from, { type: MSG_ERROR, error: 'that record does not
|
|
396
|
+
return reply(from, { type: MSG_ERROR, error: 'that record does not let this vault seal it' })
|
|
394
397
|
}
|
|
395
398
|
if (record.sealedBy !== pend.dpub) {
|
|
396
399
|
audit('rejected', { what: 'adopt', reason: 'sealed-by-other' })
|
package/lib/src/index.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* No reimplementa nada del ecosistema.
|
|
27
27
|
*/
|
|
28
28
|
import { verifyChain, verifyDeviceSig } from '@dotrino/identity/capabilities'
|
|
29
|
-
import {
|
|
29
|
+
import { memberCanScope } from '@dotrino/identity/acta'
|
|
30
30
|
import { createEnrollDesk, deviceIdOf, DEVICE_TTL_MS, FRESH_WINDOW_MS } from './enroll.js'
|
|
31
31
|
// Las constantes del protocolo salen del MISMO módulo que usa el daemon: si la lista
|
|
32
32
|
// local se queda corta, el dispositivo deja de handle mensajes sin que nadie lo note.
|
|
@@ -195,6 +195,17 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
195
195
|
send(from, { type: MSG.ERROR, error: 'unauthorized: ' + chk.reason })
|
|
196
196
|
return null
|
|
197
197
|
}
|
|
198
|
+
// EL ACTA MANDA, EL PAPEL SOLO ACOMPAÑA. Aquí y no en cada mostrador: el certificado
|
|
199
|
+
// dice a qué se comprometió esta bóveda al conectar el aparato, y el acta lo que puede
|
|
200
|
+
// HOY. Quitarle un permiso sella el acta pero no le retira el papel, que vive hasta 30
|
|
201
|
+
// días, así que sin esto seguiría entrando hasta que lo renovara.
|
|
202
|
+
if (expectedScope) {
|
|
203
|
+
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
204
|
+
if (record && !memberCanScope(record, chk.device, expectedScope)) {
|
|
205
|
+
send(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member no longer has that permission' })
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
}
|
|
198
209
|
return chk
|
|
199
210
|
}
|
|
200
211
|
|
|
@@ -206,14 +217,6 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
206
217
|
async function handleSign (from, p) {
|
|
207
218
|
const chk = await authorise(from, p, SCOPE.SIGN)
|
|
208
219
|
if (!chk) return
|
|
209
|
-
// MANDA EL ACTA, NO EL PAPEL. `authorise` mira el certificado, y quitarle `firma` a un
|
|
210
|
-
// aparato NO reemite ni revoca su cert: seguiría diciendo `vault:sign` hasta 30 días.
|
|
211
|
-
// Sin esto, retirarle el permiso lo cambiaba de firmar él a que firmara la bóveda por
|
|
212
|
-
// él — o sea, no se lo retiraba. Mismo arreglo que en el daemon (`src/vault.js`).
|
|
213
|
-
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
214
|
-
if (record && !memberCanSign(record, chk.device)) {
|
|
215
|
-
return send(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member does not sign' })
|
|
216
|
-
}
|
|
217
220
|
const toSign = p.data?.payload
|
|
218
221
|
if (toSign == null) return send(from, { type: MSG.ERROR, error: 'data.payload required' })
|
|
219
222
|
const { signature, publickey } = await identity.signData(toSign)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.64.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@dotrino/identity": "^0.
|
|
23
|
+
"@dotrino/identity": "^0.67.0",
|
|
24
24
|
"@dotrino/passmanager": "^0.9.0",
|
|
25
25
|
"@dotrino/proxy-client": "^0.13.1",
|
|
26
26
|
"ws": "^8.18.0"
|
package/src/ctl.js
CHANGED
|
@@ -548,7 +548,10 @@ async function cmdMembers () {
|
|
|
548
548
|
for (const m of record.members) {
|
|
549
549
|
const who = m.label || m.id
|
|
550
550
|
const marks = [
|
|
551
|
-
|
|
551
|
+
// Ya no hay UN master: la marca dice quién puede sellar, y pueden ser varios. El
|
|
552
|
+
// permiso también sale abajo en la lista, pero aquí se ve de un vistazo — que es
|
|
553
|
+
// justo lo que se busca al mirar quién es quién.
|
|
554
|
+
m.isMaster ? `${B}Sella${Z}` : null,
|
|
552
555
|
m.isMe ? 'este dispositivo' : null,
|
|
553
556
|
m.cn ? `servicio «${m.cn}»` : null
|
|
554
557
|
].filter(Boolean)
|
package/src/manager.js
CHANGED
|
@@ -24,13 +24,16 @@ import { atRestFor } from './atrest.js'
|
|
|
24
24
|
* Pura a propósito (recibe el veredicto ya calculado) para poder probar la regla sin
|
|
25
25
|
* levantar un vault entero.
|
|
26
26
|
*/
|
|
27
|
-
export function assertCanRemove ({ isMaster, memberCount, name = '' }) {
|
|
28
|
-
|
|
27
|
+
export function assertCanRemove ({ isMaster, memberCount, sealerCount = 1, name = '' }) {
|
|
28
|
+
// `isMaster` ahora es «¿puedo sellar?». Y con varios selladores, borrar esta bóveda ya
|
|
29
|
+
// NO deja la cuenta muerta si queda otro que pueda sellar: eso es exactamente para lo
|
|
30
|
+
// que existe el multivault, y el freno tiene que dejar de estorbar en ese caso.
|
|
31
|
+
if (!isMaster || memberCount <= 1 || sealerCount > 1) return true
|
|
29
32
|
const others = memberCount - 1
|
|
30
33
|
const e = new Error(
|
|
31
|
-
`this vault is the
|
|
32
|
-
'
|
|
33
|
-
'with their key and nobody able to
|
|
34
|
+
`this vault is the only one that can seal account "${name}", and it has ${others} more device(s): ` +
|
|
35
|
+
'give another one the sealing permission first (`caps <ID> +sella`). Deleting it like this ' +
|
|
36
|
+
'leaves them with their key and nobody able to seal the record again.'
|
|
34
37
|
)
|
|
35
38
|
e.code = 'MASTER_WITH_MEMBERS'
|
|
36
39
|
e.members = memberCount
|
|
@@ -159,7 +162,12 @@ export async function startVaultManager ({ root = dataDir(), proxyUrl, log = con
|
|
|
159
162
|
v.isMaster().catch(() => false),
|
|
160
163
|
v.profileMembers().catch(() => ({ members: [] }))
|
|
161
164
|
])
|
|
162
|
-
assertCanRemove({
|
|
165
|
+
assertCanRemove({
|
|
166
|
+
isMaster,
|
|
167
|
+
memberCount: (record?.members || []).length,
|
|
168
|
+
sealerCount: (record?.sealers || []).length || 1,
|
|
169
|
+
name: profiles.get(id)?.name || id
|
|
170
|
+
})
|
|
163
171
|
} else {
|
|
164
172
|
log('[vault] profile %s is not open: deleting without being able to check its record', id)
|
|
165
173
|
}
|
package/src/vault.js
CHANGED
|
@@ -307,6 +307,28 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
307
307
|
}
|
|
308
308
|
})
|
|
309
309
|
|
|
310
|
+
/**
|
|
311
|
+
* EL ACTA MANDA, EL PAPEL SOLO ACOMPAÑA. Se pregunta DESPUÉS de verificar la cadena y
|
|
312
|
+
* ANTES de hacer nada, en todos los mostradores.
|
|
313
|
+
*
|
|
314
|
+
* El certificado dice a qué se comprometió esta bóveda cuando conectó el aparato; el
|
|
315
|
+
* acta dice lo que puede HOY. Y no coinciden: `caps <ID> -lee` sella el acta pero no
|
|
316
|
+
* reemite ni revoca el papel, que vive hasta 30 días. Sin esto, quitarle un permiso a un
|
|
317
|
+
* aparato no se lo quitaba — seguía leyendo, guardando o firmando un mes más.
|
|
318
|
+
*
|
|
319
|
+
* Va por `memberCanScope` (el pilar) y no a mano en cada sitio: la regla es la misma en
|
|
320
|
+
* las dos bóvedas y se comprobaba mal en las dos.
|
|
321
|
+
*
|
|
322
|
+
* @returns {boolean} `true` si puede seguir; si no, ya contestó el «no».
|
|
323
|
+
*/
|
|
324
|
+
async function actaAllows (from, chk, scope, what) {
|
|
325
|
+
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
326
|
+
if (!record || Acta.memberCanScope(record, chk.device, scope)) return true
|
|
327
|
+
audit('rejected', { what, reason: 'acta' })
|
|
328
|
+
reply(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member no longer has that permission' })
|
|
329
|
+
return false
|
|
330
|
+
}
|
|
331
|
+
|
|
310
332
|
// --- handleSign / handleGet: idénticos (verifyChain de la cadena D←maestra) ---
|
|
311
333
|
async function handleSign (from, p) {
|
|
312
334
|
if (!isFresh(p.data)) { audit('rejected', { what: 'sign', reason: 'stale' }); return staleReply(from) }
|
|
@@ -323,11 +345,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
323
345
|
// `signData` local veía que ya no puede y se lo PEDÍA a la bóveda… que decía que sí.
|
|
324
346
|
// O sea que quitarle el permiso de firmar no se lo quitaba: lo cambiaba de firmar él
|
|
325
347
|
// a que firmaras tú por él.
|
|
326
|
-
|
|
327
|
-
if (record && !Acta.memberCanSign(record, chk.device)) {
|
|
328
|
-
audit('rejected', { what: 'sign', reason: 'acta' })
|
|
329
|
-
return reply(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member does not sign' })
|
|
330
|
-
}
|
|
348
|
+
if (!await actaAllows(from, chk, SCOPE.SIGN, 'sign')) return
|
|
331
349
|
|
|
332
350
|
// Y CERRADA NO FIRMA NADA (dueño, 2026-08-31). La regla de antes —«el daemon sigue
|
|
333
351
|
// firmando con el perfil bloqueado, para que un reinicio no deje las apps muertas»— es
|
|
@@ -355,6 +373,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
355
373
|
expectedScope: SCOPE.READ, trustedIssuer: master, revoked: await revocationSet()
|
|
356
374
|
})
|
|
357
375
|
if (!chk.ok) return denyChain(from, chk, p, 'get')
|
|
376
|
+
if (!await actaAllows(from, chk, SCOPE.READ, 'get')) return
|
|
358
377
|
const id = p.data?.id || 'root'
|
|
359
378
|
reply(from, { type: MSG.DATA, id, node: store.getNode(id) })
|
|
360
379
|
}
|
|
@@ -383,6 +402,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
383
402
|
chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.READ, trustedIssuer: master, revoked })
|
|
384
403
|
}
|
|
385
404
|
if (!chk.ok) return denyChain(from, chk, p, 'store')
|
|
405
|
+
// El scope que valió es el que hay que preguntarle al acta: un método de lectura pudo
|
|
406
|
+
// pasar con `read` en vez de `store`, y exigirle `store` al acta lo rechazaría a pesar
|
|
407
|
+
// de tener permiso para lo que pidió.
|
|
408
|
+
if (!await actaAllows(from, chk, chk.scope === SCOPE.READ ? SCOPE.READ : SCOPE.STORE, 'store')) return
|
|
386
409
|
try {
|
|
387
410
|
// CIFRADO de punta a punta con la clave de contenido del perfil: el proxy transporta
|
|
388
411
|
// pero no ve nada de lo que el usuario guarda. Si el dispositivo mandó `enc`, se abre
|