@dotrino/vaultd 0.136.0 → 0.137.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/protocol.js +3 -3
- package/lib/src/service.js +14 -9
- package/package.json +1 -1
- package/src/vault.js +22 -11
package/lib/src/protocol.js
CHANGED
|
@@ -65,7 +65,7 @@ export const MSG = Object.freeze({
|
|
|
65
65
|
// empieza con el heap limpio.
|
|
66
66
|
// Va FIRMADO por la maestra y el agente lo verifica contra su `iss` pineada: un
|
|
67
67
|
// aviso de reinicio sin autenticar ES un ataque de denegación.
|
|
68
|
-
SECRETS_CHANGED: 'vault.secrets.changed', // vault → servicio: { body:{op,ns,ts},
|
|
68
|
+
SECRETS_CHANGED: 'vault.secrets.changed', // vault → servicio: { body:{op,ns,ts}, seal, acta }
|
|
69
69
|
// REPARTIR LA LLAVE DE UN CAJÓN A UN MIEMBRO NUEVO. Lo hace el SERVICIO y no la
|
|
70
70
|
// bóveda, porque la bóveda no puede abrir la llave sin la frase y el servicio ya la
|
|
71
71
|
// tiene: re-envolverla no le añade ningún poder. Ver `docs/secretos-sellados.md` §8.11.
|
|
@@ -74,7 +74,7 @@ export const MSG = Object.freeze({
|
|
|
74
74
|
// el ACTA que viene dentro (también firmada) y saca de ahí la llave pública del
|
|
75
75
|
// destinatario. Así, ni siquiera una bóveda comprometida puede hacerle envolver la
|
|
76
76
|
// llave para alguien que la maestra no haya metido en su propio cajón.
|
|
77
|
-
REWRAP: 'vault.rewrap', // vault → servicio: { body:{op,owner,gen,wrap,target,acta,ts},
|
|
77
|
+
REWRAP: 'vault.rewrap', // vault → servicio: { body:{op,owner,gen,wrap,target,acta,ts}, seal }
|
|
78
78
|
REWRAP_OK: 'vault.rewrap.ok', // servicio → vault: { data:{op,owner,gen,target,wrap,ts}, signature, cert }
|
|
79
79
|
// --- CONSOLA REMOTA (docs/consola-remota.md) — requiere cert `vault:admin` ---
|
|
80
80
|
// Un solo mensaje con `data.op`: pending · pair · approve · reject · revoke · audit.
|
|
@@ -85,7 +85,7 @@ export const MSG = Object.freeze({
|
|
|
85
85
|
// Aviso a TODOS los miembros de que el perfil cambió (alguien entró o salió). Es la
|
|
86
86
|
// contrapartida de administrar a distancia: sin esto, un enrolamiento remoto sería
|
|
87
87
|
// invisible para el resto de tus dispositivos.
|
|
88
|
-
ADMIN_EVENT: 'vault.admin.event', // vault → todos: { body:{ev,deviceId,by,ts},
|
|
88
|
+
ADMIN_EVENT: 'vault.admin.event', // vault → todos: { body:{ev,deviceId,by,ts}, acta } (pedidos: + seal)
|
|
89
89
|
// RÉPLICAS: la principal empuja lo que hay que servir (el acta y los sobres, que van
|
|
90
90
|
// firmados de antes y no se pueden falsificar) y la réplica acusa hasta qué `seq` tiene.
|
|
91
91
|
REPLICA_PUSH: 'vault.replica.push', // master → réplica: { body:{seq,acta,secrets,ts}, signature }
|
package/lib/src/service.js
CHANGED
|
@@ -1137,8 +1137,9 @@ export async function watchSecretsChanges ({
|
|
|
1137
1137
|
* NO SE FÍA DE LO QUE LE MANDAN, y esto es lo que hace que sea seguro incluso si la
|
|
1138
1138
|
* bóveda estuviera comprometida:
|
|
1139
1139
|
*
|
|
1140
|
-
* · la petición
|
|
1141
|
-
*
|
|
1140
|
+
* · la petición la firma la llave de SELLADO que nombra el acta (`verifyResponder`), no la
|
|
1141
|
+
* maestra: esto tiene que funcionar con la bóveda cerrada;
|
|
1142
|
+
* · el acta viaja dentro y la selló la maestra que este agente conoce;
|
|
1142
1143
|
* · **la llave pública del destinatario se saca del ACTA, nunca del mensaje** — si
|
|
1143
1144
|
* se cogiera del mensaje, quien lo mandara podría hacer que este agente envolviera
|
|
1144
1145
|
* la llave para una pública suya;
|
|
@@ -1150,9 +1151,9 @@ export async function watchSecretsChanges ({
|
|
|
1150
1151
|
if (!body || body.op !== 'rewrap') return
|
|
1151
1152
|
const mineOwners = [`ns:${ns}`, `dev:${saved.device.publickey}`]
|
|
1152
1153
|
if (!mineOwners.includes(body.owner)) return log('[vault] rewrap for a drawer that is not mine: ignored')
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1154
|
+
try {
|
|
1155
|
+
await verifyResponder({ body, seal: payload.seal, acta: body.acta, masterPubkey: master, knownSeq: readServiceIdentity(dir)?.actaSeq ?? null })
|
|
1156
|
+
} catch (e) { return log(`[vault] rewrap request BADLY SIGNED: ignored (${e.message})`) }
|
|
1156
1157
|
const acta = body.acta
|
|
1157
1158
|
if (!acta || acta.sealedBy !== master || !(await verifyActa({ acta })).ok) {
|
|
1158
1159
|
return log('[vault] rewrap request without a valid record: ignored')
|
|
@@ -1192,11 +1193,15 @@ export async function watchSecretsChanges ({
|
|
|
1192
1193
|
// que un aviso falso con fecha lejana no pueda dejar fuera a los de verdad.
|
|
1193
1194
|
if (inFlight.has(body.ts)) return
|
|
1194
1195
|
inFlight.add(body.ts)
|
|
1195
|
-
|
|
1196
|
+
// FIRMADO COMO TODO LO QUE SIRVE LA BÓVEDA: la llave de sellado que nombra el acta, con
|
|
1197
|
+
// el acta al lado. Lo firmaba la maestra, y la bóveda vive CERRADA: el aviso reventaba
|
|
1198
|
+
// con «vault locked» antes de salir y el servicio no se enteraba de ningún cambio
|
|
1199
|
+
// (la maestra solo firma el acta y reenvuelve sobres, CLAUDE.md).
|
|
1200
|
+
let bad = null
|
|
1196
1201
|
try {
|
|
1197
|
-
|
|
1198
|
-
} finally { inFlight.delete(body.ts) }
|
|
1199
|
-
if (
|
|
1202
|
+
await verifyResponder({ body, seal: payload.seal, acta: payload.acta, masterPubkey: master, knownSeq: readServiceIdentity(dir)?.actaSeq ?? null })
|
|
1203
|
+
} catch (e) { bad = e } finally { inFlight.delete(body.ts) }
|
|
1204
|
+
if (bad) return log(`[vault] change notice BADLY SIGNED: ignored (${bad.message})`)
|
|
1200
1205
|
if (body.ts <= lastTs) return
|
|
1201
1206
|
lastTs = body.ts
|
|
1202
1207
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.137.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": {
|
package/src/vault.js
CHANGED
|
@@ -1420,9 +1420,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1420
1420
|
await spreadKey(owner, (esPublica) => recipientsOf(owner, { public: esPublica }))
|
|
1421
1421
|
.catch((e) => log(`[vault] ${owner}: could not hand it the key: ${e.message}`))
|
|
1422
1422
|
}
|
|
1423
|
+
// Con la llave de sellado, como todo lo que sirve la bóveda: la maestra no firma con la
|
|
1424
|
+
// bóveda cerrada, y esto tiene que funcionar cerrada.
|
|
1423
1425
|
const body = { op: 'secrets.result', ns, enc: null, ok: true, ts: Date.now() }
|
|
1424
|
-
|
|
1425
|
-
reply(from, { type: MSG.SECRETS_RESULT, body, signature })
|
|
1426
|
+
reply(from, { type: MSG.SECRETS_RESULT, body, seal: await sealOrFail(body), acta: record })
|
|
1426
1427
|
} catch (e) {
|
|
1427
1428
|
reply(from, { type: MSG.ERROR, error: 'enckey: ' + e.message })
|
|
1428
1429
|
}
|
|
@@ -2157,10 +2158,14 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2157
2158
|
} catch (e) { return log('[vault] could not list who to notify:', e.message) }
|
|
2158
2159
|
if (!targets.length) return
|
|
2159
2160
|
|
|
2161
|
+
// FIRMA LA LLAVE DE SELLADO, con el acta al lado (el servicio lo comprueba con
|
|
2162
|
+
// `verifyResponder`). Firmaba la maestra, y con la bóveda cerrada —que es como vive— el
|
|
2163
|
+
// aviso reventaba antes de salir: el servicio no se enteraba de ningún cambio.
|
|
2160
2164
|
const body = { op: 'secrets.changed', ns, ts: Date.now() }
|
|
2161
|
-
const
|
|
2165
|
+
const acta = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
2166
|
+
const seal = await sealOrFail(body)
|
|
2162
2167
|
for (const d of targets) {
|
|
2163
|
-
try { client.sendByPubkey(d.sub, { type: MSG.SECRETS_CHANGED, body,
|
|
2168
|
+
try { client.sendByPubkey(d.sub, { type: MSG.SECRETS_CHANGED, body, seal, acta }) } catch (_) {}
|
|
2164
2169
|
}
|
|
2165
2170
|
audit('secrets.changed', { ns, notified: targets.length })
|
|
2166
2171
|
log(`[vault] config for "${ns}" changed: notified ${targets.length} agent(s)`)
|
|
@@ -2174,14 +2179,15 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2174
2179
|
*/
|
|
2175
2180
|
async function notifyDeviceChange (pub) {
|
|
2176
2181
|
let cn = null
|
|
2182
|
+
let record = null
|
|
2177
2183
|
try {
|
|
2178
|
-
|
|
2184
|
+
record = (await identity.profileActa?.().catch(() => null))?.acta
|
|
2179
2185
|
cn = (record?.members || []).find((m) => m.pub === pub)?.cn || null
|
|
2180
2186
|
} catch (e) { return log('[vault] could not look up who to notify:', e.message) }
|
|
2181
2187
|
if (!cn) return
|
|
2182
2188
|
const body = { op: 'secrets.changed', ns: cn, ts: Date.now() }
|
|
2183
|
-
const
|
|
2184
|
-
try { client.sendByPubkey(pub, { type: MSG.SECRETS_CHANGED, body,
|
|
2189
|
+
const seal = await sealOrFail(body)
|
|
2190
|
+
try { client.sendByPubkey(pub, { type: MSG.SECRETS_CHANGED, body, seal, acta: record }) } catch (_) {}
|
|
2185
2191
|
const device = await deviceIdOf(pub).catch(() => null)
|
|
2186
2192
|
audit('secrets.changed', { ns: cn, device, notified: 1 })
|
|
2187
2193
|
log(`[vault] config for device ${device} ("${cn}") changed: notified it`)
|
|
@@ -2235,8 +2241,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2235
2241
|
*/
|
|
2236
2242
|
async function notifyMembers (ev, info = {}) {
|
|
2237
2243
|
try {
|
|
2244
|
+
// SIN FIRMA, y no falta nada: quien lo usa es el ACTA que viaja al lado, que se
|
|
2245
|
+
// autentica sola (otra bóveda la adopta por `canAdopt`; el teléfono solo refresca y
|
|
2246
|
+
// pregunta). Lo firmaba la maestra, que no firma con la bóveda cerrada: el aviso
|
|
2247
|
+
// reventaba antes de salir y el acta nueva no llegaba a nadie.
|
|
2238
2248
|
const body = { ev, ...info, ts: Date.now() }
|
|
2239
|
-
const { signature } = await identity.signData(body)
|
|
2240
2249
|
// EL ACTA VIAJA CON EL AVISO. Sin esto, un miembro se enteraba de que «algo cambió»
|
|
2241
2250
|
// pero no de QUÉ, y no veía el acta nueva hasta renovar su cert — hasta 30 días.
|
|
2242
2251
|
// Para un aparato eso era lento; para OTRA BÓVEDA es fatal: se le concede `sella` y
|
|
@@ -2274,7 +2283,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2274
2283
|
// el perfil cambió puede esperar a que el aparato abra solo; timbrar por él hacía
|
|
2275
2284
|
// sonar el teléfono con «alguien pide tus claves» sin ningún pedido detrás.
|
|
2276
2285
|
// El timbre es para los pedidos (`notifyApprovers`), que sí esperan una mano.
|
|
2277
|
-
try { client.sendByPubkey(pub, { type: MSG.ADMIN_EVENT, body,
|
|
2286
|
+
try { client.sendByPubkey(pub, { type: MSG.ADMIN_EVENT, body, acta }, { quiet: true }) }
|
|
2278
2287
|
catch (e) { log(`[vault] could not notify ${pub.slice(0, 24)}… of "${ev}": ${e.message}`) }
|
|
2279
2288
|
}
|
|
2280
2289
|
for (const m of miembros) avisar(m.pub)
|
|
@@ -3122,8 +3131,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3122
3131
|
|
|
3123
3132
|
let done = 0
|
|
3124
3133
|
for (const gen of pending) {
|
|
3134
|
+
// La llave de sellado firma (el servicio lo comprueba con `verifyResponder`): esto
|
|
3135
|
+
// tiene que funcionar con la bóveda cerrada, y la maestra entonces no firma.
|
|
3125
3136
|
const body = { op: 'rewrap', owner, gen: gen.gen, wrap: gen.mine, target: targetPub, acta: record, ts: Date.now() }
|
|
3126
|
-
const
|
|
3137
|
+
const seal = await sealOrFail(body)
|
|
3127
3138
|
const answer = new Promise((resolve) => {
|
|
3128
3139
|
const off = onRewrapOk((d) => {
|
|
3129
3140
|
if (d.owner !== owner || d.gen !== gen.gen || d.target !== targetPub) return
|
|
@@ -3131,7 +3142,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3131
3142
|
})
|
|
3132
3143
|
setTimeout(() => { off(); resolve(null) }, timeoutMs).unref?.()
|
|
3133
3144
|
})
|
|
3134
|
-
try { client.sendByPubkey(helpers[0].pub, { type: MSG.REWRAP, body,
|
|
3145
|
+
try { client.sendByPubkey(helpers[0].pub, { type: MSG.REWRAP, body, seal }) } catch (e) {
|
|
3135
3146
|
log(`[vault] ${owner}: could not ask for the key to be handed out (${e.message})`)
|
|
3136
3147
|
continue
|
|
3137
3148
|
}
|