@dotrino/vault 0.49.1 → 0.50.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 +90 -9
- 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.50.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,
|
|
@@ -205,14 +220,14 @@ export function createAdminDesk ({
|
|
|
205
220
|
// Una bóveda anterior a esto sabe guardar de una en una y nada más. Decirlo es
|
|
206
221
|
// mejor que reventar con un TypeError que no explica qué falta actualizar.
|
|
207
222
|
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
|
|
223
|
+
if (!Array.isArray(data.items) || !data.items.length) {
|
|
224
|
+
return { ok: false, error: 'admin: var.setMany needs `items`, each with its key and its sealed envelope' }
|
|
210
225
|
}
|
|
211
226
|
const result = await vars.setMany({
|
|
212
227
|
ns: toScope ? data.ns : null,
|
|
213
228
|
pub: toDevice ? data.pub : null,
|
|
214
|
-
|
|
215
|
-
|
|
229
|
+
items: data.items,
|
|
230
|
+
caller: chk.device,
|
|
216
231
|
by
|
|
217
232
|
})
|
|
218
233
|
const keys = result?.keys || []
|
|
@@ -222,17 +237,22 @@ export function createAdminDesk ({
|
|
|
222
237
|
}
|
|
223
238
|
|
|
224
239
|
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
|
-
|
|
240
|
+
if (!data.sealed || typeof data.sealed !== 'object') {
|
|
241
|
+
// El valor NUNCA viaja en claro, y desde 2026-09-02 tampoco sellado al perfil: el
|
|
242
|
+
// sobre llega HECHO y la bóveda no lo abre. Si llega de otra forma es un error de
|
|
243
|
+
// quien llama, no algo que se pueda «arreglar» aceptándolo.
|
|
244
|
+
return { ok: false, error: 'admin: var.set needs the value already sealed (`sealed`); build it with buildSealedVar()' }
|
|
229
245
|
}
|
|
230
246
|
const result = await vars.set({
|
|
231
247
|
ns: toScope ? data.ns : null,
|
|
232
248
|
pub: toDevice ? data.pub : null,
|
|
233
249
|
key: data.key,
|
|
234
|
-
|
|
250
|
+
sealed: data.sealed,
|
|
235
251
|
public: typeof data.public === 'boolean' ? data.public : undefined,
|
|
252
|
+
// LA LLAVE DE QUIEN LLAMA, no solo su nombre corto. `by` es el `deviceId`, que
|
|
253
|
+
// vale para el registro pero no para comprobar una firma: el sobre trae la de su
|
|
254
|
+
// AUTOR y hay que verificarla contra la llave de quien lo manda.
|
|
255
|
+
caller: chk.device,
|
|
236
256
|
by
|
|
237
257
|
})
|
|
238
258
|
audit('admin.var.set', { by, ns: toScope ? data.ns : null, device: toDevice ? await deviceIdOf(data.pub).catch(() => null) : null, key: data.key })
|
|
@@ -276,3 +296,64 @@ export function createAdminDesk ({
|
|
|
276
296
|
}
|
|
277
297
|
|
|
278
298
|
export default { createAdminDesk, ADMIN_OPS, ADMIN_OPS_WHILE_LOCKED, ADMIN_NONCE_TTL_MS }
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* FABRICAR EL SOBRE DE UNA VARIABLE — del lado de quien escribe.
|
|
302
|
+
*
|
|
303
|
+
* La bóveda ya no ve el valor (dueño, 2026-09-01): recibe el sobre hecho, comprueba lo que
|
|
304
|
+
* puede sin la llave —quién lo firma y a quién envuelve— y lo guarda. Esto es lo que hay que
|
|
305
|
+
* mandarle, y vive aquí para que no lo rearme cada consola por su cuenta.
|
|
306
|
+
*
|
|
307
|
+
* El orden importa y no es casual:
|
|
308
|
+
* 1. se pide a la BÓVEDA a quién hay que envolver (`var.recipients`). La lista sale del
|
|
309
|
+
* acta y la sabe ella; tener dos sitios respondiendo a eso es cómo se deja fuera a
|
|
310
|
+
* alguien sin que nadie se entere.
|
|
311
|
+
* 2. CEK nueva y el valor cifrado con ella;
|
|
312
|
+
* 3. esa CEK envuelta a la PÚBLICA de cada destinatario, más la de recuperación —sin la
|
|
313
|
+
* cual el cajón nacería ilegible para siempre;
|
|
314
|
+
* 4. y la firma del AUTOR sobre el cajón, la clave y el sobre.
|
|
315
|
+
*
|
|
316
|
+
* QUIÉN FIRMA SE PASA COMO FUNCIÓN, no como llave. En el navegador la privada del aparato
|
|
317
|
+
* vive dentro del iframe de identidad y NO SALE de ahí —esa es media garantía del diseño—,
|
|
318
|
+
* así que la consola no puede firmar aquí: pide la firma y ya. En Node, donde la llave sí
|
|
319
|
+
* está a mano, se envuelve `signWithDevice` en dos líneas. Un solo camino para los dos.
|
|
320
|
+
*
|
|
321
|
+
* @param {{recipients:{recoveryPub:string, members:Array<{pub:string,encPub:string}>},
|
|
322
|
+
* owner:string, key:string, value:string,
|
|
323
|
+
* author:{publickey:string, sign:(body:any)=>Promise<{signature:string}>}}} opts
|
|
324
|
+
*/
|
|
325
|
+
export async function buildSealedVar ({ recipients, owner, key, value, author } = {}) {
|
|
326
|
+
const { makeContentKey, encryptWithCek, wrapForMember } = await import('@dotrino/identity/content')
|
|
327
|
+
if (!recipients?.recoveryPub) throw new Error('buildSealedVar: ask the vault for the recipients first (var.recipients)')
|
|
328
|
+
if (typeof value !== 'string' || !value) throw new Error('buildSealedVar: the value must be a non-empty string')
|
|
329
|
+
if (typeof author?.sign !== 'function' || typeof author?.publickey !== 'string') {
|
|
330
|
+
throw new Error('buildSealedVar: author needs { publickey, sign(body) }')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const cek = await makeContentKey()
|
|
334
|
+
const e = await encryptWithCek({ cek, gen: 0, plaintext: value })
|
|
335
|
+
// Envolver solo necesita PÚBLICAS: por eso esto se puede hacer en un navegador sin que
|
|
336
|
+
// ninguna llave privada ande suelta.
|
|
337
|
+
const wraps = { '#recovery': await wrapForMember({ cek, memberEncPub: recipients.recoveryPub }) }
|
|
338
|
+
for (const m of recipients.members || []) {
|
|
339
|
+
wraps[m.pub] = await wrapForMember({ cek, memberEncPub: m.encPub })
|
|
340
|
+
}
|
|
341
|
+
const ts = Date.now()
|
|
342
|
+
const { signature } = await author.sign(authorBody(owner, key, e, ts))
|
|
343
|
+
return { e, wraps, author: { pub: author.publickey, sig: signature, ts } }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* El `author` de `buildSealedVar` a partir de una llave de aparato de Node (la que trae
|
|
348
|
+
* `makeDeviceKey` o un `service-identity.json`). En el navegador no se usa: allí la firma
|
|
349
|
+
* la da el iframe.
|
|
350
|
+
*/
|
|
351
|
+
export function authorFromDeviceKey (device) {
|
|
352
|
+
return {
|
|
353
|
+
publickey: device.publickey,
|
|
354
|
+
sign: async (body) => {
|
|
355
|
+
const { signWithDevice } = await import('@dotrino/identity/capabilities')
|
|
356
|
+
return signWithDevice({ ...device, data: body })
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
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
|