@dotrino/vault 0.24.0 → 0.25.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.24.0",
4
- "description": "Usa ESTE dispositivo (navegador) como b\u00f3veda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegaci\u00f3n a tus m\u00e1quinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
3
+ "version": "0.25.0",
4
+ "description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "module": "src/index.js",
@@ -32,7 +32,8 @@
32
32
  "src",
33
33
  "bin",
34
34
  "README.md",
35
- "LICENSE"
35
+ "LICENSE",
36
+ "!src/types.d.ts"
36
37
  ],
37
38
  "keywords": [
38
39
  "dotrino",
@@ -42,7 +43,7 @@
42
43
  "pairing"
43
44
  ],
44
45
  "peerDependencies": {
45
- "@dotrino/identity": ">=0.30.0",
46
+ "@dotrino/identity": ">=0.53.0",
46
47
  "@dotrino/proxy-client": ">=0.9.0"
47
48
  },
48
49
  "license": "MIT",
@@ -50,12 +51,5 @@
50
51
  "type": "git",
51
52
  "url": "git+https://github.com/imdotrino/dotrino-vault.git",
52
53
  "directory": "lib"
53
- },
54
- "devDependencies": {
55
- "typescript": "^5.7.3",
56
- "@types/node": "^22.0.0"
57
- },
58
- "scripts": {
59
- "type-check": "tsc --noEmit"
60
54
  }
61
55
  }
package/src/atrest.js CHANGED
Binary file
package/src/service.js CHANGED
@@ -22,9 +22,10 @@ import fs from 'node:fs'
22
22
  import path from 'node:path'
23
23
  import { createHash } from 'node:crypto'
24
24
  import {
25
- makeDeviceKey, signWithDevice, verifyDelegation, verifyDeviceSig,
26
- makePairingCode, commitCode, pubkeyId
25
+ makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice, verifyDelegation,
26
+ verifyDeviceSig, makePairingCode, commitCode, pubkeyId
27
27
  } from '@dotrino/identity/capabilities'
28
+ import { openWrap, decryptWithCek } from '@dotrino/identity/content'
28
29
  import { MSG, secretsScope, isValidSecretsNs } from './protocol.js'
29
30
  import { makeEphemeralKey, openSealed } from './sealed.js'
30
31
  import { parseInvite } from './invite.js'
@@ -277,6 +278,11 @@ export async function enrollService ({ qr, ns, dir, label, onCode, onReplace, ap
277
278
  }
278
279
  try {
279
280
  const device = await makeDeviceKey({ label })
281
+ // La llave de CIFRADO, hermana de la de firma. Sin ella el vault no puede
282
+ // sellarle sus variables a ESTE aparato, y arrancaría sin configuración sin
283
+ // que nadie sepa por qué. Su pública viaja en el enrolamiento y acaba en el
284
+ // acta como `encPub`; la privada no sale de este disco.
285
+ const enc = await makeDeviceEncKey()
280
286
  const deviceId = (await pubkeyId(device.publickey)).slice(0, 8).toUpperCase().replace(/(.{4})(.{4})/, '$1-$2')
281
287
  // Código ALEATORIO generado AQUÍ: el vault no lo conoce; solo puede echarlo
282
288
  // de vuelta si el dueño lo tipeó (= tiene esta pantalla a la vista).
@@ -288,7 +294,7 @@ export async function enrollService ({ qr, ns, dir, label, onCode, onReplace, ap
288
294
  // si falta, asume `join` — pero un agente no debe apoyarse en un default
289
295
  // para algo que decide de quién es la cuenta. Yendo dentro de `data`, viaja
290
296
  // firmado: nadie en el medio puede convertirlo en una adopción.
291
- const data = { op: 'enroll', intent: 'join', dpub: device.publickey, token: qr.token || qr.sn, sn: qr.sn, commit, label, ts: Date.now() }
297
+ const data = { op: 'enroll', intent: 'join', dpub: device.publickey, encPub: enc.encPublickey, token: qr.token || qr.sn, sn: qr.sn, commit, label, ts: Date.now() }
292
298
  const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
293
299
 
294
300
  const enrolled = new Promise((resolve, reject) => {
@@ -314,8 +320,10 @@ export async function enrollService ({ qr, ns, dir, label, onCode, onReplace, ap
314
320
 
315
321
  // Reemplazo, no acumulación: el archivo se sobrescribe entero y la identidad
316
322
  // anterior deja de existir en este agente.
317
- writeServiceIdentity(dir, { v: 1, ns, iss: qr.iss, proxy: qr.proxy, device, cert: res.cert, enrolledAt: Date.now() })
318
- return { device, cert: res.cert, iss: qr.iss, replaced }
323
+ // v2: suma `enc`. El `device` (la llave de FIRMA) no se toca — de él sale el
324
+ // `nodeId` del proxio, y cambiarlo le costaría su cajón de variables.
325
+ writeServiceIdentity(dir, { v: 2, ns, iss: qr.iss, proxy: qr.proxy, device, enc: { publickey: enc.encPublickey, privateJwk: enc.encPrivateJwk }, cert: res.cert, enrolledAt: Date.now() })
326
+ return { device, enc, cert: res.cert, iss: qr.iss, replaced }
319
327
  } finally { client.close() }
320
328
  }
321
329
 
@@ -373,11 +381,51 @@ export async function fetchSecrets ({ dir, ns, proxyUrl, masterPubkey, device, c
373
381
  if (!ok) throw new Error('invalid master signature on the secrets reply')
374
382
 
375
383
  const payload = await openSealed({ privateKey: eph.privateKey, enc: body.enc })
384
+
385
+ // DOS CAPAS DE SOBRE, y hacen cosas distintas:
386
+ // · la de fuera (`ek` efímera, recién abierta) tapa el TRAMO — el proxio no ve
387
+ // ni los nombres de tus variables;
388
+ // · la de dentro (`sealed`) tapa el REPOSO — la bóveda guarda lo que reparte
389
+ // sin poder abrirlo.
390
+ // Se quedan las dos: quitar la de fuera dejaría los nombres al aire.
391
+ if (payload?.sealed) return openSealedBundle(payload.sealed, saved)
392
+
393
+ // Bóveda todavía en v3: manda los valores tal cual, como siempre. Desaparece
394
+ // cuando el último vault haya migrado (ver `docs/secretos-sellados.md`).
376
395
  if (!payload || typeof payload.secrets !== 'object') throw new Error('malformed secrets envelope')
377
396
  return payload.secrets
378
397
  } finally { client.close() }
379
398
  }
380
399
 
400
+ /**
401
+ * Abre un bundle sellado: saca la CEK de la envoltura dirigida a este aparato y
402
+ * descifra con ella las variables privadas. Las públicas vienen en claro.
403
+ *
404
+ * Un fallo al abrir es un ERROR DURO, nunca un salto a lo del scope ni un valor
405
+ * omitido: silenciarlo convertiría una rotación mal sellada en «el servicio sigue
406
+ * con el valor viejo y nadie se entera», que es el peor modo de fallo de todo esto.
407
+ */
408
+ async function openSealedBundle (sealed, ident) {
409
+ if (!ident?.enc?.privateJwk) {
410
+ throw new Error('this service has no encryption key: update @dotrino/vault and re-enroll it')
411
+ }
412
+ const mine = await importDeviceEncKey(ident.enc.privateJwk)
413
+ const ceks = {}
414
+ for (const [cual, info] of Object.entries({ ns: sealed.ns, dev: sealed.dev })) {
415
+ if (info?.wrap) ceks[cual] = await openWrap({ wrap: info.wrap, myEncPrivateKey: mine })
416
+ }
417
+
418
+ const out = {}
419
+ for (const [key, e] of Object.entries(sealed.entries || {})) {
420
+ if (e.pub) { out[key] = e.v; continue }
421
+ // `owner` dice de qué cajón salió, y por tanto con qué CEK se abre.
422
+ const cek = ceks[String(e.owner || '').startsWith('dev:') ? 'dev' : 'ns']
423
+ if (!cek) throw new Error(`no key to open ${key}: this device has no wrapping for its drawer`)
424
+ out[key] = await decryptWithCek({ cek, envelope: e.e })
425
+ }
426
+ return out
427
+ }
428
+
381
429
  /**
382
430
  * Escucha los avisos de cambio de configuración de la bóveda.
383
431
  *
package/src/types.d.ts DELETED
@@ -1,5 +0,0 @@
1
- interface Element { [key: string]: any; }
2
- interface EventTarget { [key: string]: any; }
3
- interface HTMLElement { [key: string]: any; }
4
- interface Event { [key: string]: any; }
5
- interface Window { [key: string]: any; }