@dotrino/vaultd 0.105.3 → 0.106.1
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 +1 -1
- package/src/replica.js +52 -9
- package/src/vault.js +46 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.106.1",
|
|
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": {
|
package/src/replica.js
CHANGED
|
@@ -24,9 +24,10 @@ import fs from 'node:fs'
|
|
|
24
24
|
import path from 'node:path'
|
|
25
25
|
import os from 'node:os'
|
|
26
26
|
import { WebSocketProxyClient } from '@dotrino/proxy-client'
|
|
27
|
-
import { signWithDevice, verifyDeviceSig, pubkeyId } from '@dotrino/identity/capabilities'
|
|
28
|
-
import { verifyActa, memberCan, sealKeyAt } from '@dotrino/identity/acta'
|
|
29
|
-
import { MSG } from '../lib/src/protocol.js'
|
|
27
|
+
import { signWithDevice, verifyDeviceSig, verifyChain, pubkeyId } from '@dotrino/identity/capabilities'
|
|
28
|
+
import { verifyActa, memberCan, memberCanReadSecrets, sealKeyAt, sealersOf } from '@dotrino/identity/acta'
|
|
29
|
+
import { MSG, secretsScope, isValidSecretsNs } from '../lib/src/protocol.js'
|
|
30
|
+
import { FRESH_WINDOW_MS } from '../lib/src/enroll.js'
|
|
30
31
|
import { seal } from '../lib/src/sealed.js'
|
|
31
32
|
import { atRestFor } from './atrest.js'
|
|
32
33
|
import { readJson, writeJson } from './paths.js'
|
|
@@ -169,17 +170,59 @@ export async function runReplica ({ dir = replicaDir(), proxyUrl, log = console.
|
|
|
169
170
|
* quien pregunta y firmado con NUESTRA llave — que es lo que el cliente acepta cuando el
|
|
170
171
|
* acta nos reconoce `replica` (`verifyResponder` de `@dotrino/vault`).
|
|
171
172
|
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
173
|
+
* SE COMPRUEBA QUIÉN PIDE, con lo mismo que comprueba la bóveda. Durante un día esto no
|
|
174
|
+
* miraba nada: bastaba con nombrar un cajón y una llave de aparato para llevarse el sobre
|
|
175
|
+
* y, dentro de él, EL ACTA — que es el inventario de máquinas del dueño, y que la bóveda
|
|
176
|
+
* no le enseña ni a un servicio de la casa. Un extraño con una llave recién hecha lo
|
|
177
|
+
* sacaba entero (`smoke/replica.mjs`, «a un DESCONOCIDO…»).
|
|
178
|
+
*
|
|
179
|
+
* El argumento de entonces era que el sobre va cerrado a su destinatario, así que a quien
|
|
180
|
+
* no le toque le llega algo que no puede abrir. Sigue siendo verdad y sigue siendo la
|
|
181
|
+
* segunda línea — pero no es una puerta: repartir cifrado a desconocidos regala el acta,
|
|
182
|
+
* deja el texto cifrado en manos de cualquiera para siempre, y no cuesta nada evitarlo.
|
|
183
|
+
* La petición YA viaja firmada y con papel desde `fetchSecrets`; aquí solo se miraban.
|
|
184
|
+
*
|
|
185
|
+
* Lo que sigue sin decidirse aquí, y eso sí es a propósito: NADA sobre el contenido. El
|
|
186
|
+
* replicador no abre, no filtra por clave y no sabe qué entrega. Comprobar quién llama no
|
|
187
|
+
* lo convierte en alguien de quien haya que fiarse: si mintiera diciendo que sí, el sobre
|
|
188
|
+
* sigue sin abrirse; si mintiera diciendo que no, es una caída, que es lo que un
|
|
189
|
+
* replicador puede costar.
|
|
190
|
+
*
|
|
191
|
+
* `revoked` no se le pasa a `verifyChain` porque aquí no hay registro de revocaciones —
|
|
192
|
+
* y no hace falta: quitar un aparato es sacarlo del acta (`docs/acta-de-perfil.md`), y de
|
|
193
|
+
* eso se ocupa `memberCanReadSecrets` con el acta de abajo.
|
|
176
194
|
*/
|
|
177
195
|
async function onSecrets (from, p) {
|
|
178
196
|
const ns = p?.data?.ns
|
|
179
197
|
const ek = p?.data?.ek
|
|
180
|
-
|
|
181
|
-
if (typeof ns !== 'string' || !ek || !quien) return
|
|
198
|
+
if (!isValidSecretsNs(ns) || typeof ek !== 'string') return
|
|
182
199
|
if (!store.acta) return client.send(from, { type: MSG.ERROR, error: 'replica: no record yet' })
|
|
200
|
+
const no = (error) => client.send(from, { type: MSG.ERROR, error })
|
|
201
|
+
const acta = store.acta
|
|
202
|
+
|
|
203
|
+
if (typeof p?.data?.ts !== 'number' || Math.abs(Date.now() - p.data.ts) > FRESH_WINDOW_MS) {
|
|
204
|
+
log('[replica] a secrets request arrived outside the freshness window: refused')
|
|
205
|
+
return no('replica: stale request')
|
|
206
|
+
}
|
|
207
|
+
const chk = await verifyChain({
|
|
208
|
+
data: p.data,
|
|
209
|
+
signature: p.signature,
|
|
210
|
+
cert: p.cert,
|
|
211
|
+
expectedScope: secretsScope(ns),
|
|
212
|
+
actaSeq: acta.seq,
|
|
213
|
+
sealers: sealersOf(acta)
|
|
214
|
+
})
|
|
215
|
+
if (!chk.ok) {
|
|
216
|
+
log(`[replica] a secrets request for "${ns}" does not check out (${chk.reason}): refused`)
|
|
217
|
+
return no('unauthorized: ' + chk.reason)
|
|
218
|
+
}
|
|
219
|
+
// EL ACTA MANDA, igual que en la bóveda: el papel dice qué se emitió, el acta dice qué
|
|
220
|
+
// vale hoy. Un aparato al que le quitaron el cajón trae un papel intacto.
|
|
221
|
+
if (!memberCanReadSecrets(acta, chk.device, ns)) {
|
|
222
|
+
log(`[replica] the record does not let ${(await pubkeyId(chk.device)).slice(0, 8)} read "${ns}": refused`)
|
|
223
|
+
return no(`unauthorized: cn — the record does not recognise this member as the "${ns}" service`)
|
|
224
|
+
}
|
|
225
|
+
const quien = chk.device
|
|
183
226
|
const b = store.bundleFor(ns, quien)
|
|
184
227
|
if (!b) {
|
|
185
228
|
log(`[replica] asked for "${ns}" and has nothing stored for that device`)
|
package/src/vault.js
CHANGED
|
@@ -644,10 +644,24 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
644
644
|
if (d.method === 'profilePublic') {
|
|
645
645
|
return reply(from, { type: MSG.STORE_RESULT, method: d.method, result: secrets.profilePublic() })
|
|
646
646
|
}
|
|
647
|
+
const revoked = await revocationSet()
|
|
648
|
+
// `vault:sign` — hablar por ti. No `vault:store`, que es guardar cosas.
|
|
649
|
+
const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, expectedScope: SCOPE.SIGN, ...(await contextoActa()), revoked })
|
|
650
|
+
if (!chk.ok) return denyChain(from, chk, p, 'profile')
|
|
651
|
+
if (!await actaAllows(from, chk, SCOPE.SIGN, 'profile')) return
|
|
652
|
+
|
|
647
653
|
// A QUIÉN HAY QUE ENVOLVERLE. Lo pide el aparato ANTES de sellar, porque sellar lo hace
|
|
648
654
|
// él: envolver solo necesita públicas, así que puede hacerlo un navegador sin que
|
|
649
655
|
// ninguna privada ande suelta. Sale de aquí y no de una lista suya para que conceder
|
|
650
656
|
// `firma` a un aparato nuevo baste — sin que nadie tenga que acordarse de nada.
|
|
657
|
+
//
|
|
658
|
+
// VA DETRÁS DEL CONTROL, y estuvo delante. Que solo haga falta material PÚBLICO para
|
|
659
|
+
// envolver no significa que la lista sea pública: lo que devuelve es el INVENTARIO
|
|
660
|
+
// —cuántas máquinas tienes, sus llaves, y la de recuperación— y eso la bóveda no se lo
|
|
661
|
+
// enseña ni a un servicio de la casa (`modelo-de-amenazas.md` §2.5). Cualquiera que
|
|
662
|
+
// supiera la llave de la cuenta, que es pública, lo sacaba entero preguntando. Y no
|
|
663
|
+
// costaba nada evitarlo: la consola YA manda su firma y su papel en esta misma llamada
|
|
664
|
+
// (`requestStore` de `@dotrino/identity`), solo que aquí no se miraban.
|
|
651
665
|
if (d.method === 'profileRecipients') {
|
|
652
666
|
const members = await recipientsOf(PROFILE_OWNER)
|
|
653
667
|
return reply(from, {
|
|
@@ -657,12 +671,6 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
657
671
|
})
|
|
658
672
|
}
|
|
659
673
|
|
|
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
674
|
try {
|
|
667
675
|
if (d.method === 'profileBundle') {
|
|
668
676
|
const b = secrets.profileBundleFor(chk.device)
|
|
@@ -1322,7 +1330,30 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1322
1330
|
sock.on('error', cerrar)
|
|
1323
1331
|
})
|
|
1324
1332
|
server.on('error', (e) => log('[vault] the local desk could not open (' + e.message + '): services will use the proxy'))
|
|
1325
|
-
|
|
1333
|
+
// EL DIRECTORIO ES EL CONTROL, no el modo del socket.
|
|
1334
|
+
//
|
|
1335
|
+
// `mkdir` con `mode` NO corrige un directorio que ya existe, y sin `XDG_RUNTIME_DIR`
|
|
1336
|
+
// —un servicio de sistema, un contenedor— esto cae en `/tmp`, donde cualquiera puede
|
|
1337
|
+
// haberlo creado antes con permiso de escritura para todos. Quien sea dueño del
|
|
1338
|
+
// directorio puede borrar el socket y poner el suyo: los servicios de esta máquina
|
|
1339
|
+
// acabarían hablando con él. No leería nada (la respuesta va firmada por una llave que
|
|
1340
|
+
// nombra el acta y el cliente la comprueba), pero los deja sin arrancar.
|
|
1341
|
+
//
|
|
1342
|
+
// Y el `chmod 0600` de abajo llega DESPUÉS de `listen`, así que por sí solo deja una
|
|
1343
|
+
// rendija. Lo que la cierra de verdad es que el directorio sea nuestro y solo nuestro.
|
|
1344
|
+
const dir = socketDir()
|
|
1345
|
+
try { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }) } catch (_) {}
|
|
1346
|
+
try {
|
|
1347
|
+
const st = fs.statSync(dir)
|
|
1348
|
+
const ajeno = typeof process.getuid === 'function' && st.uid !== process.getuid()
|
|
1349
|
+
const abierto = (st.mode & 0o077) !== 0
|
|
1350
|
+
if (ajeno || abierto) {
|
|
1351
|
+
return log(`[vault] the local desk stays closed: ${dir} is ${ajeno ? 'owned by another user' : 'writable by others'} ` +
|
|
1352
|
+
'— services on this machine will use the proxy instead')
|
|
1353
|
+
}
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
return log('[vault] the local desk stays closed: could not check ' + dir + ' (' + e.message + ')')
|
|
1356
|
+
}
|
|
1326
1357
|
try { fs.rmSync(socketPath, { force: true }) } catch (_) {}
|
|
1327
1358
|
server.listen(socketPath, () => {
|
|
1328
1359
|
try { fs.chmodSync(socketPath, 0o600) } catch (_) {}
|
|
@@ -1344,6 +1375,14 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
1344
1375
|
async function handleAdminEvent (payload) {
|
|
1345
1376
|
const acta = payload?.acta
|
|
1346
1377
|
if (!acta || typeof acta !== 'object') return
|
|
1378
|
+
// ESTE MENSAJE NO LO FIRMA NADIE, y no hace falta: el acta se autentica sola —va sellada
|
|
1379
|
+
// y `canAdopt` comprueba que la selló quien la MÍA reconoce—. Pero eso solo funciona si
|
|
1380
|
+
// hay una mía contra la que comparar: sin acta previa, `canAdopt` acepta cualquiera
|
|
1381
|
+
// («sin-acta-previa»), y este es el único mostrador donde una llega de un desconocido.
|
|
1382
|
+
// Anunciar no es cómo se entra en una cuenta —para eso está el enrolamiento—, así que
|
|
1383
|
+
// sin acta propia no se adopta: se dice y se para.
|
|
1384
|
+
const propia = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
1385
|
+
if (!propia) return log('[vault] a record was announced but this vault has none of its own to compare it with: ignored')
|
|
1347
1386
|
try {
|
|
1348
1387
|
const r = await identity.adoptActa?.(acta)
|
|
1349
1388
|
if (r?.adopted) log(`[vault] adopted record #${acta.seq} announced by another vault (${r.reason})`)
|