@dotrino/vaultd 0.98.0 → 0.105.2
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/admin.js +29 -3
- package/lib/src/index.js +4 -1
- package/lib/src/localdesk.js +96 -0
- package/lib/src/service.js +95 -18
- package/package.json +7 -3
- package/src/approvals.js +7 -2
- package/src/client.js +4 -1
- package/src/ctl.js +19 -2
- package/src/daemon.js +4 -0
- package/src/replica.js +34 -1
- package/src/secretsStore.js +136 -0
- package/src/threadStore.js +10 -0
- package/src/transport.js +27 -4
- package/src/tui/app.js +42 -2
- package/src/tui/i18n.js +52 -35
- package/src/vault.js +211 -14
- package/src/vaultControl.js +24 -0
package/src/vault.js
CHANGED
|
@@ -26,7 +26,7 @@ import { startSealersPublisher } from './sealers.js'
|
|
|
26
26
|
import { assertKeyOwnsDir } from './keyowner.js'
|
|
27
27
|
import { openStore } from './store.js'
|
|
28
28
|
import { openThreadStore, STORE_READ_METHODS, PROFILE_EDIT_METHODS } from './threadStore.js'
|
|
29
|
-
import { openSecretsStore, assertVar, RECOVERY as RECOVERY_WRAP } from './secretsStore.js'
|
|
29
|
+
import { openSecretsStore, assertVar, RECOVERY as RECOVERY_WRAP, PROFILE_OWNER } from './secretsStore.js'
|
|
30
30
|
// `PENDING_TTL_MS` se usa abajo, al esperar la firma del aprobador: sin importarlo, esa
|
|
31
31
|
// espera reventaba con un ReferenceError y la aprobación del mostrador de contraseñas no
|
|
32
32
|
// llegaba a existir. Solo se veía por ese camino —el único que lo usa—, y no había prueba
|
|
@@ -36,7 +36,9 @@ import { makeSealer } from './sealer.js'
|
|
|
36
36
|
import { openSealKeys } from './sealKey.js'
|
|
37
37
|
import { openCommKey, COMM_CN, COMM_CAPS } from './commKey.js'
|
|
38
38
|
import { seal } from '../lib/src/sealed.js'
|
|
39
|
+
import { localSocketPath, socketDir } from '../lib/src/localdesk.js'
|
|
39
40
|
import { dataDir, ensureDir } from './paths.js'
|
|
41
|
+
import net from 'node:net'
|
|
40
42
|
import { atRestFor, kekFor, migrateFile, encryptText, decryptText } from './atrest.js'
|
|
41
43
|
import { MSG, SCOPE, secretsScope, isValidSecretsNs } from './protocol.js'
|
|
42
44
|
|
|
@@ -321,15 +323,26 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
321
323
|
* Así que una respuesta que no quepa no se manda: se sustituye por un error, que sí cabe.
|
|
322
324
|
* Una bóveda que dice «no cabe» se arregla; una muda no se puede ni diagnosticar.
|
|
323
325
|
*/
|
|
326
|
+
/**
|
|
327
|
+
* QUIÉN CONTESTA A QUIÉN. Por defecto el proxio; un remitente LOCAL (el socket de esta
|
|
328
|
+
* máquina) se apunta aquí con su propia forma de contestar.
|
|
329
|
+
*
|
|
330
|
+
* Que haya dos transportes no cambia NADA de quién puede pedir qué: la autorización sale
|
|
331
|
+
* del certificado y del acta, igual por los dos caminos. Alcanzar el socket no autoriza
|
|
332
|
+
* nada — es la misma puerta, con el camino más corto.
|
|
333
|
+
*/
|
|
334
|
+
const canales = new Map()
|
|
335
|
+
|
|
324
336
|
const reply = (to, obj) => {
|
|
325
337
|
try {
|
|
338
|
+
const enviar = canales.get(to) || ((o) => client.send(to, o))
|
|
326
339
|
const bytes = Buffer.byteLength(JSON.stringify(obj))
|
|
327
340
|
if (bytes > MAX_REPLY_BYTES) {
|
|
328
341
|
log(`[vault] the reply to ${obj?.type} does not fit (${bytes} bytes > ${MAX_REPLY_BYTES}): sending an error instead of killing the connection`)
|
|
329
342
|
audit('reply-too-big', { type: obj?.type || null, bytes })
|
|
330
|
-
return
|
|
343
|
+
return enviar({ type: MSG.ERROR, error: `reply too big (${bytes} bytes): ask for less at a time` })
|
|
331
344
|
}
|
|
332
|
-
|
|
345
|
+
enviar(obj)
|
|
333
346
|
} catch (e) { log('[vault] could not reply:', e.message) }
|
|
334
347
|
}
|
|
335
348
|
|
|
@@ -610,6 +623,69 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
610
623
|
|
|
611
624
|
// Store de hilos+aperturas (Fase 3): escrituras requieren vault:store; lecturas
|
|
612
625
|
// aceptan vault:store o vault:read. Cada op va firmada por D + cert (cadena D←maestra).
|
|
626
|
+
/**
|
|
627
|
+
* LOS DATOS DEL PERFIL (`docs/datos-del-perfil.md`). Tres métodos y ninguno más:
|
|
628
|
+
*
|
|
629
|
+
* · `profilePut` — guarda un dato. Privado = sobre ya sellado; público = en claro.
|
|
630
|
+
* · `profileBundle` — los sobres de ESTE aparato, para que arme su perfil.
|
|
631
|
+
* · `profilePublic` — lo público en claro, que puede pedir cualquiera.
|
|
632
|
+
*
|
|
633
|
+
* Lo que NO se comprueba aquí: el candado. Un sobre sellado no lo puede leer ni fabricar
|
|
634
|
+
* la bóveda, así que guardarlo no es una decisión suya y no necesita su maestra. Ese era
|
|
635
|
+
* el motivo real por el que editar el perfil exigía abrirla, y es el síntoma que abrió
|
|
636
|
+
* todo esto: se cerraba sola a los cinco minutos y editar dejaba de funcionar.
|
|
637
|
+
*/
|
|
638
|
+
const PROFILE_METHODS = new Set(['profilePut', 'profileBundle', 'profilePublic', 'profileRecipients'])
|
|
639
|
+
|
|
640
|
+
async function handleProfile (from, p, d) {
|
|
641
|
+
// LO PÚBLICO NO PIDE NADA. Es lo que ve quien pregunta desde fuera y no tiene ninguna
|
|
642
|
+
// llave tuya: pedir un papel para leerlo sería contradecir lo que significa «público».
|
|
643
|
+
// Va firmado, así que quien lo recibe comprueba que es tuyo sin fiarse de nadie.
|
|
644
|
+
if (d.method === 'profilePublic') {
|
|
645
|
+
return reply(from, { type: MSG.STORE_RESULT, method: d.method, result: secrets.profilePublic() })
|
|
646
|
+
}
|
|
647
|
+
// A QUIÉN HAY QUE ENVOLVERLE. Lo pide el aparato ANTES de sellar, porque sellar lo hace
|
|
648
|
+
// él: envolver solo necesita públicas, así que puede hacerlo un navegador sin que
|
|
649
|
+
// ninguna privada ande suelta. Sale de aquí y no de una lista suya para que conceder
|
|
650
|
+
// `firma` a un aparato nuevo baste — sin que nadie tenga que acordarse de nada.
|
|
651
|
+
if (d.method === 'profileRecipients') {
|
|
652
|
+
const members = await recipientsOf(PROFILE_OWNER)
|
|
653
|
+
return reply(from, {
|
|
654
|
+
type: MSG.STORE_RESULT,
|
|
655
|
+
method: d.method,
|
|
656
|
+
result: { recoveryPub: secrets.recoveryPub(), members: members.map((m) => ({ pub: m.pub, encPub: m.encPub })) }
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const revoked = await revocationSet()
|
|
661
|
+
// `vault:sign` — hablar por ti. No `vault:store`, que es guardar cosas.
|
|
662
|
+
const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.SIGN, ...(await contextoActa()), revoked })
|
|
663
|
+
if (!chk.ok) return denyChain(from, chk, p, 'profile')
|
|
664
|
+
if (!await actaAllows(from, chk, SCOPE.SIGN, 'profile')) return
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
if (d.method === 'profileBundle') {
|
|
668
|
+
const b = secrets.profileBundleFor(chk.device)
|
|
669
|
+
return reply(from, { type: MSG.STORE_RESULT, method: d.method, result: b })
|
|
670
|
+
}
|
|
671
|
+
// profilePut
|
|
672
|
+
const { key, cls, sobre, value } = d.args || {}
|
|
673
|
+
const quien = await deviceIdOf(chk.device).catch(() => null)
|
|
674
|
+
let r
|
|
675
|
+
if (cls === 'public') {
|
|
676
|
+
r = await secrets.profilePutPublic(key, value, { by: quien })
|
|
677
|
+
} else {
|
|
678
|
+
r = await secrets.putSealed(PROFILE_OWNER, key, sobre, { by: quien })
|
|
679
|
+
}
|
|
680
|
+
audit('profile.put', { key, cls: cls || 'private', by: quien, gen: r?.gen ?? null })
|
|
681
|
+
// Y a los demás aparatos, que si no se enteran al arrancar y vuelve la divergencia.
|
|
682
|
+
await notifyMembers('profile', { key, by: quien })
|
|
683
|
+
return reply(from, { type: MSG.STORE_RESULT, method: d.method, result: r })
|
|
684
|
+
} catch (e) {
|
|
685
|
+
return reply(from, { type: MSG.ERROR, error: 'profile: ' + e.message })
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
613
689
|
async function handleStore (from, p) {
|
|
614
690
|
const d = p.data
|
|
615
691
|
// `Object.hasOwn` y no `threads.methods[d.method]`: con la comprobación laxa,
|
|
@@ -619,9 +695,19 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
619
695
|
return reply(from, { type: MSG.ERROR, error: 'store: invalid method' })
|
|
620
696
|
}
|
|
621
697
|
if (!isFresh(d)) return staleReply(from)
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
698
|
+
// EL PERFIL VA POR OTRO CAMINO, y por eso se atiende antes que nada
|
|
699
|
+
// (`docs/datos-del-perfil.md`). Dos diferencias, y las dos son el arreglo:
|
|
700
|
+
//
|
|
701
|
+
// · **No hace falta abrir la bóveda.** Lo que llega es un sobre ya sellado y
|
|
702
|
+
// firmado, que ella no puede leer ni falsificar: aceptarlo no es una decisión
|
|
703
|
+
// suya. El candado estaba ahí porque guardaba el perfil en claro; ya no.
|
|
704
|
+
// · **Lo gatea `firma`, no `guarda`.** Escribir tu identidad es hablar por ti, no
|
|
705
|
+
// guardar un archivo en tu bóveda (dueño, 2026-09-03). El proxio tiene `guarda`
|
|
706
|
+
// para lo suyo y no debe poder cambiar quién eres.
|
|
707
|
+
if (PROFILE_METHODS.has(d.method)) return await handleProfile(from, p, d)
|
|
708
|
+
// CANDADO del perfil (contraseña opcional): solo frena EDITAR el perfil heredado
|
|
709
|
+
// (`profileSet`, el `me` plano). Un dispositivo enrolado puede seguir firmando,
|
|
710
|
+
// leyendo y guardando contenido.
|
|
625
711
|
if (PROFILE_EDIT_METHODS.has(d.method) && isLocked()) {
|
|
626
712
|
audit('rejected', { what: 'store', method: d.method, reason: 'locked' })
|
|
627
713
|
return reply(from, { type: MSG.ERROR, error: 'profile locked: unlock it on the vault machine (dotrino-vault unlock) to edit it' })
|
|
@@ -981,7 +1067,11 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
981
1067
|
if (!publicOnly && needsApproval(chk.device, record)) {
|
|
982
1068
|
const deviceId = await deviceIdOf(chk.device).catch(() => null)
|
|
983
1069
|
const label = (record?.members || []).find((m) => m.pub === chk.device)?.label || ''
|
|
984
|
-
|
|
1070
|
+
// `from` SE GUARDA: la respuesta llega más tarde —cuando alguien apruebe— y tiene que
|
|
1071
|
+
// volver POR DONDE VINO. Si la pregunta entró por el mostrador local, mandarla por el
|
|
1072
|
+
// proxio la deja en el vacío: quien preguntó está escuchando en el socket, no ahí.
|
|
1073
|
+
// Lo cazó el E2E de aprobación, que se quedó esperando cinco minutos.
|
|
1074
|
+
const pend = approvals.request({ ns, device: chk.device, deviceId, label, ek: p.data.ek, from })
|
|
985
1075
|
audit('secrets.pending', { device: deviceId, ns, id: pend.id })
|
|
986
1076
|
log(`[vault] ${ns}: ${deviceId || '????-????'} is waiting for approval (${pend.id})`)
|
|
987
1077
|
const body = { op: 'secrets.pending', ns, id: pend.id, exp: pend.exp, ts: Date.now() }
|
|
@@ -1093,8 +1183,12 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1093
1183
|
if (op === 'deny') {
|
|
1094
1184
|
audit('secrets.denied', { device: pend.deviceId, ns: pend.ns, id, by })
|
|
1095
1185
|
log(`[vault] ${pend.ns}: request of ${pend.deviceId} DENIED by ${by}`)
|
|
1096
|
-
try {
|
|
1097
|
-
|
|
1186
|
+
try {
|
|
1187
|
+
const err = { type: MSG.ERROR, error: `unauthorized: denied — the "${pend.ns}" request was denied from ${by}` }
|
|
1188
|
+
const porLocal = pend.from && canales.get(pend.from)
|
|
1189
|
+
if (porLocal) porLocal(err)
|
|
1190
|
+
else client.sendByPubkey(pend.device, err)
|
|
1191
|
+
} catch (e) { log(`[vault] ${pend.ns}: could not tell ${pend.deviceId} it was denied: ${e.message}`) }
|
|
1098
1192
|
return answer({ op: 'deny.result', id, ok: true })
|
|
1099
1193
|
}
|
|
1100
1194
|
if (op !== 'approve') return reply(from, { type: MSG.ERROR, error: 'approval: unknown op' })
|
|
@@ -1108,8 +1202,13 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1108
1202
|
// EL FALLO NO SE TRAGA. Tragarlo era lo peor de este mostrador: la bóveda apuntaba
|
|
1109
1203
|
// «aprobado», el que pedía no recibía nada, y no quedaba una sola línea que mirar —
|
|
1110
1204
|
// así que el dueño aprobaba una y otra vez creyendo que el teléfono no llegaba.
|
|
1111
|
-
|
|
1112
|
-
|
|
1205
|
+
// Por el canal por el que preguntó. Si fue el proxio, `sendByPubkey`: así lo recoge al
|
|
1206
|
+
// volver aunque ahora esté apagado. Si fue el socket local, por el socket.
|
|
1207
|
+
try {
|
|
1208
|
+
const porLocal = pend.from && canales.get(pend.from)
|
|
1209
|
+
if (porLocal) porLocal({ type: MSG.SECRETS_RESULT, ...res })
|
|
1210
|
+
else client.sendByPubkey(pend.device, { type: MSG.SECRETS_RESULT, ...res })
|
|
1211
|
+
} catch (e) { log(`[vault] ${pend.ns}: approved, but the reply did not reach ${pend.deviceId}: ${e.message}`) }
|
|
1113
1212
|
return answer({ op: 'approve.result', id, ok: true })
|
|
1114
1213
|
}
|
|
1115
1214
|
|
|
@@ -1144,7 +1243,20 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1144
1243
|
} catch (e) { log('[vault] could not notify approvers:', e.message) }
|
|
1145
1244
|
}
|
|
1146
1245
|
|
|
1147
|
-
client.on('message',
|
|
1246
|
+
client.on('message', (from, payload) => onMessage(from, payload))
|
|
1247
|
+
|
|
1248
|
+
// EL MOSTRADOR LOCAL, siempre que se pueda. Si no se puede abrir (un sistema sin sockets
|
|
1249
|
+
// de archivo, un directorio de solo lectura) se dice y se sigue: los servicios usan el
|
|
1250
|
+
// proxio como hasta ahora. Lo que NO se hace es callarlo — un atajo que no está y nadie
|
|
1251
|
+
// lo dice es media hora buscando por qué algo va lento.
|
|
1252
|
+
// El socket se nombra por la LLAVE de esta bóveda: una máquina puede tener varias, y un
|
|
1253
|
+
// servicio solo conoce la maestra que lleva pineada. Así los dos lados calculan la misma
|
|
1254
|
+
// ruta sin mirar ningún índice.
|
|
1255
|
+
if (identity.me?.publickey) {
|
|
1256
|
+
abrirMostradorLocal(localSocketPath((await pubkeyId(identity.me.publickey)).slice(0, 16)))
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
async function onMessage (from, payload) {
|
|
1148
1260
|
if (!payload || typeof payload !== 'object') return
|
|
1149
1261
|
try {
|
|
1150
1262
|
if (payload.type === MSG.HELLO) return desk.handleHello(from, payload)
|
|
@@ -1165,7 +1277,60 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1165
1277
|
} catch (e) {
|
|
1166
1278
|
reply(from, { type: MSG.ERROR, error: e.message })
|
|
1167
1279
|
}
|
|
1168
|
-
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* EL MOSTRADOR LOCAL: el mismo protocolo, sin salir de la máquina.
|
|
1284
|
+
*
|
|
1285
|
+
* Un servicio en ESTA máquina hablaba con la bóveda dando la vuelta por
|
|
1286
|
+
* `proxy.dotrino.com`: sale a internet, vuelve, y si el proxio tiene un mal momento el
|
|
1287
|
+
* arranque se retrasa cinco segundos por nada. Absurdo cuando los dos procesos comparten
|
|
1288
|
+
* disco (dueño, 2026-09-03: «estando en la misma máquina debería ser inmediato»).
|
|
1289
|
+
*
|
|
1290
|
+
* Es EL MISMO PROTOCOLO y el mismo enrutador: llega `{type, data, signature, cert}` y se
|
|
1291
|
+
* comprueba igual, contra el certificado y contra el acta. **Alcanzar el socket no
|
|
1292
|
+
* autoriza nada** — es la misma puerta por un camino más corto, no una puerta de atrás.
|
|
1293
|
+
* Por eso no hay un solo `if` de «viene de local» en ningún handler, y no debe haberlo.
|
|
1294
|
+
*
|
|
1295
|
+
* Y no es un puerto: es un socket de archivo, en modo 0600 y bajo el directorio del
|
|
1296
|
+
* usuario. No se alcanza desde otra máquina ni desde otro usuario, así que la promesa
|
|
1297
|
+
* del `docker-compose` —«la bóveda no escucha nada»— sigue siendo cierta donde importa.
|
|
1298
|
+
*/
|
|
1299
|
+
function abrirMostradorLocal (socketPath) {
|
|
1300
|
+
let n = 0
|
|
1301
|
+
const server = net.createServer((sock) => {
|
|
1302
|
+
const id = 'local:' + (++n)
|
|
1303
|
+
let buf = ''
|
|
1304
|
+
const enviar = (obj) => { try { sock.write(JSON.stringify(obj) + '\n') } catch (_) {} }
|
|
1305
|
+
canales.set(id, enviar)
|
|
1306
|
+
sock.on('data', (chunk) => {
|
|
1307
|
+
buf += chunk
|
|
1308
|
+
// UN TOPE, y por lo mismo que en las respuestas: sin él, quien se conecte puede
|
|
1309
|
+
// hacer crecer este buffer hasta tumbar la bóveda mandando y sin saltos de línea.
|
|
1310
|
+
if (buf.length > MAX_REPLY_BYTES) { buf = ''; enviar({ type: MSG.ERROR, error: 'request too big' }); return }
|
|
1311
|
+
let i
|
|
1312
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
1313
|
+
const linea = buf.slice(0, i); buf = buf.slice(i + 1)
|
|
1314
|
+
if (!linea.trim()) continue
|
|
1315
|
+
let p = null
|
|
1316
|
+
try { p = JSON.parse(linea) } catch (_) { enviar({ type: MSG.ERROR, error: 'malformed request' }); continue }
|
|
1317
|
+
onMessage(id, p).catch((e) => enviar({ type: MSG.ERROR, error: e.message }))
|
|
1318
|
+
}
|
|
1319
|
+
})
|
|
1320
|
+
const cerrar = () => canales.delete(id)
|
|
1321
|
+
sock.on('close', cerrar)
|
|
1322
|
+
sock.on('error', cerrar)
|
|
1323
|
+
})
|
|
1324
|
+
server.on('error', (e) => log('[vault] the local desk could not open (' + e.message + '): services will use the proxy'))
|
|
1325
|
+
try { fs.mkdirSync(socketDir(), { recursive: true, mode: 0o700 }) } catch (_) {}
|
|
1326
|
+
try { fs.rmSync(socketPath, { force: true }) } catch (_) {}
|
|
1327
|
+
server.listen(socketPath, () => {
|
|
1328
|
+
try { fs.chmodSync(socketPath, 0o600) } catch (_) {}
|
|
1329
|
+
log('[vault] local desk at ' + socketPath + ' (same machine, no round trip)')
|
|
1330
|
+
})
|
|
1331
|
+
server.unref?.()
|
|
1332
|
+
return server
|
|
1333
|
+
}
|
|
1169
1334
|
|
|
1170
1335
|
/**
|
|
1171
1336
|
* AVISO DE OTRA BÓVEDA de la misma cuenta (multivault). Trae el acta nueva y aquí se
|
|
@@ -1897,6 +2062,19 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1897
2062
|
// abrirse desde un navegador. Lo que cambia es que «pública» ya sí quiere decir algo
|
|
1898
2063
|
// para quien administra — puede leerla— además de que se despacha sin aprobación.
|
|
1899
2064
|
const admins = isPublic ? await adminDevices() : []
|
|
2065
|
+
// EL PERFIL SE LE ENVUELVE A QUIEN PUEDE HABLAR POR TI (`docs/datos-del-perfil.md`).
|
|
2066
|
+
//
|
|
2067
|
+
// Va explícito y antes que nada porque si no cae en la rama de «cajón sin dueño» y
|
|
2068
|
+
// acabaría envuelto solo para quien ADMINISTRA — que no es lo que se decidió: leer y
|
|
2069
|
+
// escribir tu identidad es `firma`, y no todos los que firman administran. Un teléfono
|
|
2070
|
+
// normal firma y no administra, y tiene que ver tu perfil.
|
|
2071
|
+
if (owner === PROFILE_OWNER) {
|
|
2072
|
+
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
2073
|
+
const firman = (record?.members || [])
|
|
2074
|
+
.filter((m) => m.encPub && Acta.memberCan(record, m.pub, 'sign'))
|
|
2075
|
+
.map((m) => ({ pub: m.pub, encPub: m.encPub }))
|
|
2076
|
+
return conCoselladores(dedup(firman), co)
|
|
2077
|
+
}
|
|
1900
2078
|
if (owner.startsWith('ns:')) {
|
|
1901
2079
|
const owned = await nsMembers(owner.slice(3))
|
|
1902
2080
|
// Sin dueño (un cajón personal, o uno cuyo servicio ya no está) entra quien administra
|
|
@@ -2452,6 +2630,25 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2452
2630
|
}
|
|
2453
2631
|
async function deleteSecret (ns, key) { const ok = await secrets.delete(ns, key); if (ok) { audit('secret.rm', { ns, key }); scheduleNotice(ns) } return ok }
|
|
2454
2632
|
|
|
2633
|
+
/**
|
|
2634
|
+
* RENOMBRAR una variable. Para el servicio que la lee es un cambio de configuración como
|
|
2635
|
+
* cualquier otro —la de antes desaparece y aparece otra—, así que se avisa igual: si no,
|
|
2636
|
+
* seguiría arrancando con el nombre viejo hasta el siguiente reinicio.
|
|
2637
|
+
*/
|
|
2638
|
+
async function renameSecret (ns, from, to) {
|
|
2639
|
+
const r = await secrets.rename(`ns:${ns}`, from, to)
|
|
2640
|
+
if (r?.renamed) { audit('secret.rename', { ns, from, to }); scheduleNotice(ns) }
|
|
2641
|
+
return r
|
|
2642
|
+
}
|
|
2643
|
+
async function renameDeviceSecret (pub, from, to) {
|
|
2644
|
+
const r = await secrets.rename(`dev:${pub}`, from, to)
|
|
2645
|
+
if (r?.renamed) {
|
|
2646
|
+
audit('secret.rename', { device: await deviceIdOf(pub).catch(() => null), from, to, scope: 'device' })
|
|
2647
|
+
scheduleDeviceNotice(pub)
|
|
2648
|
+
}
|
|
2649
|
+
return r
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2455
2652
|
/**
|
|
2456
2653
|
* CARGAR CONFIGURACIÓN ES UNA TRANSACCIÓN: muchas variables, UN aviso.
|
|
2457
2654
|
*
|
|
@@ -2738,7 +2935,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2738
2935
|
// El mostrador que atiende a la consola remota. Se expone para poder probar la
|
|
2739
2936
|
// frontera de verdad (que el valor de una privada no salga ni dentro del sobre).
|
|
2740
2937
|
vars: varsDesk,
|
|
2741
|
-
setSecret, deleteSecret, listSecrets, setSecretVisibility, openSecrets,
|
|
2938
|
+
setSecret, deleteSecret, renameSecret, renameDeviceSecret, listSecrets, setSecretVisibility, openSecrets,
|
|
2742
2939
|
// Sella un `secrets.json` v3 entero. Es el punto de no retorno del despliegue y
|
|
2743
2940
|
// por eso es una operación con nombre propio, no algo que ocurra de refilón al
|
|
2744
2941
|
// desbloquear: deja `secrets.json.v3.bak` para poder volver.
|
package/src/vaultControl.js
CHANGED
|
@@ -513,6 +513,30 @@ export function deleteSecret (ns, key, profile) {
|
|
|
513
513
|
})
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
/**
|
|
517
|
+
* Renombra una variable: el mismo sobre con otro nombre. No pide la frase — lo único que
|
|
518
|
+
* hay que rehacer es la firma, y de eso se encarga la llave de sellado.
|
|
519
|
+
*
|
|
520
|
+
* Se comprueban LAS DOS cosas: que la nueva esté y que la vieja ya no. Mirar solo una
|
|
521
|
+
* dejaría pasar una copia a medias, que es peor que un fallo.
|
|
522
|
+
*/
|
|
523
|
+
export function renameSecret (ns, key, to, profile) {
|
|
524
|
+
return secretOp({ op: 'rename', ns, key, to }, profile, (out) => {
|
|
525
|
+
if (!has(out.ns[ns], to) || has(out.ns[ns], key)) {
|
|
526
|
+
throw coded('the daemon did not rename the variable (check the service logs)', 'NOT_RENAMED')
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Lo mismo para una variable de UN aparato. */
|
|
532
|
+
export function renameDeviceSecret (pub, key, to, profile) {
|
|
533
|
+
return secretOp({ op: 'dev-rename', pub, key, to }, profile, (out) => {
|
|
534
|
+
if (!has(keysOf(out, pub), to) || has(keysOf(out, pub), key)) {
|
|
535
|
+
throw coded('the daemon did not rename the variable (check the service logs)', 'NOT_RENAMED')
|
|
536
|
+
}
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
516
540
|
/**
|
|
517
541
|
* Cambia SOLO quién puede ver el valor: `public` deja que la consola remota lo vea,
|
|
518
542
|
* `private` lo encierra en esta máquina. No toca el valor (ni hace falta conocerlo).
|