@dotrino/vaultd 0.136.0 → 0.138.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.138.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": {
@@ -25,7 +25,7 @@
25
25
  "@dotrino/identity": "0.105.0",
26
26
  "@dotrino/opaque": "0.1.0",
27
27
  "@dotrino/passmanager": "0.17.0",
28
- "@dotrino/proxy-client": "0.24.0",
28
+ "@dotrino/proxy-client": "0.27.1",
29
29
  "@dotrino/roadmap": "0.3.19",
30
30
  "@dotrino/store": "0.11.0",
31
31
  "@dotrino/update": "0.2.2",
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
  }
@@ -1872,7 +1873,9 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
1872
1873
  // desde fuera se ve igual que «nadie ha aprobado todavía»: se espera para siempre
1873
1874
  // sin nada que mirar. Es el mismo cuidado que en `notifyMembers`.
1874
1875
  for (const pub of who) {
1875
- try { client.sendByPubkey(pub, { type: MSG.ADMIN_EVENT, body, seal }) }
1876
+ // A la app que APRUEBA (`vault`): en un teléfono la llave es de todas sus apps, y sin
1877
+ // decirlo el pedido timbraba a messenger (websocket-proxy ≥ 1.4.0).
1878
+ try { client.sendByPubkey(pub, { type: MSG.ADMIN_EVENT, body, seal }, { app: 'vault' }) }
1876
1879
  catch (e) { log(`[vault] could not ring ${pub.slice(0, 24)}… about ${pend.ns}: ${e.message}`) }
1877
1880
  }
1878
1881
  log(`[vault] ${pend.ns}: rang ${who.length} approver(s) for ${pend.id}`)
@@ -2157,10 +2160,14 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2157
2160
  } catch (e) { return log('[vault] could not list who to notify:', e.message) }
2158
2161
  if (!targets.length) return
2159
2162
 
2163
+ // FIRMA LA LLAVE DE SELLADO, con el acta al lado (el servicio lo comprueba con
2164
+ // `verifyResponder`). Firmaba la maestra, y con la bóveda cerrada —que es como vive— el
2165
+ // aviso reventaba antes de salir: el servicio no se enteraba de ningún cambio.
2160
2166
  const body = { op: 'secrets.changed', ns, ts: Date.now() }
2161
- const { signature } = await identity.signData(body)
2167
+ const acta = (await identity.profileActa?.().catch(() => null))?.acta || null
2168
+ const seal = await sealOrFail(body)
2162
2169
  for (const d of targets) {
2163
- try { client.sendByPubkey(d.sub, { type: MSG.SECRETS_CHANGED, body, signature }) } catch (_) {}
2170
+ try { client.sendByPubkey(d.sub, { type: MSG.SECRETS_CHANGED, body, seal, acta }) } catch (_) {}
2164
2171
  }
2165
2172
  audit('secrets.changed', { ns, notified: targets.length })
2166
2173
  log(`[vault] config for "${ns}" changed: notified ${targets.length} agent(s)`)
@@ -2174,14 +2181,15 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2174
2181
  */
2175
2182
  async function notifyDeviceChange (pub) {
2176
2183
  let cn = null
2184
+ let record = null
2177
2185
  try {
2178
- const record = (await identity.profileActa?.().catch(() => null))?.acta
2186
+ record = (await identity.profileActa?.().catch(() => null))?.acta
2179
2187
  cn = (record?.members || []).find((m) => m.pub === pub)?.cn || null
2180
2188
  } catch (e) { return log('[vault] could not look up who to notify:', e.message) }
2181
2189
  if (!cn) return
2182
2190
  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 (_) {}
2191
+ const seal = await sealOrFail(body)
2192
+ try { client.sendByPubkey(pub, { type: MSG.SECRETS_CHANGED, body, seal, acta: record }) } catch (_) {}
2185
2193
  const device = await deviceIdOf(pub).catch(() => null)
2186
2194
  audit('secrets.changed', { ns: cn, device, notified: 1 })
2187
2195
  log(`[vault] config for device ${device} ("${cn}") changed: notified it`)
@@ -2235,8 +2243,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2235
2243
  */
2236
2244
  async function notifyMembers (ev, info = {}) {
2237
2245
  try {
2246
+ // SIN FIRMA, y no falta nada: quien lo usa es el ACTA que viaja al lado, que se
2247
+ // autentica sola (otra bóveda la adopta por `canAdopt`; el teléfono solo refresca y
2248
+ // pregunta). Lo firmaba la maestra, que no firma con la bóveda cerrada: el aviso
2249
+ // reventaba antes de salir y el acta nueva no llegaba a nadie.
2238
2250
  const body = { ev, ...info, ts: Date.now() }
2239
- const { signature } = await identity.signData(body)
2240
2251
  // EL ACTA VIAJA CON EL AVISO. Sin esto, un miembro se enteraba de que «algo cambió»
2241
2252
  // pero no de QUÉ, y no veía el acta nueva hasta renovar su cert — hasta 30 días.
2242
2253
  // Para un aparato eso era lento; para OTRA BÓVEDA es fatal: se le concede `sella` y
@@ -2274,7 +2285,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
2274
2285
  // el perfil cambió puede esperar a que el aparato abra solo; timbrar por él hacía
2275
2286
  // sonar el teléfono con «alguien pide tus claves» sin ningún pedido detrás.
2276
2287
  // 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 }) }
2288
+ try { client.sendByPubkey(pub, { type: MSG.ADMIN_EVENT, body, acta }, { quiet: true, app: 'vault' }) }
2278
2289
  catch (e) { log(`[vault] could not notify ${pub.slice(0, 24)}… of "${ev}": ${e.message}`) }
2279
2290
  }
2280
2291
  for (const m of miembros) avisar(m.pub)
@@ -3122,8 +3133,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
3122
3133
 
3123
3134
  let done = 0
3124
3135
  for (const gen of pending) {
3136
+ // La llave de sellado firma (el servicio lo comprueba con `verifyResponder`): esto
3137
+ // tiene que funcionar con la bóveda cerrada, y la maestra entonces no firma.
3125
3138
  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)
3139
+ const seal = await sealOrFail(body)
3127
3140
  const answer = new Promise((resolve) => {
3128
3141
  const off = onRewrapOk((d) => {
3129
3142
  if (d.owner !== owner || d.gen !== gen.gen || d.target !== targetPub) return
@@ -3131,7 +3144,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
3131
3144
  })
3132
3145
  setTimeout(() => { off(); resolve(null) }, timeoutMs).unref?.()
3133
3146
  })
3134
- try { client.sendByPubkey(helpers[0].pub, { type: MSG.REWRAP, body, signature }) } catch (e) {
3147
+ try { client.sendByPubkey(helpers[0].pub, { type: MSG.REWRAP, body, seal }) } catch (e) {
3135
3148
  log(`[vault] ${owner}: could not ask for the key to be handed out (${e.message})`)
3136
3149
  continue
3137
3150
  }