@dotrino/vaultd 0.51.0 → 0.52.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/lib/src/enroll.js +1 -1
- package/lib/src/protocol.js +5 -1
- package/package.json +2 -2
- package/src/ctl.js +14 -38
- package/src/daemon.js +0 -5
- package/src/tui/app.js +1 -1
- package/src/tui/i18n.js +8 -4
- package/src/vault.js +93 -77
package/lib/src/enroll.js
CHANGED
|
@@ -52,7 +52,7 @@ export const MSG_REVOKED = 'vault.revoked'
|
|
|
52
52
|
export const MSG_ERROR = 'vault.error'
|
|
53
53
|
|
|
54
54
|
/** Los scopes del cert se corresponden 1:1 con las capacidades del acta (§D7). */
|
|
55
|
-
const SCOPE_TO_CAP = { 'vault:sign': 'sign', 'vault:store': 'store', 'vault:read': 'read', 'vault:admin': 'admin' }
|
|
55
|
+
const SCOPE_TO_CAP = { 'vault:sign': 'sign', 'vault:store': 'store', 'vault:read': 'read', 'vault:admin': 'admin', 'vault:passwords': 'passwords' }
|
|
56
56
|
export const scopeToCaps = (scope) =>
|
|
57
57
|
(Array.isArray(scope) ? scope : [scope]).map((s) => SCOPE_TO_CAP[s]).filter(Boolean)
|
|
58
58
|
|
package/lib/src/protocol.js
CHANGED
|
@@ -98,7 +98,11 @@ export const SCOPE = Object.freeze({
|
|
|
98
98
|
// NO incluye cambiar permisos, traspasar el mando ni conceder `admin`: eso es el rol
|
|
99
99
|
// de master y sigue siendo local. No se empareja — se concede desde el PC.
|
|
100
100
|
ADMIN: 'vault:admin',
|
|
101
|
-
APPROVE: 'vault:approve' // aprobar pedidos de secretos (cajones con `approval`); se concede a mano, como admin
|
|
101
|
+
APPROVE: 'vault:approve', // aprobar pedidos de secretos (cajones con `approval`); se concede a mano, como admin
|
|
102
|
+
// El gestor de contraseñas: pedir credenciales de la bóveda, de a una y por dominio.
|
|
103
|
+
// Nunca lista la bóveda entera. Este SÍ se empareja (`pair --scope contrasenas`): es
|
|
104
|
+
// lo primero que hace la extensión, y no tendría sentido obligar a un segundo paso.
|
|
105
|
+
PASSWORDS: 'vault:passwords'
|
|
102
106
|
})
|
|
103
107
|
|
|
104
108
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.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": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"node": ">=20"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@dotrino/identity": "^0.
|
|
23
|
+
"@dotrino/identity": "^0.61.0",
|
|
24
24
|
"@dotrino/passmanager": "^0.1.0",
|
|
25
25
|
"@dotrino/proxy-client": "^0.13.1",
|
|
26
26
|
"ws": "^8.18.0"
|
package/src/ctl.js
CHANGED
|
@@ -195,16 +195,19 @@ async function cmdPair (args = []) {
|
|
|
195
195
|
if (scIdx >= 0) {
|
|
196
196
|
const raw = args[scIdx + 1]
|
|
197
197
|
if (!raw || raw.startsWith('-')) { console.error('uso: dotrino-vault pair --scope sign,read,store,secrets:<ns>'); process.exit(2) }
|
|
198
|
-
const ALIAS = { firma: 'sign', lee: 'read', guarda: 'store' }
|
|
198
|
+
const ALIAS = { firma: 'sign', lee: 'read', guarda: 'store', contrasenas: 'passwords', 'contraseñas': 'passwords' }
|
|
199
199
|
scope = []
|
|
200
200
|
for (const tok of raw.split(',').map((t) => t.trim()).filter(Boolean)) {
|
|
201
201
|
const t = ALIAS[tok] || tok
|
|
202
202
|
if (t === 'admin' || t === 'administra') { console.error('`admin` no se empareja: concédelo desde el PC con dotrino-vault caps <ID> +administra'); process.exit(2) }
|
|
203
203
|
if (t === 'approve' || t === 'aprueba') { console.error('`approve` no se empareja: concédelo desde el PC con dotrino-vault caps <ID> +aprueba'); process.exit(2) }
|
|
204
204
|
if (t === 'sign' || t === 'read' || t === 'store') { scope.push('vault:' + t); continue }
|
|
205
|
+
// El gestor de contraseñas SÍ se empareja con su permiso puesto: es lo único que
|
|
206
|
+
// va a hacer ese aparato, y pedirlo en dos pasos era el paso que nadie daba.
|
|
207
|
+
if (t === 'passwords') { scope.push('vault:passwords'); continue }
|
|
205
208
|
const m = /^secrets:([a-z0-9-]{1,32})$/.exec(t)
|
|
206
209
|
if (m) { scope.push('vault:secrets:' + m[1]); continue }
|
|
207
|
-
console.error('permiso desconocido: %s (sign | read | store | secrets:<ns>)', tok); process.exit(2)
|
|
210
|
+
console.error('permiso desconocido: %s (sign | read | store | contrasenas | secrets:<ns>)', tok); process.exit(2)
|
|
208
211
|
}
|
|
209
212
|
if (service) scope.push('vault:secrets:' + service)
|
|
210
213
|
scope = [...new Set(scope)]
|
|
@@ -404,7 +407,7 @@ async function cmdMembers () {
|
|
|
404
407
|
// se mira quién es quién — y en la lista de variables ya seria tarde.
|
|
405
408
|
if (m.cn && !m.canSeal) console.log(' %ssin llave de cifrado: NO puede leer sus variables%s', R, Z)
|
|
406
409
|
}
|
|
407
|
-
console.log('\n Cambiar permisos: dotrino-vault caps <ID> +firma | -firma | +guarda | -guarda | +lee | -lee | +administra | +aprueba | +permiso')
|
|
410
|
+
console.log('\n Cambiar permisos: dotrino-vault caps <ID> +firma | -firma | +guarda | -guarda | +lee | -lee | +administra | +aprueba | +contrasenas | +permiso')
|
|
408
411
|
console.log(' «Permiso»: ese aparato solo recibe claves privadas cuando lo apruebas desde un aparato con «aprueba» (en cada arranque).')
|
|
409
412
|
console.log(' «Administra» deja conectar y quitar dispositivos desde ese aparato, sin venir aquí.')
|
|
410
413
|
console.log(' No deja cambiar permisos ni traspasar el mando: eso solo se hace en esta máquina.')
|
|
@@ -463,42 +466,20 @@ async function cmdApproval (args = []) {
|
|
|
463
466
|
: `Listo: ${m.id} vuelve a recibir sus claves sin aprobación.`)
|
|
464
467
|
}
|
|
465
468
|
|
|
466
|
-
/**
|
|
467
|
-
* `dotrino-vault passwords <ID> on|off` — deja (o no) que ese aparato pida contraseñas
|
|
468
|
-
* de tu bóveda.
|
|
469
|
-
*
|
|
470
|
-
* Es una lista de ESTA bóveda, no del acta, por lo mismo que `approval`: es ella la que
|
|
471
|
-
* entrega. El acta manda igual — si revocas el aparato allí, deja de pedir aunque siga
|
|
472
|
-
* en esta lista. Y si además le pones `approval on`, cada arranque suyo te pedirá el
|
|
473
|
-
* visto bueno en el teléfono.
|
|
474
|
-
*/
|
|
475
|
-
async function cmdPasswords (args = []) {
|
|
476
|
-
const [id, val] = args
|
|
477
|
-
if (!id || !['on', 'off'].includes(val)) {
|
|
478
|
-
console.error('uso: dotrino-vault passwords <ID> on|off')
|
|
479
|
-
process.exit(1)
|
|
480
|
-
}
|
|
481
|
-
const m = await findMember(id)
|
|
482
|
-
writeReq('secret-request.json', { op: 'passwords', pub: m.pub, id: m.id, label: m.label || '', on: val === 'on' })
|
|
483
|
-
sendSignal(requireDaemon().pid, 'SIGUSR2')
|
|
484
|
-
if (val === 'on') {
|
|
485
|
-
console.log(`Listo: ${m.id} puede pedir contraseñas de tu bóveda (de una en una; nunca la lista entera).`)
|
|
486
|
-
console.log('Reinicia el vault: al arrancar imprime el código de enlace que se pega en la extensión.')
|
|
487
|
-
} else {
|
|
488
|
-
console.log(`Listo: ${m.id} deja de poder pedir contraseñas.`)
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
469
|
/** `dotrino-vault caps <ID> ±permiso` — cambia lo que puede hacer un dispositivo. */
|
|
493
470
|
async function cmdCaps (args = []) {
|
|
494
471
|
const [id, ...changes] = args
|
|
495
472
|
if (!id || !changes.length) {
|
|
496
|
-
console.error('uso: dotrino-vault caps <ID> +firma|-firma|+guarda|-guarda|+lee|-lee|+administra|-administra|+aprueba|-aprueba|+permiso|-permiso')
|
|
473
|
+
console.error('uso: dotrino-vault caps <ID> +firma|-firma|+guarda|-guarda|+lee|-lee|+administra|-administra|+aprueba|-aprueba|+contrasenas|-contrasenas|+permiso|-permiso')
|
|
497
474
|
process.exit(2)
|
|
498
475
|
}
|
|
499
476
|
const CAP_BY_WORD = {
|
|
500
477
|
firma: 'sign', guarda: 'store', lee: 'read', administra: 'admin', aprueba: 'approve',
|
|
501
|
-
|
|
478
|
+
// `contraseñas`: el gestor (la extensión, la app del teléfono) puede PEDIR
|
|
479
|
+
// credenciales de a una. Se acepta con y sin tilde: nadie escribe la ñ en una CLI.
|
|
480
|
+
contrasenas: 'passwords', 'contraseñas': 'passwords',
|
|
481
|
+
sign: 'sign', store: 'store', read: 'read', admin: 'admin', approve: 'approve',
|
|
482
|
+
passwords: 'passwords'
|
|
502
483
|
}
|
|
503
484
|
const s = requireDaemon()
|
|
504
485
|
const m = await findMember(id)
|
|
@@ -1191,7 +1172,7 @@ function help () {
|
|
|
1191
1172
|
(sin la bandera entra a la cuenta activa, o a la de --profile)
|
|
1192
1173
|
pair --service <ns> empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
|
|
1193
1174
|
pair --approval el aparato que entre pedirá tu aprobación (teléfono) al recibir claves
|
|
1194
|
-
pair --scope <lista> los PERMISOS del cert: sign,read,store,secrets:<ns> (sin esto: sign,read,store;
|
|
1175
|
+
pair --scope <lista> los PERMISOS del cert: sign,read,store,contrasenas,secrets:<ns> (sin esto: sign,read,store;
|
|
1195
1176
|
se combina con --service: --service eco --scope sign = bot que firma y lee su cajón)
|
|
1196
1177
|
secret set <ns> <CLAVE> <valor> variable del scope <ns>: la comparten TODOS los
|
|
1197
1178
|
aparatos del perfil que sirven ese namespace
|
|
@@ -1206,10 +1187,6 @@ function help () {
|
|
|
1206
1187
|
secret device set <ID> CLAVE=valor …
|
|
1207
1188
|
secret device import <ID> [archivo.env]
|
|
1208
1189
|
secret device rm <ID> <CLAVE> borra una variable de ese aparato
|
|
1209
|
-
passwords <ID> on|off deja que ese aparato pida CONTRASEÑAS de tu bóveda
|
|
1210
|
-
(de una en una; nunca la lista entera). Con
|
|
1211
|
-
approval on además te pedirá el visto bueno en el
|
|
1212
|
-
teléfono. Reinicia el vault tras el primer on
|
|
1213
1190
|
secret list lista los dos cajones: el valor de las públicas,
|
|
1214
1191
|
tapadas las privadas
|
|
1215
1192
|
secret show [device] <ns|ID> <CLAVE>
|
|
@@ -1234,7 +1211,7 @@ function help () {
|
|
|
1234
1211
|
me tu perfil (nombre, foto, datos) tal como lo tiene la bóveda
|
|
1235
1212
|
members el acta del perfil: quién es tuyo y qué puede hacer
|
|
1236
1213
|
label <ID> <nombre> renombra un dispositivo (el nombre con el que lo reconoces)
|
|
1237
|
-
caps <ID> ±permiso cambia permisos (+firma -guarda +administra …)
|
|
1214
|
+
caps <ID> ±permiso cambia permisos (+firma -guarda +administra +contrasenas …)
|
|
1238
1215
|
revoke <ID|nonce> quita un dispositivo (con el ID, todos sus certificados)
|
|
1239
1216
|
activity [n] bitácora de seguridad: firmas, renovaciones, enrolados, rechazos
|
|
1240
1217
|
logs últimos logs del servicio
|
|
@@ -1284,7 +1261,6 @@ export async function runCtl (argv) {
|
|
|
1284
1261
|
case 'revoke': return cmdRevoke(rest[0])
|
|
1285
1262
|
case 'secret': return cmdSecret(rest)
|
|
1286
1263
|
case 'approval': return cmdApproval(rest)
|
|
1287
|
-
case 'passwords': return cmdPasswords(rest)
|
|
1288
1264
|
case 'activity': return cmdActivity(Number(rest[0]) || 30)
|
|
1289
1265
|
case 'logs': return cmdLogs()
|
|
1290
1266
|
case 'version':
|
package/src/daemon.js
CHANGED
|
@@ -405,11 +405,6 @@ export async function runDaemon () {
|
|
|
405
405
|
console.log('[vault] %d secret(s) applied in one go: %s', changed.length, sec.pub ? 'device' : sec.ns)
|
|
406
406
|
} else if (sec.op === 'set') { await vault.setSecret(sec.ns, sec.key, sec.value, sec.public); console.log('[vault] secret saved: %s/%s', sec.ns, sec.key) }
|
|
407
407
|
else if (sec.op === 'rm') { await vault.deleteSecret(sec.ns, sec.key); console.log('[vault] secret deleted: %s/%s', sec.ns, sec.key) }
|
|
408
|
-
else if (sec.op === 'passwords') {
|
|
409
|
-
const r = await vault.setPasswordDevice(sec.pub, { label: sec.label, on: !!sec.on })
|
|
410
|
-
console.log('[vault] passwords: device %s %s', sec.id || '?', sec.on ? 'ALLOWED' : 'removed')
|
|
411
|
-
if (sec.on) console.log('[vault] restart the vault so it starts answering password requests')
|
|
412
|
-
}
|
|
413
408
|
else if (sec.op === 'approval') { const r = await vault.setApproval(sec.pub, !!sec.approval); console.log('[vault] device %s: approval %s', sec.id || '?', r.approval ? 'REQUIRED on every key request' : 'off') }
|
|
414
409
|
else if (sec.op === 'dev-set') { await vault.setDeviceSecret(sec.pub, sec.key, sec.value, sec.public); console.log('[vault] device secret saved: %s', sec.key) }
|
|
415
410
|
else if (sec.op === 'dev-rm') { await vault.deleteDeviceSecret(sec.pub, sec.key); console.log('[vault] device secret deleted: %s', sec.key) }
|
package/src/tui/app.js
CHANGED
|
@@ -318,7 +318,7 @@ function pairModeRows (st, t) {
|
|
|
318
318
|
* una marca de si los tiene. El de administrar va aparte y avisado: es el único que deja
|
|
319
319
|
* a ese aparato meter y sacar dispositivos sin venir aquí.
|
|
320
320
|
*/
|
|
321
|
-
const CAPS_ORDER = ['sign', 'store', 'read', 'admin']
|
|
321
|
+
const CAPS_ORDER = ['sign', 'store', 'read', 'admin', 'passwords']
|
|
322
322
|
|
|
323
323
|
function capsRows (st, t) {
|
|
324
324
|
const i = L(st)
|
package/src/tui/i18n.js
CHANGED
|
@@ -45,13 +45,15 @@ const es = {
|
|
|
45
45
|
sign: 'Firmar como tú',
|
|
46
46
|
store: 'Guardar tus datos',
|
|
47
47
|
read: 'Leer tus datos',
|
|
48
|
-
admin: 'Administrar el perfil'
|
|
48
|
+
admin: 'Administrar el perfil',
|
|
49
|
+
passwords: 'Pedir tus contraseñas'
|
|
49
50
|
},
|
|
50
51
|
capHint: {
|
|
51
52
|
sign: 'usar tu identidad en las apps del ecosistema',
|
|
52
53
|
store: 'escribir en tu bóveda (perfil, contenido, datos sensibles)',
|
|
53
54
|
read: 'ver lo que guardaste',
|
|
54
|
-
admin: 'conectar y quitar dispositivos desde ese aparato, sin venir aquí'
|
|
55
|
+
admin: 'conectar y quitar dispositivos desde ese aparato, sin venir aquí',
|
|
56
|
+
passwords: 'el gestor: pedir una credencial por sitio, nunca la lista entera'
|
|
55
57
|
},
|
|
56
58
|
confirmAdmin: (id) => `¿Dejar que ${id} conecte y quite dispositivos sin venir aquí?`,
|
|
57
59
|
capGiven: (n) => `Concedido: ${n}`,
|
|
@@ -341,13 +343,15 @@ const en = {
|
|
|
341
343
|
sign: 'Sign as you',
|
|
342
344
|
store: 'Save your data',
|
|
343
345
|
read: 'Read your data',
|
|
344
|
-
admin: 'Manage the profile'
|
|
346
|
+
admin: 'Manage the profile',
|
|
347
|
+
passwords: 'Ask for your passwords'
|
|
345
348
|
},
|
|
346
349
|
capHint: {
|
|
347
350
|
sign: 'use your identity across the ecosystem apps',
|
|
348
351
|
store: 'write to your vault (profile, content, sensitive data)',
|
|
349
352
|
read: 'see what you saved',
|
|
350
|
-
admin: 'connect and remove devices from that device, without coming here'
|
|
353
|
+
admin: 'connect and remove devices from that device, without coming here',
|
|
354
|
+
passwords: 'the manager: ask for one credential per site, never the whole list'
|
|
351
355
|
},
|
|
352
356
|
confirmAdmin: (id) => `Let ${id} connect and remove devices without coming here?`,
|
|
353
357
|
capGiven: (n) => `Granted: ${n}`,
|
package/src/vault.js
CHANGED
|
@@ -102,9 +102,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
102
102
|
return { approval: !!on }
|
|
103
103
|
}
|
|
104
104
|
// LA BÓVEDA DE CONTRASEÑAS: entradas y su llave, en el dir del perfil y cifradas en
|
|
105
|
-
// reposo como todo lo demás.
|
|
106
|
-
//
|
|
107
|
-
//
|
|
105
|
+
// reposo como todo lo demás. QUIÉN PUEDE PEDIR LO DICE EL ACTA (capacidad
|
|
106
|
+
// `passwords`), como cualquier otro permiso: aquí no hay una segunda lista de
|
|
107
|
+
// aparatos. Tenerla obligaba a acordarse de dos sitios al quitar un aparato, y era
|
|
108
|
+
// además un emparejamiento paralelo al del ecosistema.
|
|
108
109
|
const passwordsFile = path.join(dir, 'passwords.json')
|
|
109
110
|
const passwordsAtRest = atRestFor(dir)
|
|
110
111
|
const readPasswordsFile = () => {
|
|
@@ -118,13 +119,14 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
118
119
|
const passwordsStore = {
|
|
119
120
|
async get (k) { return readPasswordsFile()?.data?.[k] },
|
|
120
121
|
async set (k, v) {
|
|
121
|
-
const d = readPasswordsFile() || { v: 1, data: {}
|
|
122
|
+
const d = readPasswordsFile() || { v: 1, data: {} }
|
|
122
123
|
d.data = { ...(d.data || {}), [k]: v }
|
|
123
124
|
writePasswordsFile(d)
|
|
124
125
|
},
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
/** Los aparatos que el acta autoriza a pedir credenciales (`caps <ID> +contraseñas`). */
|
|
129
|
+
const passwordDevices = () => (actaCache?.members || []).filter((m) => Acta.memberCan(actaCache, m.pub, 'passwords'))
|
|
128
130
|
|
|
129
131
|
/**
|
|
130
132
|
* El acta vigente, en caché.
|
|
@@ -153,20 +155,10 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
153
155
|
}
|
|
154
156
|
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'])
|
|
155
157
|
const raw = new Uint8Array(await crypto.subtle.exportKey('raw', key))
|
|
156
|
-
writePasswordsFile({ ...(d || { v: 1, data: {} }), cek: Buffer.from(raw).toString('base64')
|
|
158
|
+
writePasswordsFile({ ...(d || { v: 1, data: {} }), cek: Buffer.from(raw).toString('base64') })
|
|
157
159
|
return key
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
/** Autorizar/retirar un aparato para pedir contraseñas (`dotrino-vault passwords`). */
|
|
161
|
-
async function setPasswordDevice (pub, { label = '', on = true } = {}) {
|
|
162
|
-
const d = readPasswordsFile() || { v: 1, data: {}, devices: [] }
|
|
163
|
-
const resto = (d.devices || []).filter((x) => x.pubkey !== pub)
|
|
164
|
-
if (on) resto.push({ pubkey: pub, label: String(label || '').slice(0, 60), ts: Date.now() })
|
|
165
|
-
writePasswordsFile({ ...d, devices: resto })
|
|
166
|
-
audit('passwords.device', { device: await deviceIdOf(pub).catch(() => null), on: !!on })
|
|
167
|
-
return { ok: true }
|
|
168
|
-
}
|
|
169
|
-
|
|
170
162
|
const approvalsSweeper = setInterval(() => { for (const g of approvals.sweep()) audit('secrets.expired', { id: g.id, ns: g.ns, device: g.deviceId }) }, 30 * 1000); approvalsSweeper.unref?.()
|
|
171
163
|
const secrets = openSecretsStore(dir, {
|
|
172
164
|
sealer: makeSealer(),
|
|
@@ -764,69 +756,94 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
764
756
|
// ----- LA BÓVEDA DE CONTRASEÑAS -----
|
|
765
757
|
//
|
|
766
758
|
// Se monta al final y envuelto: es una pieza opcional y un fallo suyo no puede tumbar
|
|
767
|
-
// la CA. Solo se levanta
|
|
759
|
+
// la CA. Solo se levanta cuando el acta autoriza a alguien — sin eso no hay a quién
|
|
768
760
|
// responder, y crear la llave por si acaso sería crear un secreto que nadie pidió.
|
|
761
|
+
//
|
|
762
|
+
// Y se levanta EN CUANTO pasa, no en el siguiente arranque: conceder el permiso ya es
|
|
763
|
+
// un acto explícito del dueño, y pedirle además que reinicie la bóveda es de las cosas
|
|
764
|
+
// que se olvidan y luego parecen un fallo del gestor.
|
|
769
765
|
let passwords = null
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
//
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const deviceId = await deviceIdOf(pubkey).catch(() => null)
|
|
793
|
-
const label = (actaCache?.members || []).find((m) => m.pub === pubkey)?.label || ''
|
|
794
|
-
const pend = approvals.request({ ns: 'passwords', device: pubkey, deviceId, label, ek: '' })
|
|
795
|
-
audit('passwords.pending', { device: deviceId, id: pend.id, op })
|
|
796
|
-
log(`[vault] passwords: ${deviceId || '????-????'} is waiting for approval (${pend.id})`)
|
|
797
|
-
const espera = new Promise((resolve) => {
|
|
798
|
-
waiters.set(pend.id, resolve)
|
|
799
|
-
// Si nadie contesta, vence solo: la promesa no se queda colgada para siempre
|
|
800
|
-
// y el aparato recibe un no en vez de esperar sin fin.
|
|
801
|
-
const t = setTimeout(() => {
|
|
802
|
-
if (waiters.delete(pend.id)) {
|
|
803
|
-
audit('passwords.expired', { device: deviceId, id: pend.id })
|
|
804
|
-
resolve(false)
|
|
805
|
-
}
|
|
806
|
-
}, PENDING_TTL_MS)
|
|
807
|
-
t.unref?.()
|
|
808
|
-
})
|
|
809
|
-
await notifyApprovers(pend, actaCache)
|
|
810
|
-
return espera
|
|
811
|
-
},
|
|
812
|
-
audit,
|
|
813
|
-
log,
|
|
814
|
-
}).start()
|
|
815
|
-
log('[vault] passwords: atendiendo peticiones de %d aparato(s)', passwordDevices().length)
|
|
816
|
-
// El código que se pega en la extensión o en la consola web. Lleva las DOS
|
|
817
|
-
// públicas: por la de firma enruta el proxio, a la de cifrado se le sella el
|
|
818
|
-
// contenido. Es público: no hay nada aquí que no pueda verse.
|
|
819
|
-
try {
|
|
820
|
-
const codigo = Buffer.from(JSON.stringify({
|
|
821
|
-
v: 1,
|
|
822
|
-
sign: await client.getPublicKey(),
|
|
823
|
-
enc: await identity.getEncryptionPubkey(),
|
|
824
|
-
})).toString('base64url')
|
|
825
|
-
log('[vault] passwords: enlaza un aparato con este código:\n%s', codigo)
|
|
826
|
-
} catch (e) {
|
|
827
|
-
log('[vault] passwords: no se pudo componer el código de enlace: %s', e.message)
|
|
766
|
+
/**
|
|
767
|
+
* EL SELLADO del gestor: la misma cripto de los sobres del ecosistema
|
|
768
|
+
* (`@dotrino/identity/content`), atada a la llave de cifrado de esta bóveda.
|
|
769
|
+
*
|
|
770
|
+
* Va aquí y no en el cliente del transporte porque el cliente del vault es UNO y lo
|
|
771
|
+
* comparte todo: el protocolo de la CA viaja en claro a propósito (un enrolamiento es
|
|
772
|
+
* público hasta que hay cert). Lo que se sella es lo del gestor, y por eso `isSealed`
|
|
773
|
+
* mira su marca y no otra.
|
|
774
|
+
*
|
|
775
|
+
* Sin esto la bóveda RECIBÍA los sobres y los tiraba —el cliente no tenía con qué
|
|
776
|
+
* abrirlos— y el aparato se quedaba esperando sin que nada lo dijera.
|
|
777
|
+
*/
|
|
778
|
+
const passwordSealing = {
|
|
779
|
+
async seal (msg, peerEncPub) {
|
|
780
|
+
if (!peerEncPub) throw Object.assign(new Error('no encryption key'), { code: 'unsealed' })
|
|
781
|
+
return {
|
|
782
|
+
app: 'passmanager',
|
|
783
|
+
// Destinatarios como OBJETOS y no como llaves sueltas: `encrypt` expande cada
|
|
784
|
+
// uno a todos los aparatos de esa persona, y una cadena a secas se le cae por
|
|
785
|
+
// el `continue` sin envolver nada — el sobre salía vacío y sin error.
|
|
786
|
+
sealed: await identity.encrypt([{ encryptionPubkey: peerEncPub }], JSON.stringify(msg)),
|
|
787
|
+
from: await identity.getEncryptionPubkey()
|
|
828
788
|
}
|
|
829
|
-
}
|
|
789
|
+
},
|
|
790
|
+
// `decrypt` devuelve `{ plaintext }`, no la cadena.
|
|
791
|
+
async open (env) { return JSON.parse((await identity.decrypt(env.from, null, env.sealed)).plaintext) },
|
|
792
|
+
isSealed: (m) => !!m && m.app === 'passmanager' && !!m.sealed
|
|
793
|
+
}
|
|
794
|
+
const startPasswordDesk = async () => {
|
|
795
|
+
if (passwords || !passwordDevices().length) return
|
|
796
|
+
const { createPasswordDesk } = await import('./passwords.js')
|
|
797
|
+
client.updateConfig({ sealing: passwordSealing })
|
|
798
|
+
passwords = createPasswordDesk({
|
|
799
|
+
client,
|
|
800
|
+
store: passwordsStore,
|
|
801
|
+
cek: await passwordsKey(),
|
|
802
|
+
// UNA sola condición, y es del ACTA: el aparato tiene la capacidad `passwords`.
|
|
803
|
+
// Quitársela —o quitar el aparato— le corta esto en la siguiente pasada, sin
|
|
804
|
+
// que haya una segunda lista que acordarse de tocar.
|
|
805
|
+
isAllowed: (pub) => !!actaCache && Acta.memberCan(actaCache, pub, 'passwords'),
|
|
806
|
+
encPubOf: (pub) => (actaCache?.members || []).find((m) => m.pub === pub)?.encPub || null,
|
|
807
|
+
needsApproval: (pub) => needsApproval(pub),
|
|
808
|
+
// El teléfono: se apunta el pedido, se le avisa y esta promesa espera su firma.
|
|
809
|
+
// Es el mismo camino que ya recorren los cajones de secretos.
|
|
810
|
+
approve: async ({ pubkey, op }) => {
|
|
811
|
+
const deviceId = await deviceIdOf(pubkey).catch(() => null)
|
|
812
|
+
const label = (actaCache?.members || []).find((m) => m.pub === pubkey)?.label || ''
|
|
813
|
+
const pend = approvals.request({ ns: 'passwords', device: pubkey, deviceId, label, ek: '' })
|
|
814
|
+
audit('passwords.pending', { device: deviceId, id: pend.id, op })
|
|
815
|
+
log(`[vault] passwords: ${deviceId || '????-????'} is waiting for approval (${pend.id})`)
|
|
816
|
+
const espera = new Promise((resolve) => {
|
|
817
|
+
waiters.set(pend.id, resolve)
|
|
818
|
+
// Si nadie contesta, vence solo: la promesa no se queda colgada para siempre
|
|
819
|
+
// y el aparato recibe un no en vez de esperar sin fin.
|
|
820
|
+
const t = setTimeout(() => {
|
|
821
|
+
if (waiters.delete(pend.id)) {
|
|
822
|
+
audit('passwords.expired', { device: deviceId, id: pend.id })
|
|
823
|
+
resolve(false)
|
|
824
|
+
}
|
|
825
|
+
}, PENDING_TTL_MS)
|
|
826
|
+
t.unref?.()
|
|
827
|
+
})
|
|
828
|
+
await notifyApprovers(pend, actaCache)
|
|
829
|
+
return espera
|
|
830
|
+
},
|
|
831
|
+
audit,
|
|
832
|
+
log,
|
|
833
|
+
}).start()
|
|
834
|
+
log('[vault] passwords: serving %d device(s)', passwordDevices().length)
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
// El acta ANTES de mirarla: es ella la que dice si hay a quién responder, y sin
|
|
838
|
+
// refrescarla primero el mostrador no se levantaba nunca en un arranque en frío.
|
|
839
|
+
await refreshActa()
|
|
840
|
+
const actaSweeper = setInterval(() => {
|
|
841
|
+
refreshActa().then(startPasswordDesk).catch(() => {})
|
|
842
|
+
}, 5000)
|
|
843
|
+
actaSweeper.unref?.()
|
|
844
|
+
if (passwordDevices().length) await startPasswordDesk()
|
|
845
|
+
// Ni código de enlace ni pegar nada: un gestor entra como cualquier otro aparato.
|
|
846
|
+
else log('[vault] passwords: no device may ask yet · pair one with `dotrino-vault pair --scope contrasenas`')
|
|
830
847
|
} catch (e) {
|
|
831
848
|
log('[vault] passwords: no se pudo levantar (%s); el resto sigue igual', e.message)
|
|
832
849
|
}
|
|
@@ -1730,7 +1747,6 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1730
1747
|
// La bóveda de contraseñas (`passwords.js`). Aquí SÍ se lista: es donde está la
|
|
1731
1748
|
// llave. Lo que no puede es listarla un aparato.
|
|
1732
1749
|
passwordDevices,
|
|
1733
|
-
setPasswordDevice,
|
|
1734
1750
|
passwordsVault: () => passwords?.vault || null,
|
|
1735
1751
|
listApprovals: () => approvals.list(),
|
|
1736
1752
|
// Aprobar desde el PC avisa igual que aprobar a distancia: el resto de tus
|