@dotrino/vaultd 0.6.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/README.md +301 -0
- package/bin/dotrino-vault-tui.js +28 -0
- package/bin/dotrino-vault.js +23 -0
- package/bin/dotrino-vaultd.js +29 -0
- package/bin/sea-entry.js +29 -0
- package/lib/README.md +139 -0
- package/lib/src/config.js +26 -0
- package/lib/src/enroll.js +293 -0
- package/lib/src/env.js +95 -0
- package/lib/src/index.js +166 -0
- package/lib/src/protocol.js +53 -0
- package/lib/src/sealed.js +84 -0
- package/lib/src/service.js +258 -0
- package/package.json +41 -0
- package/src/atrest.js +0 -0
- package/src/client.js +149 -0
- package/src/ctl.js +597 -0
- package/src/daemon.js +217 -0
- package/src/manager.js +88 -0
- package/src/node-globals.js +37 -0
- package/src/paths.js +47 -0
- package/src/profiles.js +214 -0
- package/src/protocol.js +6 -0
- package/src/qr.js +61 -0
- package/src/secretsStore.js +61 -0
- package/src/store.js +64 -0
- package/src/threadStore.js +111 -0
- package/src/transport.js +64 -0
- package/src/tui/app.js +722 -0
- package/src/tui/term.js +278 -0
- package/src/vault.js +303 -0
- package/src/vaultControl.js +296 -0
- package/vendor/qrcode-generator.cjs +2297 -0
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Dotrino Vault — tu certificador personal
|
|
2
|
+
|
|
3
|
+
> **Parte del ecosistema [Dotrino](https://dotrino.com).** Tu identidad, en tu
|
|
4
|
+
> máquina, bajo tus reglas — sin anuncios, sin cookies, sin rastreo.
|
|
5
|
+
|
|
6
|
+
`dotrino-vault` es el **certificador personal** del usuario: un **servicio headless**
|
|
7
|
+
que custodia tu **clave maestra** y actúa como tu **propia CA**. En vez de depender
|
|
8
|
+
de las CAs, del "Inicia sesión con Google/Apple" o de verificadores de KYC, **tú
|
|
9
|
+
certificas**: enrolas tus dispositivos, firmas documentos y avalas a otras personas,
|
|
10
|
+
sin pedirle permiso a ningún portero central. La maestra **nunca sale** de tu máquina.
|
|
11
|
+
|
|
12
|
+
## Modelo: identidad delegada (la maestra se queda en una sola máquina)
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
PC (vault) ── clave maestra P (NUNCA sale) ────────────────────────────┐
|
|
16
|
+
· genera/custodia P (vía @dotrino/identity) │
|
|
17
|
+
· firma un CERT por dispositivo: "D puede <scope> en nombre de P, │
|
|
18
|
+
hasta <exp>, revocable por <nonce>" │
|
|
19
|
+
· firma datos a pedido de un dispositivo enrolado (devuelve solo la firma)
|
|
20
|
+
▲ proxy (sendByPubkey + cola offline 24h) │
|
|
21
|
+
│ │
|
|
22
|
+
cel / laptop ── su propia sub-clave D (P nunca la ve) ────────────────┘
|
|
23
|
+
· se enrola escaneando un QR del vault → recibe su cert
|
|
24
|
+
· firma cada acción con D y adjunta el cert; el vault verifica la CADENA D←P
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Todo esto **no se reimplementa**: la cripto de delegación vive en
|
|
28
|
+
`@dotrino/identity` (`signDelegation`, `verifyChain`, `makeDeviceKey`), el transporte
|
|
29
|
+
es `@dotrino/proxy-client`. Este repo solo **orquesta**.
|
|
30
|
+
|
|
31
|
+
## Instalación (Linux)
|
|
32
|
+
|
|
33
|
+
**Ubuntu / Debian — `.deb`** (lo más simple): descarga el `.deb` (versionado) desde
|
|
34
|
+
[Releases](https://github.com/imdotrino/dotrino-vault/releases/latest) y haz doble
|
|
35
|
+
clic, o en la terminal:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
sudo apt install ./dotrino-vault_*.deb
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Otro Linux — tarball:** descarga el binario autosuficiente y ejecuta el instalador:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
tar xzf dotrino-vault-*-linux-x64.tar.gz
|
|
45
|
+
cd dotrino-vault-*-linux-x64
|
|
46
|
+
sh install.sh
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
El `.deb` deja los binarios en `/usr/bin`, instala la unidad `systemd --user` y la
|
|
50
|
+
habilita; el tarball hace lo equivalente en tu `$HOME`. Ambos: nada de Node ni
|
|
51
|
+
dependencias, y el servicio arranca solo.
|
|
52
|
+
|
|
53
|
+
El binario **trae Node embebido**: no necesitas instalar Node ni dependencias de npm. Lo
|
|
54
|
+
único que espera del sistema es la biblioteca `libatomic1`, que casi todas las distribuciones
|
|
55
|
+
traen puesta; el `.deb` la declara y la instala sola si falta. Con el tarball, en un sistema
|
|
56
|
+
muy pelado (un contenedor mínimo, por ejemplo), instálala tú:
|
|
57
|
+
`sudo apt install libatomic1`. El instalador lo
|
|
58
|
+
deja como **servicio systemd `--user`** que arranca solo (también en el boot, vía
|
|
59
|
+
`linger`). En el primer arranque genera tu identidad y se conecta al proxy. **Sin
|
|
60
|
+
contraseña, sin abrir puertos** (el vault marca hacia afuera).
|
|
61
|
+
|
|
62
|
+
> Sin firma de código: tu sistema puede advertir que el binario no está firmado. Es
|
|
63
|
+
> autohospedado y de código abierto; en Linux solo necesita permiso de ejecución (el
|
|
64
|
+
> instalador lo da). macOS y Windows llegan en v2.
|
|
65
|
+
|
|
66
|
+
### CLI de control
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
dotrino-vault tui # interfaz de terminal a pantalla completa (ver abajo)
|
|
70
|
+
dotrino-vault status # estado del servicio + fingerprint
|
|
71
|
+
dotrino-vault pair # inicia un emparejamiento (muestra el código y espera al dispositivo)
|
|
72
|
+
dotrino-vault pending # muestra el dispositivo pendiente + su código a comparar
|
|
73
|
+
dotrino-vault approve <deviceId> # aprueba un dispositivo (tras comparar el código en ambas pantallas)
|
|
74
|
+
dotrino-vault reject <deviceId> # rechaza un dispositivo pendiente
|
|
75
|
+
dotrino-vault devices # lista dispositivos enrolados / revocados
|
|
76
|
+
dotrino-vault revoke <nonce> # revoca un dispositivo (le ordena autoborrarse)
|
|
77
|
+
dotrino-vault pair --service <ns> # empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
|
|
78
|
+
dotrino-vault secret set <ns> <CLAVE> <valor> # guarda un secreto para ese servicio
|
|
79
|
+
dotrino-vault secret rm <ns> <CLAVE> # borra un secreto
|
|
80
|
+
dotrino-vault secret list # nombres de secretos (nunca valores)
|
|
81
|
+
dotrino-vault logs # últimos logs del servicio
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Interfaz de terminal (TUI)
|
|
85
|
+
|
|
86
|
+
Todo lo anterior (bóvedas, dispositivos y secretos) también se maneja desde una
|
|
87
|
+
**interfaz de terminal a pantalla completa**, sin memorizar subcomandos:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
dotrino-vault tui # binario instalado
|
|
91
|
+
node bin/dotrino-vault-tui.js # en desarrollo (o: npm run tui)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Como la CLI, la TUI **no abre la identidad ni la red**: le habla al daemon por el
|
|
95
|
+
mismo canal de archivos + señales. El daemon debe estar corriendo; si no, la TUI
|
|
96
|
+
ofrece arrancarlo. No agrega dependencias (se dibuja con ANSI y lee el teclado en
|
|
97
|
+
raw mode).
|
|
98
|
+
|
|
99
|
+
**Navegación en dos niveles**, para que siempre sea explícito de qué bóveda son
|
|
100
|
+
los dispositivos/variables que estás viendo:
|
|
101
|
+
|
|
102
|
+
1. **Bóvedas** es la pantalla de entrada: lista tus perfiles (`↑↓` mover, `Enter`
|
|
103
|
+
**entrar** a uno — lo activa si no lo estaba). Ahí también creas una bóveda
|
|
104
|
+
nueva, renombras, borras y pones/quitas/usas la contraseña (candado).
|
|
105
|
+
2. Al entrar caes en sus **pestañas horizontales**, que cambias con `←→`:
|
|
106
|
+
- **Dispositivos (pares):** verlos, **emparejar** uno nuevo (muestra el QR y la
|
|
107
|
+
URL, y espera a que se conecte), **aprobar** con el código que muestra el
|
|
108
|
+
dispositivo, **rechazar** y **revocar**.
|
|
109
|
+
- **Scopes y variables (secretos):** ver los scopes y sus variables (nunca los
|
|
110
|
+
valores), **agregar** una variable (con su scope) y **quitar** una variable o
|
|
111
|
+
un scope entero.
|
|
112
|
+
|
|
113
|
+
`Esc` desde las pestañas vuelve a la lista de bóvedas (para entrar a otra); `q`
|
|
114
|
+
sale desde cualquier pantalla. Las teclas de cada acción se listan en la barra
|
|
115
|
+
inferior.
|
|
116
|
+
|
|
117
|
+
### Varios perfiles en el mismo PC
|
|
118
|
+
|
|
119
|
+
Puedes tener varias identidades tuyas en la misma máquina (p. ej. personal y
|
|
120
|
+
trabajo). Cada perfil es **una identidad distinta**: su propia clave, sus propios
|
|
121
|
+
dispositivos, sus propios datos y secretos — nada se cruza entre ellos. **Todos
|
|
122
|
+
atienden a la vez**: el perfil «activo» solo decide a cuál va un comando cuando no
|
|
123
|
+
lo dices con `--profile`, no apaga a los demás.
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
dotrino-vault profile ls # lista los perfiles (* = el activo)
|
|
127
|
+
dotrino-vault profile add Trabajo # crea un perfil (identidad nueva, vacía)
|
|
128
|
+
dotrino-vault profile use Trabajo # elige el activo
|
|
129
|
+
dotrino-vault profile rename <nombre> # renombra
|
|
130
|
+
dotrino-vault profile rm Trabajo # BORRA el perfil y su identidad (irreversible)
|
|
131
|
+
|
|
132
|
+
dotrino-vault pair --profile Trabajo # cualquier comando acepta --profile
|
|
133
|
+
dotrino-vault devices --profile personal
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Si ya usabas el vault antes de esto, tu identidad de siempre se convierte sola en
|
|
137
|
+
el primer perfil («Perfil 1»): la misma clave, los mismos dispositivos, nada que
|
|
138
|
+
volver a emparejar.
|
|
139
|
+
|
|
140
|
+
### Contraseña del perfil (opcional)
|
|
141
|
+
|
|
142
|
+
Cada perfil puede llevar contraseña. **Solo se pide para EDITAR el perfil** (cambiar
|
|
143
|
+
tu nombre, avatar o datos): tus dispositivos siguen firmando, leyendo y guardando
|
|
144
|
+
aunque el perfil esté bloqueado — así un reinicio del PC nunca deja tus apps
|
|
145
|
+
muertas esperando a que alguien teclee algo.
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
dotrino-vault profile password # pone o cambia la contraseña (te la pregunta)
|
|
149
|
+
dotrino-vault profile password rm # la quita
|
|
150
|
+
dotrino-vault unlock # desbloquea para poder editar
|
|
151
|
+
dotrino-vault lock # vuelve a bloquear
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
El perfil se vuelve a bloquear al reiniciar el servicio. La contraseña **no se
|
|
155
|
+
guarda**: solo un verificador con sal (PBKDF2), igual que el candado del navegador.
|
|
156
|
+
Para que quede claro qué protege y qué no: evita que otro que se siente en tu
|
|
157
|
+
máquina —o un dispositivo tuyo comprometido— te reescriba el perfil; **no** cifra la
|
|
158
|
+
clave en el disco (eso es el cifrado en reposo, ver *Alcance*).
|
|
159
|
+
|
|
160
|
+
**Emparejamiento endurecido** (ver [`docs/pairing-protocol.md`](./docs/pairing-protocol.md)):
|
|
161
|
+
el dispositivo prueba posesión de su llave firmando el enrolamiento, y la maestra
|
|
162
|
+
solo firma el certificado **después** de que compares un código de 6 dígitos (SAS)
|
|
163
|
+
entre las dos pantallas y corras `approve`. Un código robado ya no alcanza para
|
|
164
|
+
entrar; la revocación de un dispositivo le ordena **autoborrarse** (con firma de la
|
|
165
|
+
maestra, no por un mensaje cualquiera).
|
|
166
|
+
|
|
167
|
+
El servicio se gestiona con systemd `--user`
|
|
168
|
+
(`systemctl --user {start,stop,restart} dotrino-vault`). Tus datos —clave maestra
|
|
169
|
+
incluida— viven en `~/.local/share/dotrino/vault` (permisos `0600`/`0700`), con un
|
|
170
|
+
subdirectorio `p/<id>/` por perfil.
|
|
171
|
+
|
|
172
|
+
## Desarrollo
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
npm install
|
|
176
|
+
node bin/dotrino-vaultd.js # arranca el daemon (modo servicio)
|
|
177
|
+
node bin/dotrino-vaultd.js --pair # arranca + imprime un QR de emparejamiento
|
|
178
|
+
bash packaging/build.sh # compila el binario único (dist/)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Enrolar y usar desde un dispositivo (Node, para testing)
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
import { enroll, requestSign } from 'dotrino-vault/src/client.js'
|
|
185
|
+
|
|
186
|
+
// 1) escaneas el QR del vault → obtienes { iss, proxy, token }
|
|
187
|
+
const { device, cert, iss } = await enroll({ qr }) // GUARDA device (privada) + cert
|
|
188
|
+
|
|
189
|
+
// 2) le pides a la maestra que firme algo (la maestra nunca sale del vault)
|
|
190
|
+
const { signature } = await requestSign({
|
|
191
|
+
masterPubkey: iss, proxyUrl: qr.proxy, device, cert,
|
|
192
|
+
payload: { hola: 'mundo' }
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Secretos de servicios (los servicios del ecosistema son clientes identificados)
|
|
197
|
+
|
|
198
|
+
Los servicios (proxy, geo, bots…) **no llevan secretos de terceros en su `.env`**:
|
|
199
|
+
se enrolan al vault como un dispositivo más, con un cert limitado al scope
|
|
200
|
+
`vault:secrets:<ns>` (`pair --service <ns>`), y al arrancar piden su bundle.
|
|
201
|
+
|
|
202
|
+
Son **dos momentos distintos, a propósito**: el **enrolamiento** (registro de la
|
|
203
|
+
máquina) es un **comando previo** que corre un humano **una sola vez**; el
|
|
204
|
+
**arranque** de la app solo lee la identidad ya guardada y no interactúa con nadie.
|
|
205
|
+
|
|
206
|
+
#### 1) Enrolamiento — comando previo, una vez por máquina
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# en el VAULT (tu PC)
|
|
210
|
+
dotrino-vault pair --service proxy # invitación con scope SOLO vault:secrets:proxy
|
|
211
|
+
dotrino-vault secret set proxy TURN_KEY_ID …
|
|
212
|
+
|
|
213
|
+
# en la MÁQUINA del servicio (pega la invitación; te MUESTRA un código)
|
|
214
|
+
npx dotrino-env enroll --ns proxy
|
|
215
|
+
|
|
216
|
+
# de vuelta en el VAULT: tipeas el código LEYÉNDOLO de la pantalla del servicio
|
|
217
|
+
dotrino-vault approve 7K3F-92Q1
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Deja `~/.dotrino/service/<ns>/service-identity.json` (0600) con la llave del
|
|
221
|
+
dispositivo (generada ahí, nunca sale) + el cert. **No hay ningún secreto en disco.**
|
|
222
|
+
|
|
223
|
+
**Por qué NO se enrola en el primer arranque de la app:** el enrolamiento exige un
|
|
224
|
+
humano que **lea el código en la pantalla del servicio** — es lo único que impide que
|
|
225
|
+
un vault falso (que nunca vio el código) enrole la máquina, o que alguien apruebe a
|
|
226
|
+
ciegas. Un servicio arranca bajo systemd/PM2, sin TTY y sin nadie mirando: el código
|
|
227
|
+
acabaría en un log (y quien lea el log ya podría aprobar). Además el arranque quedaría
|
|
228
|
+
bloqueado esperando una aprobación que quizá nadie da, y un reinicio automático de
|
|
229
|
+
madrugada intentaría re-enrolar. Separados, el arranque es determinista e idempotente:
|
|
230
|
+
solo **lee**; el enrolamiento **escribe** y consume una invitación de un solo uso.
|
|
231
|
+
|
|
232
|
+
Va donde va el `npm ci` al aprovisionar el VPS. (Para máquinas efímeras —Docker,
|
|
233
|
+
autoescalado— haría falta una invitación pre-provisionada de un solo uso con TTL corto
|
|
234
|
+
y auto-aprobación; **aún no está decidido ni implementado**.)
|
|
235
|
+
|
|
236
|
+
#### 2) Arranque — sin interacción, en cada reinicio
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
import '@dotrino/vault/config' // como `dotenv/config`, pero contra el vault (ns = DOTRINO_NS)
|
|
240
|
+
console.log(process.env.TURN_KEY_ID)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
o explícito, con `@dotrino/vault/env` / `@dotrino/vault/service`:
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
import { loadEnv } from '@dotrino/vault/env'
|
|
247
|
+
const { secrets } = await loadEnv({ ns: 'proxy', required: ['TURN_KEY_ID'] })
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Modos de fallo (importante):**
|
|
251
|
+
|
|
252
|
+
- **Vault caído / proxy caído** → **espera** (reintento con backoff, para siempre). Sin
|
|
253
|
+
vault el servicio no opera: no arranca con secretos viejos ni vacíos.
|
|
254
|
+
- **Sin enrolar, cert revocado o vencido, scope equivocado** → **aborta en el acto**.
|
|
255
|
+
Son errores que no se arreglan reintentando: hay que (re)enrolar.
|
|
256
|
+
|
|
257
|
+
CLI de apoyo: `dotrino-env status` (qué hay enrolado aquí), `dotrino-env check` (lista
|
|
258
|
+
los **nombres** de los secretos, nunca los valores), `dotrino-env run -- <cmd>` (inyecta
|
|
259
|
+
los secretos en el entorno de un proceso que no es Node).
|
|
260
|
+
|
|
261
|
+
Garantías: la petición va firmada por la llave del servicio + cert (scope solo
|
|
262
|
+
su `ns`); la respuesta viaja **sellada** a una llave efímera por petición (el
|
|
263
|
+
proxy que la transporta no puede leerla, y un replay no se puede descifrar) y
|
|
264
|
+
**firmada por la maestra** (verificada contra la `iss` pineada del enrolamiento).
|
|
265
|
+
Revocar el cert (`revoke`) corta el acceso de inmediato; `activity` audita cada
|
|
266
|
+
lectura. Los servicios críticos sin secretos en su core (el propio proxy) arrancan
|
|
267
|
+
sin vault; solo la feature que los necesita (TURN) espera. Primer consumidor:
|
|
268
|
+
`dotrino-proxy` (TURN de Cloudflare, ver su README).
|
|
269
|
+
|
|
270
|
+
## Alcance
|
|
271
|
+
|
|
272
|
+
- **v1 (este):** servicio headless en Node (Linux), **multi-perfil**, con
|
|
273
|
+
**contraseña opcional por perfil para editarlo** (verificador PBKDF2) pero la
|
|
274
|
+
**clave privada en claro** en el disco (en `~/.local/share/dotrino/vault`, permisos
|
|
275
|
+
`0600`). Enrolamiento de dispositivos, firma delegada y lectura del árbol de
|
|
276
|
+
contenidos por el proxy. Distribución como binario único (Node SEA) + servicio systemd.
|
|
277
|
+
- **v2:** **cifrado en reposo** con la contraseña (keychain del SO o archivo, a
|
|
278
|
+
elección) — hoy la contraseña es un candado de edición, no cifra la clave; UI de
|
|
279
|
+
escritorio (Tauri) como cliente del daemon; firma de documentos con sellado de
|
|
280
|
+
tiempo (`dotrino-signer`); macOS y Windows.
|
|
281
|
+
|
|
282
|
+
## Estructura
|
|
283
|
+
|
|
284
|
+
- `src/vault.js` — núcleo de UN perfil (Identity + transporte + router: enrolar/firmar/leer).
|
|
285
|
+
- `src/profiles.js` — registro multi-perfil (`profiles.json`, `p/<id>/`) + candado por contraseña.
|
|
286
|
+
- `src/manager.js` — corre todos los perfiles a la vez (uno por maestra/conexión).
|
|
287
|
+
- `src/daemon.js` — modo servicio: `state.json`, emparejamiento por señal, apagado limpio.
|
|
288
|
+
- `src/ctl.js` — CLI de control (habla con el daemon por archivos + señales, sin socket).
|
|
289
|
+
- `src/vaultControl.js` — API de control programática (misma vía que la CLI); la usa la TUI.
|
|
290
|
+
- `src/tui/` — interfaz de terminal a pantalla completa, sin dependencias (`term.js` primitivas ANSI/raw-mode; `app.js` pantallas).
|
|
291
|
+
- `src/transport.js` — conexión headless al proxy + `identify` firmado.
|
|
292
|
+
- `src/store.js` — árbol de contenidos (`vault.json`, versionado).
|
|
293
|
+
- `src/client.js` — helper de **dispositivo** (enrolar / pedir firma / leer).
|
|
294
|
+
- `src/protocol.js` — tipos de mensaje y scopes. · `src/qr.js` — QR ASCII. · `src/paths.js` — dirs.
|
|
295
|
+
- `bin/sea-entry.js` — entrypoint del binario único (multicall daemon / `--ctl` / `--tui`).
|
|
296
|
+
- `bin/dotrino-vaultd.js` — entrypoint de desarrollo (node directo).
|
|
297
|
+
- `bin/dotrino-vault-tui.js` — entrypoint de desarrollo de la TUI.
|
|
298
|
+
- `packaging/` — `build.sh` (binario), `install.sh`/`uninstall.sh`, unit systemd.
|
|
299
|
+
- `web/` — la página `vault.dotrino.com` (Vite + Vue).
|
|
300
|
+
|
|
301
|
+
Sin anuncios, sin cuentas, sin rastreo. MIT · parte de Dotrino.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dotrino-vault-tui — interfaz de terminal (pantalla completa) del vault.
|
|
4
|
+
*
|
|
5
|
+
* Entrypoint de DESARROLLO (node directo). En producción el binario SEA expone
|
|
6
|
+
* lo mismo con `dotrino-vaultd --tui` (ver bin/sea-entry.js).
|
|
7
|
+
*
|
|
8
|
+
* NO abre la identidad ni la red: le da órdenes al daemon por archivos + señales
|
|
9
|
+
* (misma vía que la CLI de control). El daemon debe estar corriendo; si no, la
|
|
10
|
+
* TUI ofrece arrancarlo.
|
|
11
|
+
*
|
|
12
|
+
* node bin/dotrino-vault-tui.js
|
|
13
|
+
*
|
|
14
|
+
* Env:
|
|
15
|
+
* DOTRINO_VAULT_DIR dir de datos (default ~/.local/share/dotrino/vault)
|
|
16
|
+
*/
|
|
17
|
+
import { runTui } from '../src/tui/app.js'
|
|
18
|
+
|
|
19
|
+
if (!process.stdout.isTTY) {
|
|
20
|
+
console.error('dotrino-vault-tui necesita un terminal interactivo (TTY).')
|
|
21
|
+
process.exit(2)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
runTui().catch((e) => {
|
|
25
|
+
// El finally de runTui ya restauró el terminal; aquí solo reportamos.
|
|
26
|
+
console.error('error en la TUI:', e?.stack || e?.message || e)
|
|
27
|
+
process.exit(1)
|
|
28
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dotrino-vault — el CLI de control, instalado por npm.
|
|
4
|
+
*
|
|
5
|
+
* En el binario autosuficiente (SEA) este mismo CLI vive dentro del ejecutable y se
|
|
6
|
+
* invoca con `dotrino-vaultd --ctl …`; instalado por npm es un `bin` propio, que es lo
|
|
7
|
+
* que espera quien lo corre con `npx` o tras un `npm i -g`.
|
|
8
|
+
*
|
|
9
|
+
* NO abre la identidad ni la red: le da órdenes al daemon escribiendo archivos en el
|
|
10
|
+
* dir de datos (ver `src/vaultControl.js`). Por eso funciona igual en Linux, Windows,
|
|
11
|
+
* macOS y —montando el dir— desde fuera de un contenedor.
|
|
12
|
+
*
|
|
13
|
+
* dotrino-vault status · pair · approve <código> · members · devices · revoke <nonce>
|
|
14
|
+
*
|
|
15
|
+
* Env:
|
|
16
|
+
* DOTRINO_VAULT_DIR dir de datos (default: ver `src/paths.js`)
|
|
17
|
+
*/
|
|
18
|
+
import { runCtl } from '../src/ctl.js'
|
|
19
|
+
|
|
20
|
+
runCtl(process.argv.slice(2)).catch((e) => {
|
|
21
|
+
console.error('error:', e?.message || e)
|
|
22
|
+
process.exit(1)
|
|
23
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dotrino-vaultd — entrypoint de DESARROLLO (node directo, sin SEA).
|
|
4
|
+
*
|
|
5
|
+
* node bin/dotrino-vaultd.js arranca el vault (modo servicio)
|
|
6
|
+
* node bin/dotrino-vaultd.js --pair arranca e imprime un QR de emparejamiento
|
|
7
|
+
*
|
|
8
|
+
* En producción el usuario corre el binario SEA (bin/sea-entry.js → src/daemon.js).
|
|
9
|
+
* Este archivo comparte el mismo núcleo (`runDaemon`) para no divergir.
|
|
10
|
+
*
|
|
11
|
+
* Env:
|
|
12
|
+
* DOTRINO_VAULT_DIR dir de datos (default ~/.dotrino/vault)
|
|
13
|
+
* PROXY_URL proxy (default wss://proxy.dotrino.com)
|
|
14
|
+
*/
|
|
15
|
+
import { runDaemon } from '../src/daemon.js'
|
|
16
|
+
import { qrToString } from '../src/qr.js'
|
|
17
|
+
|
|
18
|
+
const mgr = await runDaemon()
|
|
19
|
+
|
|
20
|
+
// Atajo de dev: --pair imprime el QR directo en stdout (en producción se usa el CLI).
|
|
21
|
+
// Empareja contra el perfil ACTIVO.
|
|
22
|
+
if (process.argv.includes('--pair')) {
|
|
23
|
+
const { qr, expiresInMs } = mgr.current().startPairing({ label: 'cli' })
|
|
24
|
+
console.log(`\nEmparejá un dispositivo (válido ${expiresInMs / 60000} min):\n`)
|
|
25
|
+
console.log(qrToString(JSON.stringify(qr)))
|
|
26
|
+
console.log(JSON.stringify(qr))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log('\n(Ctrl+C para detener)')
|
package/bin/sea-entry.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sea-entry.js — punto de entrada ÚNICO del binario autosuficiente (SEA).
|
|
3
|
+
*
|
|
4
|
+
* El mismo binario hace de daemon y de CLI de control (multicall):
|
|
5
|
+
* dotrino-vaultd → arranca el daemon (modo servicio)
|
|
6
|
+
* dotrino-vaultd --ctl ... → CLI de control (status / pair / devices / revoke)
|
|
7
|
+
*
|
|
8
|
+
* El wrapper `dotrino-vault` invoca siempre con `--ctl`. Se usa import dinámico
|
|
9
|
+
* para no cargar el grafo del daemon (transporte/identity) cuando solo quieres
|
|
10
|
+
* un `status` rápido.
|
|
11
|
+
*/
|
|
12
|
+
const argv = process.argv.slice(2)
|
|
13
|
+
|
|
14
|
+
if (argv[0] === '--tui') {
|
|
15
|
+
// Interfaz de terminal (pantalla completa). Como la CLI, NO toca la identidad
|
|
16
|
+
// ni el proxy: habla con el daemon por el dir de datos + señales.
|
|
17
|
+
if (!process.stdout.isTTY) { console.error('la TUI necesita un terminal interactivo (TTY).'); process.exit(2) }
|
|
18
|
+
import('../src/tui/app.js').then(({ runTui }) => runTui())
|
|
19
|
+
.catch((e) => { console.error('error en la TUI:', e.message); process.exit(1) })
|
|
20
|
+
} else if (argv[0] === '--ctl') {
|
|
21
|
+
// CLI de control: NO toca la identidad ni el proxy; habla con el daemon vía
|
|
22
|
+
// el dir de datos (state.json) y señales (SIGUSR1 para emparejar).
|
|
23
|
+
import('../src/ctl.js').then(({ runCtl }) => runCtl(argv.slice(1)))
|
|
24
|
+
.catch((e) => { console.error('error:', e.message); process.exit(1) })
|
|
25
|
+
} else {
|
|
26
|
+
// Modo daemon (servicio).
|
|
27
|
+
import('../src/daemon.js').then(({ runDaemon }) => runDaemon(argv))
|
|
28
|
+
.catch((e) => { console.error('error fatal del daemon:', e.message); process.exit(1) })
|
|
29
|
+
}
|
package/lib/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @dotrino/vault
|
|
2
|
+
|
|
3
|
+
Usa **este dispositivo (navegador) como bóveda/CA** del ecosistema Dotrino, sin un PC
|
|
4
|
+
con el daemon. Es la contraparte browser del daemon `dotrino-vault`: atiende el mismo
|
|
5
|
+
protocolo de enrolamiento endurecido por el proxy y firma certificados de delegación
|
|
6
|
+
`D ← P` (donde `P` es la identidad de este dispositivo, `@dotrino/identity`).
|
|
7
|
+
|
|
8
|
+
Pensado para que **cualquier app** del ecosistema (no solo la terminal) pueda ofrecer
|
|
9
|
+
"usar este dispositivo como bóveda".
|
|
10
|
+
|
|
11
|
+
## Uso
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { Identity } from '@dotrino/identity'
|
|
15
|
+
import { startDeviceVault } from '@dotrino/vault'
|
|
16
|
+
|
|
17
|
+
const identity = await Identity.connect()
|
|
18
|
+
const vault = await startDeviceVault(identity) // se conecta al proxy como P
|
|
19
|
+
|
|
20
|
+
// 1) Abrir un emparejamiento y mostrar el QR/JSON al dispositivo a enrolar:
|
|
21
|
+
const { qr } = vault.startPairing({ label: 'mi-agente' })
|
|
22
|
+
// El dispositivo (p. ej. @dotrino/identity#enrollDevice) consume `qr`, GENERA un
|
|
23
|
+
// código aleatorio y lo MUESTRA (no lo envía).
|
|
24
|
+
|
|
25
|
+
// 2) Cuando el dispositivo pide acceso, aparece en la lista de pendientes:
|
|
26
|
+
vault.onPendingChange(() => {
|
|
27
|
+
for (const { deviceId } of vault.listPending()) {
|
|
28
|
+
// Un humano LEE el código del dispositivo y lo TIPEA aquí:
|
|
29
|
+
// await vault.approve(deviceId, codigoTipeado)
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// 3) Máquinas ya enroladas / revocar:
|
|
34
|
+
const machines = await vault.listMachines() // [{ sub, deviceId, label, exp, nonce, scope }]
|
|
35
|
+
// await vault.revoke(nonce)
|
|
36
|
+
|
|
37
|
+
vault.close()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Credenciales del vault en vez del `.env` (Node)
|
|
41
|
+
|
|
42
|
+
La cara "dotenv" del paquete: **cualquier proyecto Node** jala sus credenciales del
|
|
43
|
+
vault del dueño y las deja en `process.env`. En el disco del servicio **no queda
|
|
44
|
+
ningún secreto**: solo la llave del dispositivo (generada ahí, nunca sale) y un
|
|
45
|
+
certificado con scope `vault:secrets:<ns>`. Los valores viven **solo en memoria**;
|
|
46
|
+
si la máquina se compromete, revocas el cert y no había nada que robar.
|
|
47
|
+
|
|
48
|
+
### 1) Registro del cliente (una sola vez)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# en el VAULT (tu PC): abres el emparejamiento del servicio y cargas sus secretos
|
|
52
|
+
dotrino-vault pair --service miapp # invitación con scope SOLO vault:secrets:miapp
|
|
53
|
+
dotrino-vault secret set miapp API_KEY sk-…
|
|
54
|
+
|
|
55
|
+
# en el PROYECTO/servidor: enrola esta máquina (pega la invitación)
|
|
56
|
+
npx dotrino-env enroll --ns miapp
|
|
57
|
+
# → muestra un código: dotrino-vault approve 7K3F-92Q1
|
|
58
|
+
|
|
59
|
+
# de vuelta en el VAULT: lo tipeas leyéndolo de esa pantalla
|
|
60
|
+
dotrino-vault approve 7K3F-92Q1
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
El código lo **genera el servicio** y **no viaja** por la red: el vault solo puede
|
|
64
|
+
echarlo de vuelta si un humano lo tipeó. Así, un vault falso no puede enrolarte y
|
|
65
|
+
aprobar a ciegas no enrola a nadie. Queda `~/.dotrino/service/<ns>/service-identity.json`
|
|
66
|
+
(0600) con `{ device, cert, iss, proxy, ns }`.
|
|
67
|
+
|
|
68
|
+
Es un **comando previo**, no el primer arranque de la app: el enrolamiento necesita a
|
|
69
|
+
un humano leyendo el código en esta pantalla (bajo systemd/PM2 no hay TTY y el código
|
|
70
|
+
acabaría en un log), bloquea esperando la aprobación y **escribe** en disco consumiendo
|
|
71
|
+
una invitación de un solo uso. El arranque, en cambio, solo **lee** la identidad ya
|
|
72
|
+
guardada: es idempotente y no interactúa con nadie. Corre el `enroll` donde corres el
|
|
73
|
+
`npm ci` al aprovisionar la máquina.
|
|
74
|
+
|
|
75
|
+
### 2) En el código
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
import '@dotrino/vault/config' // como `dotenv/config`, pero contra el vault (ns = DOTRINO_NS)
|
|
79
|
+
console.log(process.env.API_KEY)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
o explícito:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import { loadEnv } from '@dotrino/vault/env'
|
|
86
|
+
const { secrets } = await loadEnv({ ns: 'miapp', required: ['API_KEY'] })
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Es **asíncrono a propósito**: el `import` bloquea el arranque (top-level await) hasta
|
|
90
|
+
que los secretos estén. Si el vault no está disponible, **espera** (reintento con
|
|
91
|
+
backoff) — un servicio sin vault no arranca, no opera con secretos viejos ni vacíos.
|
|
92
|
+
Un fallo NO transitorio (sin enrolar, cert revocado, scope equivocado) sí aborta.
|
|
93
|
+
|
|
94
|
+
Para procesos que no son Node, el CLI los inyecta en el entorno de un hijo:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
dotrino-env run --ns miapp -- ./mi-binario
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### API `@dotrino/vault/env`
|
|
101
|
+
|
|
102
|
+
- `loadEnv({ ns?, dir?, override?, wait?, required?, onRetry? }) → { ns, secrets, injected, skipped }`
|
|
103
|
+
(por defecto **no pisa** variables ya presentes en el entorno; `override: true` sí)
|
|
104
|
+
- `serviceDir(ns)`, `serviceRoot()`, `listEnrolled()`, `resolveNs(ns?)`
|
|
105
|
+
- Entorno: `DOTRINO_NS` · `DOTRINO_ENV_DIR` · `DOTRINO_ENV_HOME` · `DOTRINO_ENV_QUIET`
|
|
106
|
+
- CLI: `dotrino-env enroll|status|check|run` (`check` lista **nombres** de secretos, nunca valores)
|
|
107
|
+
|
|
108
|
+
Bajo el capó es `@dotrino/vault/service` (`enrollService` / `waitForSecrets`): petición
|
|
109
|
+
firmada por la llave del servicio + cert, respuesta **sellada** (ECDH efímero + AES-GCM,
|
|
110
|
+
el proxy no ve los valores) y **firmada por la maestra**, verificada contra la `iss`
|
|
111
|
+
pineada en el enrolamiento.
|
|
112
|
+
|
|
113
|
+
## Modelo de aprobación (seguro por diseño)
|
|
114
|
+
|
|
115
|
+
- El **dispositivo** que se enrola genera un **código aleatorio** (`makePairingCode`) y
|
|
116
|
+
lo **muestra**; el código **no viaja** por la red.
|
|
117
|
+
- Esta bóveda **no conoce** el código: un humano lo **lee del dispositivo** y lo **tipea**
|
|
118
|
+
aquí. Al aprobar, la bóveda firma el cert y **echa** el código tipeado de vuelta.
|
|
119
|
+
- El dispositivo acepta el cert **solo si el código echado coincide** con el que generó.
|
|
120
|
+
Así, una bóveda falsa (que nunca vio el código) no puede enrolarlo, y **aprobar a ciegas**
|
|
121
|
+
(sin ir a leer el código del dispositivo) no enrola a nadie.
|
|
122
|
+
|
|
123
|
+
## API
|
|
124
|
+
|
|
125
|
+
`startDeviceVault(identity, { proxyUrl? }) → Promise<handle>`
|
|
126
|
+
|
|
127
|
+
- `startPairing({ scope?, ttlMs?, label? }) → { qr, expiresInMs }`
|
|
128
|
+
- `listPending() → [{ deviceId, label }]`
|
|
129
|
+
- `approve(deviceId, code) → Promise<{ ok, deviceId }>` (code = lo que muestra el dispositivo)
|
|
130
|
+
- `reject(deviceId)`
|
|
131
|
+
- `listMachines() → Promise<[{ sub, deviceId, label, scope, exp, nonce }]>`
|
|
132
|
+
- `revoke(nonce) → Promise`
|
|
133
|
+
- `getSelfCert() → Promise<cert>` (self-cert `P ← P`, para actuar además de cliente)
|
|
134
|
+
- `onPendingChange(fn)`, `close()`
|
|
135
|
+
|
|
136
|
+
Cripto y firma: `@dotrino/identity`. Transporte: `@dotrino/proxy-client`. No reimplementa
|
|
137
|
+
nada del ecosistema.
|
|
138
|
+
|
|
139
|
+
MIT · parte de [Dotrino](https://dotrino.com).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `import '@dotrino/vault/config'` — el equivalente de `import 'dotenv/config'`,
|
|
3
|
+
* pero contra el vault del dueño.
|
|
4
|
+
*
|
|
5
|
+
* Bloquea el arranque (top-level await) hasta que los secretos del ns estén en
|
|
6
|
+
* `process.env`. Si el vault no está disponible, ESPERA (reintento con backoff):
|
|
7
|
+
* la regla del ecosistema es que un servicio sin vault no arranca — no opera con
|
|
8
|
+
* secretos viejos ni vacíos. Un fallo NO transitorio (sin enrolar, cert revocado,
|
|
9
|
+
* scope equivocado) sí aborta el proceso.
|
|
10
|
+
*
|
|
11
|
+
* Config por entorno:
|
|
12
|
+
* DOTRINO_NS namespace de secretos (si no, el único enrolado en la máquina)
|
|
13
|
+
* DOTRINO_ENV_DIR directorio de la identidad del servicio (si no, ~/.dotrino/service/<ns>)
|
|
14
|
+
* DOTRINO_ENV_QUIET '1' para no imprimir la línea de arranque
|
|
15
|
+
*/
|
|
16
|
+
import { loadEnv } from './env.js'
|
|
17
|
+
|
|
18
|
+
const quiet = process.env.DOTRINO_ENV_QUIET === '1'
|
|
19
|
+
|
|
20
|
+
const { ns, injected } = await loadEnv({
|
|
21
|
+
onRetry: (e, ms) => {
|
|
22
|
+
if (!quiet) console.error('[dotrino-env] vault no disponible (%s); reintentando en %ds…', e.message, Math.round(ms / 1000))
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (!quiet) console.error('[dotrino-env] %d secreto(s) del ns "%s" cargados en process.env', injected.length, ns)
|