@dotrino/vault 0.49.1 → 0.51.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 +4 -1
- package/src/admin.js +93 -10
- package/src/service.js +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.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",
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"./protocol": {
|
|
32
32
|
"import": "./src/protocol.js"
|
|
33
|
+
},
|
|
34
|
+
"./admin": {
|
|
35
|
+
"import": "./src/admin.js"
|
|
33
36
|
}
|
|
34
37
|
},
|
|
35
38
|
"files": [
|
package/src/admin.js
CHANGED
|
@@ -110,6 +110,21 @@ export const AUDIT_MAX = 500
|
|
|
110
110
|
* @param {()=>boolean} [o.isLocked] candado del perfil. Cerrado, solo se atiende
|
|
111
111
|
* `ADMIN_OPS_WHILE_LOCKED`; lo demás responde con el código `vault-locked`.
|
|
112
112
|
*/
|
|
113
|
+
/**
|
|
114
|
+
* EL CUERPO QUE FIRMA EL AUTOR DE UN SOBRE. Uno solo, para los dos lados.
|
|
115
|
+
*
|
|
116
|
+
* Quien escribe firma esto y la bóveda verifica esto MISMO. Si cada lado se armara su
|
|
117
|
+
* versión, la firma dejaría de cuadrar el día que alguien añada un campo —y el síntoma
|
|
118
|
+
* sería «la firma no verifica», que manda a mirar la cripto en vez del formato.
|
|
119
|
+
*
|
|
120
|
+
* Lleva el CAJÓN y la CLAVE, no solo el sobre: sin ellos, una firma válida para
|
|
121
|
+
* `ns:aws/TOKEN` valdría igual para `ns:proxy/TOKEN`, que es exactamente el sobre colado
|
|
122
|
+
* fuera de su sitio que esto viene a impedir.
|
|
123
|
+
*/
|
|
124
|
+
export function authorBody (owner, key, e, ts) {
|
|
125
|
+
return { op: 'var.author', owner, key, iv: e?.iv || null, ct: e?.ct || null, ts }
|
|
126
|
+
}
|
|
127
|
+
|
|
113
128
|
export function createAdminDesk ({
|
|
114
129
|
desk, verify, readActivity = () => [], deviceIdOf, vars = null,
|
|
115
130
|
notify = async () => {}, audit = () => {}, isLocked = () => false,
|
|
@@ -189,7 +204,9 @@ export function createAdminDesk ({
|
|
|
189
204
|
const toScope = typeof data.ns === 'string' && !!data.ns
|
|
190
205
|
const toDevice = typeof data.pub === 'string' && !!data.pub
|
|
191
206
|
if (data.op === 'vars') {
|
|
192
|
-
|
|
207
|
+
// QUIÉN PREGUNTA, no solo su nombre: con su llave se le manda la envoltura de
|
|
208
|
+
// cada pública para que pueda abrirla en su casa.
|
|
209
|
+
const result = await vars.list({ by, caller: chk.device })
|
|
193
210
|
audit('admin.vars', { by })
|
|
194
211
|
return { ok: true, result }
|
|
195
212
|
}
|
|
@@ -205,14 +222,14 @@ export function createAdminDesk ({
|
|
|
205
222
|
// Una bóveda anterior a esto sabe guardar de una en una y nada más. Decirlo es
|
|
206
223
|
// mejor que reventar con un TypeError que no explica qué falta actualizar.
|
|
207
224
|
if (typeof vars.setMany !== 'function') return { ok: false, error: 'admin: this vault cannot save several variables at once (update it)' }
|
|
208
|
-
if (!data.
|
|
209
|
-
return { ok: false, error: 'admin: var.setMany needs
|
|
225
|
+
if (!Array.isArray(data.items) || !data.items.length) {
|
|
226
|
+
return { ok: false, error: 'admin: var.setMany needs `items`, each with its key and its sealed envelope' }
|
|
210
227
|
}
|
|
211
228
|
const result = await vars.setMany({
|
|
212
229
|
ns: toScope ? data.ns : null,
|
|
213
230
|
pub: toDevice ? data.pub : null,
|
|
214
|
-
|
|
215
|
-
|
|
231
|
+
items: data.items,
|
|
232
|
+
caller: chk.device,
|
|
216
233
|
by
|
|
217
234
|
})
|
|
218
235
|
const keys = result?.keys || []
|
|
@@ -222,17 +239,22 @@ export function createAdminDesk ({
|
|
|
222
239
|
}
|
|
223
240
|
|
|
224
241
|
if (typeof data.key !== 'string' || !data.key) return { ok: false, error: 'admin: var.set needs a key' }
|
|
225
|
-
if (!data.
|
|
226
|
-
// El valor NUNCA viaja en claro
|
|
227
|
-
// no
|
|
228
|
-
|
|
242
|
+
if (!data.sealed || typeof data.sealed !== 'object') {
|
|
243
|
+
// El valor NUNCA viaja en claro, y desde 2026-09-02 tampoco sellado al perfil: el
|
|
244
|
+
// sobre llega HECHO y la bóveda no lo abre. Si llega de otra forma es un error de
|
|
245
|
+
// quien llama, no algo que se pueda «arreglar» aceptándolo.
|
|
246
|
+
return { ok: false, error: 'admin: var.set needs the value already sealed (`sealed`); build it with buildSealedVar()' }
|
|
229
247
|
}
|
|
230
248
|
const result = await vars.set({
|
|
231
249
|
ns: toScope ? data.ns : null,
|
|
232
250
|
pub: toDevice ? data.pub : null,
|
|
233
251
|
key: data.key,
|
|
234
|
-
|
|
252
|
+
sealed: data.sealed,
|
|
235
253
|
public: typeof data.public === 'boolean' ? data.public : undefined,
|
|
254
|
+
// LA LLAVE DE QUIEN LLAMA, no solo su nombre corto. `by` es el `deviceId`, que
|
|
255
|
+
// vale para el registro pero no para comprobar una firma: el sobre trae la de su
|
|
256
|
+
// AUTOR y hay que verificarla contra la llave de quien lo manda.
|
|
257
|
+
caller: chk.device,
|
|
236
258
|
by
|
|
237
259
|
})
|
|
238
260
|
audit('admin.var.set', { by, ns: toScope ? data.ns : null, device: toDevice ? await deviceIdOf(data.pub).catch(() => null) : null, key: data.key })
|
|
@@ -276,3 +298,64 @@ export function createAdminDesk ({
|
|
|
276
298
|
}
|
|
277
299
|
|
|
278
300
|
export default { createAdminDesk, ADMIN_OPS, ADMIN_OPS_WHILE_LOCKED, ADMIN_NONCE_TTL_MS }
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* FABRICAR EL SOBRE DE UNA VARIABLE — del lado de quien escribe.
|
|
304
|
+
*
|
|
305
|
+
* La bóveda ya no ve el valor (dueño, 2026-09-01): recibe el sobre hecho, comprueba lo que
|
|
306
|
+
* puede sin la llave —quién lo firma y a quién envuelve— y lo guarda. Esto es lo que hay que
|
|
307
|
+
* mandarle, y vive aquí para que no lo rearme cada consola por su cuenta.
|
|
308
|
+
*
|
|
309
|
+
* El orden importa y no es casual:
|
|
310
|
+
* 1. se pide a la BÓVEDA a quién hay que envolver (`var.recipients`). La lista sale del
|
|
311
|
+
* acta y la sabe ella; tener dos sitios respondiendo a eso es cómo se deja fuera a
|
|
312
|
+
* alguien sin que nadie se entere.
|
|
313
|
+
* 2. CEK nueva y el valor cifrado con ella;
|
|
314
|
+
* 3. esa CEK envuelta a la PÚBLICA de cada destinatario, más la de recuperación —sin la
|
|
315
|
+
* cual el cajón nacería ilegible para siempre;
|
|
316
|
+
* 4. y la firma del AUTOR sobre el cajón, la clave y el sobre.
|
|
317
|
+
*
|
|
318
|
+
* QUIÉN FIRMA SE PASA COMO FUNCIÓN, no como llave. En el navegador la privada del aparato
|
|
319
|
+
* vive dentro del iframe de identidad y NO SALE de ahí —esa es media garantía del diseño—,
|
|
320
|
+
* así que la consola no puede firmar aquí: pide la firma y ya. En Node, donde la llave sí
|
|
321
|
+
* está a mano, se envuelve `signWithDevice` en dos líneas. Un solo camino para los dos.
|
|
322
|
+
*
|
|
323
|
+
* @param {{recipients:{recoveryPub:string, members:Array<{pub:string,encPub:string}>},
|
|
324
|
+
* owner:string, key:string, value:string,
|
|
325
|
+
* author:{publickey:string, sign:(body:any)=>Promise<{signature:string}>}}} opts
|
|
326
|
+
*/
|
|
327
|
+
export async function buildSealedVar ({ recipients, owner, key, value, author } = {}) {
|
|
328
|
+
const { makeContentKey, encryptWithCek, wrapForMember } = await import('@dotrino/identity/content')
|
|
329
|
+
if (!recipients?.recoveryPub) throw new Error('buildSealedVar: ask the vault for the recipients first (var.recipients)')
|
|
330
|
+
if (typeof value !== 'string' || !value) throw new Error('buildSealedVar: the value must be a non-empty string')
|
|
331
|
+
if (typeof author?.sign !== 'function' || typeof author?.publickey !== 'string') {
|
|
332
|
+
throw new Error('buildSealedVar: author needs { publickey, sign(body) }')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const cek = await makeContentKey()
|
|
336
|
+
const e = await encryptWithCek({ cek, gen: 0, plaintext: value })
|
|
337
|
+
// Envolver solo necesita PÚBLICAS: por eso esto se puede hacer en un navegador sin que
|
|
338
|
+
// ninguna llave privada ande suelta.
|
|
339
|
+
const wraps = { '#recovery': await wrapForMember({ cek, memberEncPub: recipients.recoveryPub }) }
|
|
340
|
+
for (const m of recipients.members || []) {
|
|
341
|
+
wraps[m.pub] = await wrapForMember({ cek, memberEncPub: m.encPub })
|
|
342
|
+
}
|
|
343
|
+
const ts = Date.now()
|
|
344
|
+
const { signature } = await author.sign(authorBody(owner, key, e, ts))
|
|
345
|
+
return { e, wraps, author: { pub: author.publickey, sig: signature, ts } }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* El `author` de `buildSealedVar` a partir de una llave de aparato de Node (la que trae
|
|
350
|
+
* `makeDeviceKey` o un `service-identity.json`). En el navegador no se usa: allí la firma
|
|
351
|
+
* la da el iframe.
|
|
352
|
+
*/
|
|
353
|
+
export function authorFromDeviceKey (device) {
|
|
354
|
+
return {
|
|
355
|
+
publickey: device.publickey,
|
|
356
|
+
sign: async (body) => {
|
|
357
|
+
const { signWithDevice } = await import('@dotrino/identity/capabilities')
|
|
358
|
+
return signWithDevice({ ...device, data: body })
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
package/src/service.js
CHANGED
|
@@ -662,7 +662,14 @@ async function openSealedBundle (sealed, ident, acta = null, masterPubkey = null
|
|
|
662
662
|
|
|
663
663
|
const out = {}
|
|
664
664
|
for (const [key, e] of Object.entries(sealed.entries || {})) {
|
|
665
|
-
|
|
665
|
+
// UNA PÚBLICA SE ABRE IGUAL QUE UNA PRIVADA (dueño, 2026-09-02). Aquí había un atajo
|
|
666
|
+
// —`if (e.pub) out[key] = e.v`— porque una pública se guardaba en claro. Ya no: `pub`
|
|
667
|
+
// solo dice si se despacha sin aprobación, y el sobre es el mismo. Dejar el atajo
|
|
668
|
+
// sería aceptar un valor en claro dentro de un paquete que se supone todo sellado, o
|
|
669
|
+
// sea la puerta para colar uno.
|
|
670
|
+
if (typeof e.v === 'string' && !e.e) {
|
|
671
|
+
throw Object.assign(new Error(`${key} arrived in the clear: since 2026-09-02 every variable travels sealed`), { code: 'plaintext-var' })
|
|
672
|
+
}
|
|
666
673
|
// `owner` dice de qué cajón salió, y `gen` con qué llave de ese cajón se abre.
|
|
667
674
|
const cual = String(e.owner || '').startsWith('dev:') ? 'dev' : 'ns'
|
|
668
675
|
const gen = e.gen ?? e.e?.gen ?? 0
|