@dotrino/vaultd 0.60.0 → 0.62.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/index.js +9 -0
- package/package.json +2 -2
- package/src/daemon.js +5 -0
- package/src/tui/app.js +30 -1
- package/src/vault.js +26 -0
- package/src/vaultControl.js +9 -0
package/lib/src/index.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* No reimplementa nada del ecosistema.
|
|
27
27
|
*/
|
|
28
28
|
import { verifyChain, verifyDeviceSig } from '@dotrino/identity/capabilities'
|
|
29
|
+
import { memberCanSign } from '@dotrino/identity/acta'
|
|
29
30
|
import { createEnrollDesk, deviceIdOf, DEVICE_TTL_MS, FRESH_WINDOW_MS } from './enroll.js'
|
|
30
31
|
// Las constantes del protocolo salen del MISMO módulo que usa el daemon: si la lista
|
|
31
32
|
// local se queda corta, el dispositivo deja de handle mensajes sin que nadie lo note.
|
|
@@ -205,6 +206,14 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
205
206
|
async function handleSign (from, p) {
|
|
206
207
|
const chk = await authorise(from, p, SCOPE.SIGN)
|
|
207
208
|
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
|
+
}
|
|
208
217
|
const toSign = p.data?.payload
|
|
209
218
|
if (toSign == null) return send(from, { type: MSG.ERROR, error: 'data.payload required' })
|
|
210
219
|
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.62.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.65.0",
|
|
24
24
|
"@dotrino/passmanager": "^0.9.0",
|
|
25
25
|
"@dotrino/proxy-client": "^0.13.1",
|
|
26
26
|
"ws": "^8.18.0"
|
package/src/daemon.js
CHANGED
|
@@ -340,6 +340,11 @@ export async function runDaemon () {
|
|
|
340
340
|
finally { wipe(ak) }
|
|
341
341
|
return { done: 'perfil desbloqueado' + note, autoLockMs: mgr.profiles.autoLockMs }
|
|
342
342
|
}
|
|
343
|
+
// TECLEAR EN LA PANTALLA DE CONTROL CUENTA COMO USO. Sin esto, navegar por ella no
|
|
344
|
+
// tocaba el daemon —solo lo hacían las acciones—, así que el candado se cerraba a
|
|
345
|
+
// los 5 minutos de ABRIRLO aunque estuvieras delante trabajando. `ref()` ya estira
|
|
346
|
+
// el plazo; esta op no hace nada más, y por eso no devuelve ningún volcado.
|
|
347
|
+
case 'touch': { ref(); return { done: 'ok' } }
|
|
343
348
|
case 'lock': { mgr.profiles.lock(ref()); return { done: 'perfil bloqueado' } }
|
|
344
349
|
// PONER contraseña: los secretos pasan de abrirse con la llave de la máquina a
|
|
345
350
|
// abrirse con la frase. Hay que volver a cerrar la copia maestra con la nueva, o
|
package/src/tui/app.js
CHANGED
|
@@ -552,6 +552,28 @@ function autoLockWakeIn (st, now = Date.now()) {
|
|
|
552
552
|
* reabriría sola en cuanto alguien pulsara Enter (ver `reunlockSilently`), así que el
|
|
553
553
|
* candado del daemon no habría cerrado nada.
|
|
554
554
|
*/
|
|
555
|
+
/** Cada cuánto se le dice al daemon «sigo aquí». Muy por debajo del plazo del candado. */
|
|
556
|
+
const TOUCH_MS = 60_000
|
|
557
|
+
let ultimoToque = 0
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Le dice al daemon que sigues delante, sin pedirle nada. Se llama al teclear.
|
|
561
|
+
*
|
|
562
|
+
* No se espera (`await`): estirar el candado no puede meterse en medio de lo que estabas
|
|
563
|
+
* haciendo, y si se pierde un aviso, el siguiente llega un minuto después — con cinco de
|
|
564
|
+
* plazo, no pasa nada.
|
|
565
|
+
*/
|
|
566
|
+
function seguirAqui (st, api = vc) {
|
|
567
|
+
const ahora = Date.now()
|
|
568
|
+
if (ahora - ultimoToque < TOUCH_MS) return
|
|
569
|
+
const p = (st.state?.profiles || []).find((x) => x.current)
|
|
570
|
+
// Solo tiene sentido en un perfil con contraseña y abierto: sin candado no hay plazo
|
|
571
|
+
// que estirar, y cerrado no hay nada que mantener abierto.
|
|
572
|
+
if (!p?.protected || p.locked) return
|
|
573
|
+
ultimoToque = ahora
|
|
574
|
+
try { Promise.resolve(api.touchProfile(p.id)).catch(() => {}) } catch (_) {}
|
|
575
|
+
}
|
|
576
|
+
|
|
555
577
|
async function forgetAutoLocked (term, st, api = vc) {
|
|
556
578
|
const ids = autoLockedIds(st)
|
|
557
579
|
if (!ids.length) return false
|
|
@@ -1743,6 +1765,13 @@ export async function runTui () {
|
|
|
1743
1765
|
if (st.confirm) { await onConfirmKey(st, key); continue }
|
|
1744
1766
|
if (key.name === 'ctrl-c') { running = false; continue }
|
|
1745
1767
|
|
|
1768
|
+
// TECLEAR ES USO. El candado se cierra a los 5 min de no usarse, y hasta ahora
|
|
1769
|
+
// «usar» solo contaba cuando algo pedía al daemon: podías estar media hora
|
|
1770
|
+
// navegando esta pantalla y se cerraba igual, que es lo contrario de lo que se
|
|
1771
|
+
// quería. Se avisa como mucho una vez por minuto —el plazo son cinco, así que
|
|
1772
|
+
// sobra— para no escribir un archivo por tecla.
|
|
1773
|
+
seguirAqui(st)
|
|
1774
|
+
|
|
1746
1775
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
1747
1776
|
// 'q' global sale.
|
|
1748
1777
|
if (ch === 'q') { running = false; continue }
|
|
@@ -1785,4 +1814,4 @@ export async function runTui () {
|
|
|
1785
1814
|
}
|
|
1786
1815
|
|
|
1787
1816
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
1788
|
-
export const __test = { render, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang, mergeMembersAndCerts }
|
|
1817
|
+
export const __test = { render, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, pairModeRows, pairingBody, scrollBody, fitHelp, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
|
package/src/vault.js
CHANGED
|
@@ -315,6 +315,32 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
315
315
|
expectedScope: SCOPE.SIGN, trustedIssuer: master, revoked: await revocationSet()
|
|
316
316
|
})
|
|
317
317
|
if (!chk.ok) return denyChain(from, chk, p, 'sign')
|
|
318
|
+
|
|
319
|
+
// MANDA EL ACTA, NO EL PAPEL. Esto miraba solo el certificado, y era el único
|
|
320
|
+
// mostrador que lo hacía —`approve` y los secretos ya preguntaban al acta—. El agujero
|
|
321
|
+
// que dejaba: `caps <ID> -firma` quita `sign` del acta pero NO reemite ni revoca el
|
|
322
|
+
// cert del aparato, que sigue diciendo `vault:sign` hasta 30 días. Entonces su
|
|
323
|
+
// `signData` local veía que ya no puede y se lo PEDÍA a la bóveda… que decía que sí.
|
|
324
|
+
// O sea que quitarle el permiso de firmar no se lo quitaba: lo cambiaba de firmar él
|
|
325
|
+
// a que firmaras tú por él.
|
|
326
|
+
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
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
|
+
}
|
|
331
|
+
|
|
332
|
+
// Y CERRADA NO FIRMA NADA (dueño, 2026-08-31). La regla de antes —«el daemon sigue
|
|
333
|
+
// firmando con el perfil bloqueado, para que un reinicio no deje las apps muertas»— es
|
|
334
|
+
// ANTERIOR AL MODELO DE SOBRES y queda derogada: con los sobres la bóveda no necesita
|
|
335
|
+
// firmar nada para servir. Lo que hace abierta es rehacer los sobres.
|
|
336
|
+
//
|
|
337
|
+
// Y las apps no se quedan muertas por esto: un aparato al que el acta le da `sign`
|
|
338
|
+
// firma con SU llave y no pasa por aquí (ver `signData` en el pilar). Leer y guardar
|
|
339
|
+
// tampoco pasan por aquí, y siguen funcionando con el candado echado.
|
|
340
|
+
if (isLocked()) {
|
|
341
|
+
audit('rejected', { what: 'sign', reason: 'locked' })
|
|
342
|
+
return reply(from, { type: MSG.ERROR, error: 'vault locked: the master key does not sign while the vault is closed' })
|
|
343
|
+
}
|
|
318
344
|
const toSign = p.data?.payload
|
|
319
345
|
if (toSign == null) return reply(from, { type: MSG.ERROR, error: 'data.payload required' })
|
|
320
346
|
const { signature, publickey } = await identity.signData(toSign)
|
package/src/vaultControl.js
CHANGED
|
@@ -224,6 +224,15 @@ export const renameProfile = (profile, name) => profileOp('rename', { profile, n
|
|
|
224
224
|
export const removeProfile = (profile) => profileOp('rm', { profile })
|
|
225
225
|
export const unlockProfile = (profile, password) => profileOp('unlock', { profile, password })
|
|
226
226
|
export const lockProfile = (profile) => profileOp('lock', { profile })
|
|
227
|
+
/**
|
|
228
|
+
* «Sigo aquí»: estira el plazo del bloqueo automático sin hacer nada más.
|
|
229
|
+
*
|
|
230
|
+
* La pantalla de control la usa al teclear. Sin esto, moverse por ella no tocaba el
|
|
231
|
+
* daemon —solo lo hacían las acciones—, así que el candado se cerraba a los 5 minutos de
|
|
232
|
+
* ABRIRLO aunque estuvieras delante. El plazo se cuenta desde el último uso, y teclear es
|
|
233
|
+
* uso.
|
|
234
|
+
*/
|
|
235
|
+
export const touchProfile = (profile) => profileOp('touch', { profile })
|
|
227
236
|
// `current`: la contraseña que YA tenía el perfil. Hace falta para abrir la copia
|
|
228
237
|
// maestra de los secretos y volver a cerrarla con la nueva — sin ese paso, cambiar la
|
|
229
238
|
// contraseña dejaría las variables privadas ilegibles.
|