@dotrino/vault 0.27.0 → 0.29.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 +2 -2
- package/src/admin.js +5 -34
- package/src/protocol.js +10 -0
- package/src/service.js +52 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"pairing"
|
|
44
44
|
],
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@dotrino/identity": ">=0.
|
|
46
|
+
"@dotrino/identity": ">=0.56.0",
|
|
47
47
|
"@dotrino/proxy-client": ">=0.9.0"
|
|
48
48
|
},
|
|
49
49
|
"license": "MIT",
|
package/src/admin.js
CHANGED
|
@@ -41,12 +41,11 @@ export const ADMIN_OPS = Object.freeze([
|
|
|
41
41
|
// no debe poder dejar sin configuración a los servicios.
|
|
42
42
|
// `var.setMany` es la MISMA operación con varias variables dentro de un solo sobre: no
|
|
43
43
|
// añade permisos, quita reinicios (ver el enrutado abajo).
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
'var.reveal', 'var.history'
|
|
44
|
+
// Ver un valor, su histórico o volver a una versión NO están, y es a propósito: un
|
|
45
|
+
// aparato que administra no tiene sobres de lo privado (solo el servicio dueño y la
|
|
46
|
+
// recuperación). Eso lo hace la bóveda en su máquina (`secret show/history/revert`).
|
|
47
|
+
// No lo cablees.
|
|
48
|
+
'vars', 'var.set', 'var.setMany'
|
|
50
49
|
])
|
|
51
50
|
|
|
52
51
|
/** Cuánto se recuerda un nonce ya usado (el doble de la ventana de frescura). */
|
|
@@ -217,34 +216,6 @@ export function createAdminDesk ({
|
|
|
217
216
|
return { ok: true, result: result || { ok: true } }
|
|
218
217
|
}
|
|
219
218
|
|
|
220
|
-
// VER un valor (y sus versiones anteriores). La bóveda entrega material sellado a
|
|
221
|
-
// ESTE aparato; abrirlo es cosa suya. Se AUDITA, porque leer un secreto a distancia
|
|
222
|
-
// es exactamente lo que hay que poder revisar después.
|
|
223
|
-
if (data.op === 'var.reveal' || data.op === 'var.history') {
|
|
224
|
-
if (!vars) return { ok: false, error: 'admin: this vault does not serve environment variables' }
|
|
225
|
-
if (typeof vars.reveal !== 'function') return { ok: false, error: 'admin: this vault cannot show values (update it)' }
|
|
226
|
-
const toScope = typeof data.ns === 'string' && !!data.ns
|
|
227
|
-
const toDevice = typeof data.pub === 'string' && !!data.pub
|
|
228
|
-
if (toScope === toDevice) return { ok: false, error: 'admin: it needs exactly one target (ns or pub)' }
|
|
229
|
-
const target = { ns: toScope ? data.ns : null, pub: toDevice ? data.pub : null }
|
|
230
|
-
|
|
231
|
-
if (data.op === 'var.history') {
|
|
232
|
-
const result = await vars.history({ ...target, key: typeof data.key === 'string' ? data.key : null })
|
|
233
|
-
audit('admin.var.history', { by, ns: target.ns, key: data.key || null })
|
|
234
|
-
return { ok: true, result }
|
|
235
|
-
}
|
|
236
|
-
if (typeof data.key !== 'string' || !data.key) return { ok: false, error: 'admin: var.reveal needs a key' }
|
|
237
|
-
const result = await vars.reveal({
|
|
238
|
-
...target,
|
|
239
|
-
key: data.key,
|
|
240
|
-
ts: Number.isFinite(data.ts) ? data.ts : null,
|
|
241
|
-
device: chk.device,
|
|
242
|
-
by
|
|
243
|
-
})
|
|
244
|
-
audit('admin.var.reveal', { by, ns: target.ns, key: data.key, ...(data.ts ? { ts: data.ts } : {}) })
|
|
245
|
-
return { ok: true, result }
|
|
246
|
-
}
|
|
247
|
-
|
|
248
219
|
// QUITAR UN DISPOSITIVO se hace por `sub` (su llave): sale del acta Y se le retiran
|
|
249
220
|
// todos los certificados. Las dos cosas o ninguna.
|
|
250
221
|
//
|
package/src/protocol.js
CHANGED
|
@@ -66,6 +66,16 @@ export const MSG = Object.freeze({
|
|
|
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
68
|
SECRETS_CHANGED: 'vault.secrets.changed', // vault → servicio: { body:{op,ns,ts}, signature }
|
|
69
|
+
// REPARTIR LA LLAVE DE UN CAJÓN A UN MIEMBRO NUEVO. Lo hace el SERVICIO y no la
|
|
70
|
+
// bóveda, porque la bóveda no puede abrir la llave sin la frase y el servicio ya la
|
|
71
|
+
// tiene: re-envolverla no le añade ningún poder. Ver `docs/secretos-sellados.md` §8.11.
|
|
72
|
+
//
|
|
73
|
+
// El servicio NO se fía de lo que le manden: comprueba la firma de la maestra, comprueba
|
|
74
|
+
// el ACTA que viene dentro (también firmada) y saca de ahí la llave pública del
|
|
75
|
+
// destinatario. Así, ni siquiera una bóveda comprometida puede hacerle envolver la
|
|
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 }
|
|
78
|
+
REWRAP_OK: 'vault.rewrap.ok', // servicio → vault: { data:{op,owner,gen,target,wrap,ts}, signature, cert }
|
|
69
79
|
// --- CONSOLA REMOTA (docs/consola-remota.md) — requiere cert `vault:admin` ---
|
|
70
80
|
// Un solo mensaje con `data.op`: pending · pair · approve · reject · revoke · audit.
|
|
71
81
|
// Admitir y expulsar, nada más: cambiar permisos, traspasar el mando y los secretos
|
package/src/service.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
makeDeviceKey, makeDeviceEncKey, importDeviceEncKey, signWithDevice, verifyDelegation,
|
|
26
26
|
verifyDeviceSig, makePairingCode, commitCode, pubkeyId
|
|
27
27
|
} from '@dotrino/identity/capabilities'
|
|
28
|
-
import { openWrap, decryptWithCek } from '@dotrino/identity/content'
|
|
28
|
+
import { openWrap, wrapForMember, decryptWithCek } from '@dotrino/identity/content'
|
|
29
29
|
import { verifyActa, sealKeyAt } from '@dotrino/identity/acta'
|
|
30
30
|
import { MSG, secretsScope, isValidSecretsNs } from './protocol.js'
|
|
31
31
|
import { makeEphemeralKey, openSealed } from './sealed.js'
|
|
@@ -652,8 +652,59 @@ export async function watchSecretsChanges ({
|
|
|
652
652
|
try { onRevoked?.({ nonce: body.nonce }) } catch (e) { log('[vault] ' + e.message) }
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
+
/**
|
|
656
|
+
* REPARTIR LA LLAVE DE MI CAJÓN a un miembro nuevo (§8.11 del diseño).
|
|
657
|
+
*
|
|
658
|
+
* Un aparato que entra después de escrita una variable no tiene envoltura de ella, y
|
|
659
|
+
* la bóveda no se la puede hacer: envolver exige abrir la llave, y abrirla pide la
|
|
660
|
+
* frase. Este agente SÍ la tiene abierta, así que la reparte él. No gana ningún poder
|
|
661
|
+
* haciéndolo —ya podía leer eso— y por eso es el único que puede hacerlo sin que
|
|
662
|
+
* nadie ceda nada.
|
|
663
|
+
*
|
|
664
|
+
* NO SE FÍA DE LO QUE LE MANDAN, y esto es lo que hace que sea seguro incluso si la
|
|
665
|
+
* bóveda estuviera comprometida:
|
|
666
|
+
*
|
|
667
|
+
* · la petición va firmada por la MAESTRA;
|
|
668
|
+
* · el acta viaja dentro y se comprueba aparte (también la firma la maestra);
|
|
669
|
+
* · **la llave pública del destinatario se saca del ACTA, nunca del mensaje** — si
|
|
670
|
+
* se cogiera del mensaje, quien lo mandara podría hacer que este agente envolviera
|
|
671
|
+
* la llave para una pública suya;
|
|
672
|
+
* · y el destinatario tiene que ser de ESTE cajón (`cn === ns`): un servicio no puede
|
|
673
|
+
* ampliar el acceso a nada que no sea lo suyo.
|
|
674
|
+
*/
|
|
675
|
+
const handleRewrap = async (payload) => {
|
|
676
|
+
const body = payload?.body
|
|
677
|
+
if (!body || body.op !== 'rewrap') return
|
|
678
|
+
const mineOwners = [`ns:${ns}`, `dev:${saved.device.publickey}`]
|
|
679
|
+
if (!mineOwners.includes(body.owner)) return log('[vault] rewrap for a drawer that is not mine: ignored')
|
|
680
|
+
if (!(await verifyDeviceSig({ publickey: master, data: body, signature: payload.signature }))) {
|
|
681
|
+
return log('[vault] rewrap request BADLY SIGNED: ignored')
|
|
682
|
+
}
|
|
683
|
+
const acta = body.acta
|
|
684
|
+
if (!acta || acta.sealedBy !== master || !(await verifyActa({ acta })).ok) {
|
|
685
|
+
return log('[vault] rewrap request without a valid record: ignored')
|
|
686
|
+
}
|
|
687
|
+
const target = (acta.members || []).find((m) => m.pub === body.target)
|
|
688
|
+
if (!target?.encPub) return log('[vault] rewrap: the target is not in the record (or has no encryption key)')
|
|
689
|
+
if (target.cn !== ns) return log(`[vault] rewrap: ${String(body.target).slice(0, 12)}… is not part of «${ns}»: refused`)
|
|
690
|
+
|
|
691
|
+
try {
|
|
692
|
+
const ident = readServiceIdentity(dir)
|
|
693
|
+
if (!ident?.enc?.privateJwk) return log('[vault] rewrap: this agent has no encryption key')
|
|
694
|
+
const cek = await openWrap({ wrap: body.wrap, myEncPrivateKey: await importDeviceEncKey(ident.enc.privateJwk) })
|
|
695
|
+
const wrap = await wrapForMember({ cek, memberEncPub: target.encPub })
|
|
696
|
+
const data = { op: 'rewrap.ok', owner: body.owner, gen: body.gen, target: body.target, wrap, ts: Date.now() }
|
|
697
|
+
const { signature } = await signWithDevice({ privateJwk: saved.device.privateJwk, data })
|
|
698
|
+
client.sendByPubkey(master, { type: MSG.REWRAP_OK, data, signature, cert: saved.cert })
|
|
699
|
+
log(`[vault] key handed to ${String(body.target).slice(0, 12)}… for ${body.owner} (gen ${body.gen})`)
|
|
700
|
+
} catch (e) {
|
|
701
|
+
log('[vault] rewrap failed: ' + e.message)
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
655
705
|
const handleMessage = async (payload) => {
|
|
656
706
|
if (payload?.type === MSG.REVOKED) return handleRevocation(payload)
|
|
707
|
+
if (payload?.type === MSG.REWRAP) return handleRewrap(payload)
|
|
657
708
|
if (payload?.type !== MSG.SECRETS_CHANGED) return
|
|
658
709
|
const body = payload.body
|
|
659
710
|
if (!body || body.op !== 'secrets.changed' || body.ns !== ns) return
|