@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.
@@ -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}, signature }
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}, signature }
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}, signature }
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 }
@@ -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 va firmada por la MAESTRA;
1141
- * · el acta viaja dentro y se comprueba aparte (también la firma la maestra);
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
- if (!(await verifyDeviceSig({ publickey: master, data: body, signature: payload.signature }))) {
1154
- return log('[vault] rewrap request BADLY SIGNED: ignored')
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
- let valid = false
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
- valid = await verifyDeviceSig({ publickey: master, data: body, signature: payload.signature })
1198
- } finally { inFlight.delete(body.ts) }
1199
- if (!valid) return log('[vault] change notice BADLY SIGNED: ignored (not from your vault)')
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.136.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
- const { signature } = await identity.signData(body)
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 { signature } = await identity.signData(body)
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, signature }) } catch (_) {}
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
- const record = (await identity.profileActa?.().catch(() => null))?.acta
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 { signature } = await identity.signData(body)
2184
- try { client.sendByPubkey(pub, { type: MSG.SECRETS_CHANGED, body, signature }) } catch (_) {}
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, signature, acta }, { quiet: true }) }
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 { signature } = await identity.signData(body)
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, signature }) } catch (e) {
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
  }