@dotrino/vaultd 0.65.0 → 0.73.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/admin.js +63 -46
- package/lib/src/index.js +34 -13
- package/lib/src/service.js +51 -9
- package/package.json +3 -3
- package/src/client.js +16 -5
- package/src/commKey.js +71 -0
- package/src/daemon.js +4 -2
- package/src/manager.js +37 -2
- package/src/profiles.js +42 -1
- package/src/sealers.js +159 -0
- package/src/transport.js +32 -8
- package/src/vault.js +152 -31
package/src/vault.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createEnrollDesk, deviceIdOf, DEVICE_TTL_MS } from '../lib/src/enroll.j
|
|
|
19
19
|
import { createAdminDesk } from '../lib/src/admin.js'
|
|
20
20
|
import { shouldNotifyRevoked } from '../lib/src/revocation.js'
|
|
21
21
|
import { createTransport, masterPubkeyOf } from './transport.js'
|
|
22
|
+
import { startSealersPublisher } from './sealers.js'
|
|
22
23
|
import { assertKeyOwnsDir } from './keyowner.js'
|
|
23
24
|
import { openStore } from './store.js'
|
|
24
25
|
import { openThreadStore, STORE_READ_METHODS, PROFILE_EDIT_METHODS } from './threadStore.js'
|
|
@@ -30,9 +31,10 @@ import { openSecretsStore, assertVar } from './secretsStore.js'
|
|
|
30
31
|
import { createApprovals, PENDING_TTL_MS } from './approvals.js'
|
|
31
32
|
import { makeSealer } from './sealer.js'
|
|
32
33
|
import { openSealKeys } from './sealKey.js'
|
|
34
|
+
import { openCommKey, COMM_CN, COMM_CAPS } from './commKey.js'
|
|
33
35
|
import { seal } from '../lib/src/sealed.js'
|
|
34
36
|
import { dataDir, ensureDir } from './paths.js'
|
|
35
|
-
import { atRestFor, kekFor, migrateFile } from './atrest.js'
|
|
37
|
+
import { atRestFor, kekFor, migrateFile, encryptText, decryptText } from './atrest.js'
|
|
36
38
|
import { MSG, SCOPE, secretsScope, isValidSecretsNs } from './protocol.js'
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -52,7 +54,7 @@ const MAX_REPLY_BYTES = 768 * 1024
|
|
|
52
54
|
* Solo bloquea EDITAR el perfil (`profileSet`): firmar/leer y el resto del store
|
|
53
55
|
* siguen sirviendo a los dispositivos enrolados aunque esté bloqueado.
|
|
54
56
|
*/
|
|
55
|
-
export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log, onEnrollChallenge, isLocked = () => false, hasPassword = () => true, deriveAdminKey = null, forAdoption = false, onAdopted } = {}) {
|
|
57
|
+
export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log, onEnrollChallenge, isLocked = () => false, hasPassword = () => true, deriveAdminKey = null, openKey = null, forAdoption = false, onAdopted } = {}) {
|
|
56
58
|
ensureDir(dir)
|
|
57
59
|
// CIFRADO EN REPOSO ligado a esta máquina: ningún archivo del dir queda en claro, así
|
|
58
60
|
// que copiarlos a otro equipo no sirve de nada. La identidad se migra AQUÍ (verificando
|
|
@@ -64,7 +66,21 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
64
66
|
const r = migrateFile(path.join(dir, 'identity.json'), kekFor(dir))
|
|
65
67
|
if (r === 'migrado') log('[vault] identity encrypted at rest (bound to this machine)')
|
|
66
68
|
} catch (e) { log('[vault] could not encrypt the identity at rest:', e.message) }
|
|
67
|
-
|
|
69
|
+
/**
|
|
70
|
+
* EL CANDADO DE LA MAESTRA. La mitad privada se guarda sellada con la llave que sale de
|
|
71
|
+
* la contraseña del perfil (`openKey`), no con la de la máquina: cerrada, la maestra no
|
|
72
|
+
* está en memoria y no hay con qué sacarla del disco.
|
|
73
|
+
*
|
|
74
|
+
* `open` devuelve `null` con el perfil cerrado, y el pilar entonces carga la identidad
|
|
75
|
+
* SIN con qué firmar — se sabe quién eres, no se puede hablar por ti. Un perfil sin
|
|
76
|
+
* contraseña no tiene `openKey`: se queda como estaba, bajo la llave de máquina, y la
|
|
77
|
+
* consola ya lo dice en voz alta.
|
|
78
|
+
*/
|
|
79
|
+
const keyLock = {
|
|
80
|
+
seal: async (texto) => { const k = openKey?.(); return k ? encryptText(texto, k) : null },
|
|
81
|
+
open: async (blob) => { const k = openKey?.(); if (!k) return null; try { return decryptText(blob, k) } catch (_) { return null } }
|
|
82
|
+
}
|
|
83
|
+
const identity = await Identity.connect({ dir, atRest: atRestFor(dir), keyLock })
|
|
68
84
|
// ESTE DIRECTORIO ES DE ESTA LLAVE. Es lo único que hay que proteger cuando varias
|
|
69
85
|
// bóvedas viven en un mismo disco: cada una con el suyo, para que nunca se mezclen.
|
|
70
86
|
assertKeyOwnsDir(dir, identity.me?.publickey || null)
|
|
@@ -81,6 +97,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
81
97
|
// abre nada, así que se usa sin la frase; su autoridad se la da el acta, que la nombra
|
|
82
98
|
// y que sella únicamente la maestra. Se estrena una por acta: aquí está el proveedor.
|
|
83
99
|
const sealKeys = openSealKeys(dir)
|
|
100
|
+
const commKey = openCommKey(dir)
|
|
84
101
|
identity.setSealKeyProvider?.(() => sealKeys.mint())
|
|
85
102
|
|
|
86
103
|
const store = openStore(dir)
|
|
@@ -176,7 +193,12 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
176
193
|
recipients: (owner) => recipientsOf(owner),
|
|
177
194
|
// La FIRMA del sobre: dice que salió de esta bóveda y con qué acta (§8.8).
|
|
178
195
|
signer: (body) => signSeal(body),
|
|
179
|
-
|
|
196
|
+
// CON QUÉ se cierra la copia de recuperación cuando quien llama no trajo la frase:
|
|
197
|
+
// la llave del perfil si está ABIERTO, y si no la de la máquina. El orden importa —
|
|
198
|
+
// un perfil CON contraseña no se abre con la llave de la máquina, así que sin lo
|
|
199
|
+
// primero un aparato enrolado con la bóveda abierta se quedaba sin su cajón y solo
|
|
200
|
+
// se enteraba al arrancar (dueño, 2026-08-31).
|
|
201
|
+
defaultKey: () => openKey?.() || new Uint8Array(kekFor(dir))
|
|
180
202
|
})
|
|
181
203
|
// SIN CONTRASEÑA NO HAY SECRETO. Escribir no la pide (sellar solo necesita públicas,
|
|
182
204
|
// §8.1), pero la copia de recuperación —la que deja al dueño VER sus valores— se cierra
|
|
@@ -214,7 +236,38 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
214
236
|
}
|
|
215
237
|
} catch (e) { log('[vault] could not set up the sealing key:', e.message) }
|
|
216
238
|
|
|
217
|
-
|
|
239
|
+
/**
|
|
240
|
+
* LA LLAVE DE COMUNICACIÓN, dentro del acta.
|
|
241
|
+
*
|
|
242
|
+
* La maestra sella el acta y reenvuelve sobres; hablar por la red no es suyo. Esta es la
|
|
243
|
+
* que se identifica ante el proxio, y su autoridad la dice el ACTA: entra como un miembro
|
|
244
|
+
* más, con `cn: 'vault'`, así que el acta la trata como un servicio del perfil — puede
|
|
245
|
+
* hablar por la bóveda, no firmar por la persona.
|
|
246
|
+
*
|
|
247
|
+
* Solo se puede meter con el perfil ABIERTO (admitir un miembro es sellar el acta, y eso
|
|
248
|
+
* es de la maestra). Con el perfil cerrado no se toca nada: si ya está, se usa; y si no,
|
|
249
|
+
* `identify` se repliega a la maestra y lo dice. Por eso una bóveda que se actualiza
|
|
250
|
+
* tiene que abrirse UNA vez, y a partir de ahí ya puede vivir cerrada.
|
|
251
|
+
*/
|
|
252
|
+
try {
|
|
253
|
+
const info = await identity.profileActa?.()
|
|
254
|
+
const acta = info?.acta
|
|
255
|
+
if (acta && !identity.masterLocked) {
|
|
256
|
+
const pub = await commKey.ensure()
|
|
257
|
+
const yaEsta = (acta.members || []).some((m) => m?.pub === pub)
|
|
258
|
+
if (!yaEsta && info?.isMaster) {
|
|
259
|
+
await identity.admitMember({ pub, label: 'esta bóveda', cn: COMM_CN, caps: [...COMM_CAPS] })
|
|
260
|
+
log('[vault] this vault is now a member of its own record: it talks with its own key, not the master one')
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (e) { log('[vault] could not put the communication key in the record:', e.message) }
|
|
264
|
+
|
|
265
|
+
const { client } = await createTransport({ identity, dir, url: proxyUrl, commKey, log })
|
|
266
|
+
|
|
267
|
+
// El registro público de cadenas de selladores: deposita, si hay a dónde, los eslabones
|
|
268
|
+
// que le dicen a un tercero si esta cuenta sigue sellada por quien él cree. Ver
|
|
269
|
+
// `sealers.js` — es una comodidad para quien lee, nunca una dependencia de la bóveda.
|
|
270
|
+
startSealersPublisher({ identity, client, log })
|
|
218
271
|
|
|
219
272
|
async function revocationSet () {
|
|
220
273
|
const { revoked } = await identity.listDelegations()
|
|
@@ -321,9 +374,35 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
321
374
|
*
|
|
322
375
|
* @returns {boolean} `true` si puede seguir; si no, ya contestó el «no».
|
|
323
376
|
*/
|
|
377
|
+
/**
|
|
378
|
+
* CON QUÉ SE JUZGA UN PAPEL: el acta que tiene esta bóveda.
|
|
379
|
+
*
|
|
380
|
+
* Sustituye a `trustedIssuer: master`, que comparaba contra UNA llave fija y por eso el
|
|
381
|
+
* multivault no podía existir — una segunda selladora sellaba el acta y luego sus papeles
|
|
382
|
+
* los rechazaban los diez mostradores. Ahora se compara contra la lista que dice el acta.
|
|
383
|
+
*
|
|
384
|
+
* Sin acta se devuelven nulos a propósito: `verifyDelegation` responde `no-acta` y el
|
|
385
|
+
* mostrador deniega. Es lo contrario de un repliegue — no hay con qué decidir, así que no
|
|
386
|
+
* se decide que sí.
|
|
387
|
+
*/
|
|
388
|
+
async function contextoActa () {
|
|
389
|
+
const acta = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
390
|
+
if (!acta) return { actaSeq: null, sealers: null }
|
|
391
|
+
return { actaSeq: acta.seq, sealers: Acta.sealersOf(acta) }
|
|
392
|
+
}
|
|
393
|
+
|
|
324
394
|
async function actaAllows (from, chk, scope, what) {
|
|
325
395
|
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
326
|
-
if (!record ||
|
|
396
|
+
// SIN ACTA NO SE ATIENDE. Esto era `if (!record || puede())`, o sea: sin acta, pasa. Un
|
|
397
|
+
// repliegue no dice «por si falta el dato», dice «si falta el dato, di que sí» — y el
|
|
398
|
+
// acta falta justo cuando algo se rompió (un archivo a medias, un `catch` que devolvió
|
|
399
|
+
// null), que es cuando menos hay que fiarse. Toda cuenta tiene acta desde hace versiones.
|
|
400
|
+
if (!record) {
|
|
401
|
+
audit('rejected', { what, reason: 'sin-acta' })
|
|
402
|
+
reply(from, { type: MSG.ERROR, error: 'unauthorized: this vault has no record to decide with' })
|
|
403
|
+
return false
|
|
404
|
+
}
|
|
405
|
+
if (Acta.memberCanScope(record, chk.device, scope)) return true
|
|
327
406
|
audit('rejected', { what, reason: 'acta' })
|
|
328
407
|
reply(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member no longer has that permission' })
|
|
329
408
|
return false
|
|
@@ -334,7 +413,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
334
413
|
if (!isFresh(p.data)) { audit('rejected', { what: 'sign', reason: 'stale' }); return staleReply(from) }
|
|
335
414
|
const chk = await verifyChain({
|
|
336
415
|
data: p.data, signature: p.signature, cert: p.cert,
|
|
337
|
-
expectedScope: SCOPE.SIGN,
|
|
416
|
+
expectedScope: SCOPE.SIGN, ...(await contextoActa()), revoked: await revocationSet()
|
|
338
417
|
})
|
|
339
418
|
if (!chk.ok) return denyChain(from, chk, p, 'sign')
|
|
340
419
|
|
|
@@ -370,7 +449,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
370
449
|
if (!isFresh(p.data)) return staleReply(from)
|
|
371
450
|
const chk = await verifyChain({
|
|
372
451
|
data: p.data, signature: p.signature, cert: p.cert,
|
|
373
|
-
expectedScope: SCOPE.READ,
|
|
452
|
+
expectedScope: SCOPE.READ, ...(await contextoActa()), revoked: await revocationSet()
|
|
374
453
|
})
|
|
375
454
|
if (!chk.ok) return denyChain(from, chk, p, 'get')
|
|
376
455
|
if (!await actaAllows(from, chk, SCOPE.READ, 'get')) return
|
|
@@ -397,9 +476,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
397
476
|
return reply(from, { type: MSG.ERROR, error: 'profile locked: unlock it on the vault machine (dotrino-vault unlock) to edit it' })
|
|
398
477
|
}
|
|
399
478
|
const revoked = await revocationSet()
|
|
400
|
-
let chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.STORE,
|
|
479
|
+
let chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.STORE, ...(await contextoActa()), revoked })
|
|
401
480
|
if (!chk.ok && STORE_READ_METHODS.has(d.method)) {
|
|
402
|
-
chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.READ,
|
|
481
|
+
chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.READ, ...(await contextoActa()), revoked })
|
|
403
482
|
}
|
|
404
483
|
if (!chk.ok) return denyChain(from, chk, p, 'store')
|
|
405
484
|
// El scope que valió es el que hay que preguntarle al acta: un método de lectura pudo
|
|
@@ -523,8 +602,12 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
523
602
|
|
|
524
603
|
async function handleDevices (from, p) {
|
|
525
604
|
if (!isFresh(p.data)) return staleReply(from)
|
|
526
|
-
const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert,
|
|
605
|
+
const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await contextoActa()), revoked: await revocationSet() })
|
|
527
606
|
if (!chk.ok) return denyChain(from, chk, p, null)
|
|
607
|
+
// La lista de aparatos es el perfil: quién eres, cómo se llama cada máquina y qué puede.
|
|
608
|
+
// Este mostrador no preguntaba al acta, así que un aparato al que le quitaste `lee`
|
|
609
|
+
// seguía viendo tu inventario entero hasta que caducara su papel.
|
|
610
|
+
if (!await actaAllows(from, chk, SCOPE.READ, 'devices')) return
|
|
528
611
|
const { issued, revoked } = await identity.listDelegations()
|
|
529
612
|
// El acta viaja con la lista: así cada dispositivo se entera de los cambios de
|
|
530
613
|
// política (quién manda, quién puede qué) sin un canal aparte.
|
|
@@ -582,7 +665,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
582
665
|
const RENEW_TTL_MS = 30 * 24 * 60 * 60 * 1000
|
|
583
666
|
async function handleRenew (from, p) {
|
|
584
667
|
if (!isFresh(p.data)) { audit('rejected', { what: 'renew', reason: 'stale' }); return staleReply(from) }
|
|
585
|
-
const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert,
|
|
668
|
+
const chk = await verifyChain({ data: p.data, signature: p.signature, cert: p.cert, ...(await contextoActa()), revoked: await revocationSet() })
|
|
586
669
|
if (!chk.ok) return denyChain(from, chk, p, 'renew')
|
|
587
670
|
// Reusar el label del cert original (si sigue registrado en delegations).
|
|
588
671
|
const { issued } = await identity.listDelegations()
|
|
@@ -594,18 +677,23 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
594
677
|
// QUITARLO tampoco surtía efecto hasta que el cert caducara, hasta un mes después.
|
|
595
678
|
// Si el miembro ya no está en el acta, no se renueva nada: lo echaron.
|
|
596
679
|
const record = (await identity.profileActa?.().catch(() => null))?.acta
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
680
|
+
// Sin acta no se renueva: caer al scope del papel viejo es reconceder a ciegas lo que
|
|
681
|
+
// el acta ya no dice. Un repliegue así no protege de nada, tapa.
|
|
682
|
+
if (!record) {
|
|
683
|
+
audit('rejected', { what: 'renew', device: await deviceIdOf(p.cert.sub), reason: 'sin-acta' })
|
|
684
|
+
return reply(from, { type: MSG.ERROR, error: 'unauthorized: this vault has no record to decide with' })
|
|
685
|
+
}
|
|
686
|
+
const scope = Acta.memberScopes(record, p.cert.sub)
|
|
687
|
+
if (!scope.length) {
|
|
688
|
+
audit('rejected', { what: 'renew', device: await deviceIdOf(p.cert.sub), reason: 'not-a-member' })
|
|
689
|
+
return reply(from, { type: MSG.ERROR, error: 'unauthorized: the record no longer lists this device' })
|
|
604
690
|
}
|
|
605
691
|
const { cert } = await identity.signDelegation(p.cert.sub, scope, { ttlMs: RENEW_TTL_MS, label: prev?.label || '' })
|
|
606
692
|
audit('renew', { device: await deviceIdOf(p.cert.sub), label: prev?.label || '', scope })
|
|
607
693
|
log(`[vault] cert renewed for ${await deviceIdOf(p.cert.sub)} (30 days)`)
|
|
608
|
-
|
|
694
|
+
// El acta viaja con el papel: sin ella quien lo recibe no puede comprobar que lo firmó
|
|
695
|
+
// una selladora del perfil, que es lo que sustituyó a «lo firmó la maestra».
|
|
696
|
+
reply(from, { type: MSG.RENEWED, cert, acta: record })
|
|
609
697
|
}
|
|
610
698
|
|
|
611
699
|
// SECRETOS de servicios: un servicio enrolado (cert `vault:secrets:<ns>`)
|
|
@@ -628,9 +716,19 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
628
716
|
if (!isValidSecretsNs(ns)) return reply(from, { type: MSG.ERROR, error: 'enckey: invalid namespace' })
|
|
629
717
|
const chk = await verifyChain({
|
|
630
718
|
data: p.data, signature: p.signature, cert: p.cert,
|
|
631
|
-
expectedScope: secretsScope(ns),
|
|
719
|
+
expectedScope: secretsScope(ns), ...(await contextoActa()), revoked: await revocationSet()
|
|
632
720
|
})
|
|
633
721
|
if (!chk.ok) return denyChain(from, chk, p, 'enckey')
|
|
722
|
+
// MISMA FRONTERA QUE SERVIR EL CAJÓN, y por la misma razón: esto no solo apunta una
|
|
723
|
+
// llave pública, acto seguido le ENVUELVE la del cajón (`spreadKey`, más abajo). Aquí
|
|
724
|
+
// solo se miraba el certificado, así que a un aparato al que el acta ya le había
|
|
725
|
+
// quitado `secrets` se le entregaba la llave igual, hasta 30 días. Es el mismo fallo
|
|
726
|
+
// que se cerró en `handleSign`, en otro mostrador.
|
|
727
|
+
const record = (await identity.profileActa?.().catch(() => null))?.acta
|
|
728
|
+
if (!record || !Acta.memberCanReadSecrets(record, chk.device, ns)) {
|
|
729
|
+
audit('rejected', { what: 'enckey', ns, reason: record ? 'cn' : 'sin-acta' })
|
|
730
|
+
return reply(from, { type: MSG.ERROR, error: `unauthorized: cn — the record does not recognise this member as the "${ns}" service` })
|
|
731
|
+
}
|
|
634
732
|
try {
|
|
635
733
|
await identity.setMemberEncPub({ pub: chk.device, encPub: p.data.encPub })
|
|
636
734
|
audit('enckey', { device: await deviceIdOf(chk.device).catch(() => null), ns })
|
|
@@ -662,15 +760,15 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
662
760
|
if (typeof p.data?.ek !== 'string') return reply(from, { type: MSG.ERROR, error: 'secrets: missing ek (requester ephemeral key)' })
|
|
663
761
|
const chk = await verifyChain({
|
|
664
762
|
data: p.data, signature: p.signature, cert: p.cert,
|
|
665
|
-
expectedScope: secretsScope(ns),
|
|
763
|
+
expectedScope: secretsScope(ns), ...(await contextoActa()), revoked: await revocationSet()
|
|
666
764
|
})
|
|
667
765
|
if (!chk.ok) return denyChain(from, chk, p, 'secrets')
|
|
668
766
|
// FRONTERA DEL CN (acta): además del scope del cert, el acta tiene que decir que este
|
|
669
767
|
// miembro es el servicio `ns`. Así el límite no depende solo de qué cert se emitió: la
|
|
670
768
|
// llave del proxy no ve nada que no sea del proxy, y está escrito donde se puede comprobar.
|
|
671
769
|
const record = (await identity.profileActa?.().catch(() => null))?.acta
|
|
672
|
-
if (record
|
|
673
|
-
audit('rejected', { what: 'secrets', ns, reason: 'cn' })
|
|
770
|
+
if (!record || !Acta.memberCanReadSecrets(record, chk.device, ns)) {
|
|
771
|
+
audit('rejected', { what: 'secrets', ns, reason: record ? 'cn' : 'sin-acta' })
|
|
674
772
|
return reply(from, { type: MSG.ERROR, error: `unauthorized: cn — the record does not recognise this member as the "${ns}" service` })
|
|
675
773
|
}
|
|
676
774
|
// APROBACIÓN: si este APARATO está marcado (`dotrino-vault approval <ID> on`), liberarle
|
|
@@ -748,12 +846,12 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
748
846
|
const op = p.data?.op
|
|
749
847
|
const chk = await verifyChain({
|
|
750
848
|
data: p.data, signature: p.signature, cert: p.cert,
|
|
751
|
-
expectedScope: SCOPE.APPROVE,
|
|
849
|
+
expectedScope: SCOPE.APPROVE, ...(await contextoActa()), revoked: await revocationSet()
|
|
752
850
|
})
|
|
753
851
|
if (!chk.ok) return denyChain(from, chk, p, 'approval')
|
|
754
852
|
const record = (await identity.profileActa?.().catch(() => null))?.acta
|
|
755
|
-
if (record
|
|
756
|
-
audit('rejected', { what: 'approval', reason: 'acta' })
|
|
853
|
+
if (!record || !Acta.memberCan(record, chk.device, 'approve')) {
|
|
854
|
+
audit('rejected', { what: 'approval', reason: record ? 'acta' : 'sin-acta' })
|
|
757
855
|
return reply(from, { type: MSG.ERROR, error: 'unauthorized: acta — this member does not approve' })
|
|
758
856
|
}
|
|
759
857
|
const by = await deviceIdOf(chk.device).catch(() => null)
|
|
@@ -1197,8 +1295,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1197
1295
|
const admin = createAdminDesk({
|
|
1198
1296
|
desk,
|
|
1199
1297
|
deviceIdOf,
|
|
1200
|
-
ttlMs: DEVICE_TTL_MS,
|
|
1201
1298
|
audit,
|
|
1299
|
+
// El MISMO candado que frena firmar y editar el perfil: revocar reescribe el acta y
|
|
1300
|
+
// configurar toca los secretos, y ninguna de las dos se hace con la bóveda cerrada.
|
|
1301
|
+
isLocked,
|
|
1202
1302
|
notify: notifyMembers,
|
|
1203
1303
|
readActivity,
|
|
1204
1304
|
vars: varsDesk,
|
|
@@ -1209,7 +1309,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1209
1309
|
verify: async ({ data, signature, cert }) => {
|
|
1210
1310
|
const chk = await verifyChain({
|
|
1211
1311
|
data, signature, cert,
|
|
1212
|
-
expectedScope: SCOPE.ADMIN,
|
|
1312
|
+
expectedScope: SCOPE.ADMIN, ...(await contextoActa()), revoked: await revocationSet()
|
|
1213
1313
|
})
|
|
1214
1314
|
// Quitarse a UNO MISMO desde la consola remota entra por aquí: la segunda petición
|
|
1215
1315
|
// que mande el aparato ya llega con el certificado retirado. Que se entere con el
|
|
@@ -1219,7 +1319,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1219
1319
|
return chk
|
|
1220
1320
|
}
|
|
1221
1321
|
const record = (await identity.profileActa?.().catch(() => null))?.acta
|
|
1222
|
-
if (record
|
|
1322
|
+
if (!record || !Acta.memberCan(record, chk.device, 'admin')) return { ok: false, reason: record ? 'acta' : 'sin-acta' }
|
|
1223
1323
|
return chk
|
|
1224
1324
|
}
|
|
1225
1325
|
})
|
|
@@ -1257,7 +1357,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1257
1357
|
// handlers); el resto de la regla vive en el módulo puro.
|
|
1258
1358
|
if (!isFresh(p.data)) return staleReply(from)
|
|
1259
1359
|
const r = await admin.handle(p.data, { signature: p.signature, cert: p.cert })
|
|
1260
|
-
|
|
1360
|
+
// El `code` viaja aparte del texto: el texto está en inglés y puede cambiar, el código
|
|
1361
|
+
// es lo que empareja quien llama (`vault-locked` ≠ «no autorizado»).
|
|
1362
|
+
if (!r.ok) return reply(from, { type: MSG.ERROR, error: r.error, ...(r.code ? { code: r.code } : {}) })
|
|
1261
1363
|
reply(from, { type: MSG.ADMIN_RESULT, op: p.data.op, result: r.result })
|
|
1262
1364
|
}
|
|
1263
1365
|
|
|
@@ -1891,6 +1993,25 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1891
1993
|
|
|
1892
1994
|
return {
|
|
1893
1995
|
identity, client, store, threads, secrets, master, fingerprint: fp, dir,
|
|
1996
|
+
/**
|
|
1997
|
+
* SOLTAR LA MAESTRA. Cerrar el perfil tiene que sacarla de la memoria, no solo dejar de
|
|
1998
|
+
* cargarla en el siguiente arranque: si no, el candado seguiría siendo una bandera al
|
|
1999
|
+
* lado de una llave descifrada, que es exactamente lo que se quitó de en medio.
|
|
2000
|
+
*
|
|
2001
|
+
* Se recarga el par: sin la llave del perfil el pilar lo deja SIN privada, así que a
|
|
2002
|
+
* partir de aquí firmar se niega con `vault-locked`. Servir sigue igual — eso lo hace la
|
|
2003
|
+
* llave de comunicación, que no depende de la contraseña.
|
|
2004
|
+
*/
|
|
2005
|
+
async dropMasterKey () {
|
|
2006
|
+
const r = await identity.reloadMasterKey?.()
|
|
2007
|
+
return { locked: r?.locked !== false }
|
|
2008
|
+
},
|
|
2009
|
+
/** Al abrir: recuperar la maestra y, si venía en claro de antes, dejarla sellada. */
|
|
2010
|
+
async takeMasterKey () {
|
|
2011
|
+
const r = await identity.reloadMasterKey?.()
|
|
2012
|
+
if (r?.locked === false) { try { await identity.sealMasterKey?.() } catch (_) {} }
|
|
2013
|
+
return { locked: r?.locked !== false }
|
|
2014
|
+
},
|
|
1894
2015
|
startPairing: desk.startPairing,
|
|
1895
2016
|
stopPairing: desk.stopPairing,
|
|
1896
2017
|
listPending: desk.listPending,
|