@dotrino/vaultd 0.12.0 → 0.13.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 +534 -188
- package/lib/README.md +136 -5
- package/lib/src/admin.js +146 -0
- package/lib/src/atrest.js +0 -0
- package/lib/src/config.js +38 -6
- package/lib/src/enroll.js +29 -29
- package/lib/src/env.js +119 -8
- package/lib/src/index.js +70 -18
- package/lib/src/protocol.js +29 -1
- package/lib/src/sealed.js +1 -1
- package/lib/src/service.js +238 -13
- package/package.json +7 -4
- package/src/atrest.js +0 -0
- package/src/client.js +64 -6
- package/src/ctl.js +10 -5
- package/src/daemon.js +19 -19
- package/src/manager.js +2 -2
- package/src/paths.js +24 -8
- package/src/profiles.js +14 -8
- package/src/secretsStore.js +14 -11
- package/src/store.js +12 -7
- package/src/threadStore.js +76 -4
- package/src/vault.js +197 -32
- package/src/vaultControl.js +8 -8
package/README.md
CHANGED
|
@@ -1,36 +1,139 @@
|
|
|
1
|
-
# Dotrino Vault — tu
|
|
1
|
+
# Dotrino Vault — tu bóveda personal
|
|
2
2
|
|
|
3
|
-
> **Parte del ecosistema [Dotrino](https://dotrino.com).** Tu identidad
|
|
4
|
-
> máquina, bajo tus reglas — sin anuncios, sin cookies, sin rastreo.
|
|
3
|
+
> **Parte del ecosistema [Dotrino](https://dotrino.com).** Tu identidad y tu
|
|
4
|
+
> contenido, en tu máquina, bajo tus reglas — sin anuncios, sin cookies, sin rastreo.
|
|
5
5
|
|
|
6
|
-
`dotrino-vault` es
|
|
7
|
-
|
|
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.
|
|
6
|
+
`dotrino-vault` es la **bóveda personal** del usuario: un **servicio headless** que
|
|
7
|
+
corre en tu propia máquina y hace dos cosas.
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
- **Es tu certificador.** Custodia la llave que manda sobre tu cuenta y actúa como
|
|
10
|
+
tu **propia CA**: enrolas tus dispositivos, firmas por ti y avalas a otras
|
|
11
|
+
personas sin pedirle permiso a ningún portero central. En vez de depender de las
|
|
12
|
+
CAs, del "Inicia sesión con Google/Apple" o de un verificador de KYC, **certificas
|
|
13
|
+
tú**. Esa llave **nunca sale** de la máquina.
|
|
14
|
+
- **Es tu almacén.** Guarda el contenido de tus apps —hilos, "recientes", tu
|
|
15
|
+
perfil— **cifrado de punta a punta** con la clave de tu cuenta, y se lo sirve a
|
|
16
|
+
los dispositivos que tú conectaste. El proxy transporta el sobre; no puede
|
|
17
|
+
abrirlo.
|
|
18
|
+
|
|
19
|
+
**No escucha nada**: no abre puertos ni hay que tocar el router. Se conecta él hacia
|
|
20
|
+
afuera al proxy del ecosistema, y tus aparatos lo alcanzan por ahí.
|
|
21
|
+
|
|
22
|
+
**Este repo publica dos paquetes npm distintos**, con versiones independientes:
|
|
23
|
+
|
|
24
|
+
| Paquete | Dónde | Qué es |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| **`@dotrino/vaultd`** | la raíz | el daemon de este README: `dotrino-vaultd`, la CLI `dotrino-vault`, la TUI y el binario único. |
|
|
27
|
+
| **`@dotrino/vault`** | [`lib/`](./lib/) | la librería: usar **este dispositivo (navegador) como bóveda**, sin un PC con el daemon, y el **cliente de servicio** en Node (`/config`, `/env`, `/service`) con su CLI `dotrino-env`. Documentación propia en [`lib/README.md`](./lib/README.md). |
|
|
28
|
+
|
|
29
|
+
Que las versiones no coincidan es normal: son dos paquetes, no uno.
|
|
30
|
+
|
|
31
|
+
## Modelo: un perfil es un acta, y una sola llave la sella
|
|
32
|
+
|
|
33
|
+
**Tres palabras para lo mismo, dicho una vez:** una **cuenta** es un **perfil** es un
|
|
34
|
+
**acta** — el conjunto de llaves miembro más la política firmada que dice qué puede
|
|
35
|
+
cada una. Un **miembro** es una **llave**, no un aparato: un mismo aparato puede
|
|
36
|
+
tener varias llaves y por lo tanto varias cuentas. La TUI llama **bóvedas** a esos
|
|
37
|
+
perfiles porque cada uno tiene su llave, su directorio y sus dispositivos; la
|
|
38
|
+
**bóveda** a secas es este servicio, que los atiende todos a la vez.
|
|
13
39
|
|
|
14
40
|
```
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
perfil (= cuenta) ── nombre estable: profileId = pubkey de la llave génesis
|
|
42
|
+
└── ACTA firmada: quién es miembro y qué puede cada uno
|
|
43
|
+
· sellador (master): UNA sola llave firma el acta. La intención es que sea la bóveda.
|
|
44
|
+
· miembros: una LLAVE cada uno, con capacidades
|
|
45
|
+
sign · store · read → un dispositivo tuyo
|
|
46
|
+
secrets + cn → un servicio: abre SOLO su propio cajón
|
|
47
|
+
· llavero: la clave de contenido del perfil, envuelta para cada miembro
|
|
48
|
+
|
|
49
|
+
PC (bóveda) ── su llave NUNCA sale ──────────────────────────────────────┐
|
|
50
|
+
· sella el acta (si es el master) y emite un CERT por miembro: │
|
|
51
|
+
"D puede <scope> en nombre de esta llave, hasta <exp>, revocable por <nonce>"
|
|
52
|
+
· firma datos a pedido de un miembro enrolado (devuelve solo la firma) │
|
|
53
|
+
· abre y cierra el contenido con la clave del perfil │
|
|
54
|
+
▲ proxy (sendByPubkey + cola offline 24 h) │
|
|
55
|
+
│ │
|
|
56
|
+
cel / laptop ── su propia llave (ninguna otra la ve) ──────────────────┘
|
|
57
|
+
· se empareja con un QR → entra al acta y recibe su cert
|
|
58
|
+
· firma cada acción con su llave y adjunta el cert; la bóveda verifica la cadena
|
|
25
59
|
```
|
|
26
60
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
**El nombre de la cuenta es el `profileId`**: la pubkey de la llave donde nació, y no
|
|
62
|
+
cambia nunca — es lo que conocen la reputación, los contactos y todo lo que esa
|
|
63
|
+
cuenta firmó. **No tiene por qué ser la llave de la bóveda**: si la cuenta nació en
|
|
64
|
+
tu teléfono y la bóveda la adoptó, la bóveda es la que manda pero el nombre sigue
|
|
65
|
+
siendo el de la llave génesis. Lo que la bóveda pone en cada cert es su propia llave
|
|
66
|
+
(quien lo emitió), no el nombre de la cuenta.
|
|
30
67
|
|
|
31
|
-
|
|
68
|
+
**Qué autoriza qué, hoy.** Para firmar, leer y guardar, la autorización sale del
|
|
69
|
+
**cert** (se verifica la cadena hasta la llave de la bóveda). El **acta** se comprueba
|
|
70
|
+
*encima* del cert en los **secretos de servicios**, donde el `cn` del miembro tiene
|
|
71
|
+
que decir que es ese servicio: dos cierres independientes, así el límite no depende
|
|
72
|
+
solo de qué cert se emitió un día.
|
|
32
73
|
|
|
33
|
-
|
|
74
|
+
```sh
|
|
75
|
+
dotrino-vault members # el acta: qué llaves son tuyas y qué puede hacer cada una
|
|
76
|
+
dotrino-vault caps <ID> +firma # cambia permisos (+firma -guarda +lee …)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Los certificados caducan y se renuevan solos.** Un cert dura **30 días**. Mientras
|
|
80
|
+
siga vigente y no esté revocado, el dispositivo pide uno fresco por su cuenta —misma
|
|
81
|
+
llave, mismos permisos— sin QR ni aprobación. Un cert **vencido o revocado ya no se
|
|
82
|
+
renueva**: ahí toca volver a emparejar. Así una máquina que te robaron y revocaste
|
|
83
|
+
queda fuera en cuanto expira, sin depender de que ella se porte bien.
|
|
84
|
+
|
|
85
|
+
**Si se pierde la llave que sella, se pierde la cuenta.** No hay recuperación, ni
|
|
86
|
+
relevo, ni frase de respaldo: es la consecuencia asumida de que las llaves no se
|
|
87
|
+
copian. Por eso la bóveda **no te deja borrar un perfil que ella manda si quedan
|
|
88
|
+
otros dispositivos dentro**: primero le pasas el mando a uno que esté conectado. Si
|
|
89
|
+
lo borraras así, los demás se quedarían con su llave y sin nadie que pueda volver a
|
|
90
|
+
firmar el acta, y la cuenta moriría para todos sin avisar.
|
|
91
|
+
|
|
92
|
+
La cripto **no se reimplementa**: vive en `@dotrino/identity` (`signDelegation`,
|
|
93
|
+
`verifyChain`, `makeDeviceKey`, más `acta` y `content`) y el transporte es
|
|
94
|
+
`@dotrino/proxy-client`. Lo que sí vive aquí es el **lado-bóveda del emparejamiento**
|
|
95
|
+
(`lib/src/enroll.js`): un solo sitio donde se decide a quién se le emite un
|
|
96
|
+
certificado, compartido por el daemon del PC, por `@dotrino/vault` —que convierte un
|
|
97
|
+
navegador en bóveda— y por la copia vendorizada del iframe de identidad.
|
|
98
|
+
|
|
99
|
+
Diseño completo en [`docs/acta-de-perfil.md`](./docs/acta-de-perfil.md).
|
|
100
|
+
|
|
101
|
+
### Qué atiende la bóveda
|
|
102
|
+
|
|
103
|
+
Las peticiones de un dispositivo **ya enrolado** (`sign`, `store`, `get`, `devices`,
|
|
104
|
+
`renew`, `secrets`) van firmadas por él y con su certificado; se verifica la cadena,
|
|
105
|
+
que el cert no esté revocado y que la hora venga dentro de una ventana de **±5
|
|
106
|
+
minutos** (sin eso, un relay podía reproducir mensajes firmados viejos durante toda
|
|
107
|
+
la vida del cert). Los del emparejamiento son la excepción, porque ahí todavía no hay
|
|
108
|
+
cert: `hello` solo se contesta a quien presente el nonce de una sesión de
|
|
109
|
+
emparejamiento viva, y `enroll` va firmado por la llave del dispositivo como prueba
|
|
110
|
+
de posesión.
|
|
111
|
+
|
|
112
|
+
| Mensaje | Qué hace |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `hello` | entrega la llave de la bóveda a quien presenta el nonce de la invitación corta, firmado |
|
|
115
|
+
| `enroll` · `acta.sealed` | los dos caminos de vinculación (entrar a una cuenta / que la bóveda adopte la del aparato) |
|
|
116
|
+
| `sign` | la llave firma un payload y devuelve **solo la firma** |
|
|
117
|
+
| `store` | el contenido del usuario: hilos, "recientes" y perfil, cifrado de punta a punta |
|
|
118
|
+
| `get` | lee un nodo del árbol de contenidos (`vault.json`) |
|
|
119
|
+
| `devices` | la lista de dispositivos **más el acta** y la cadena de versiones que le falte |
|
|
120
|
+
| `renew` | cert fresco a 30 días, misma llave y mismo scope |
|
|
121
|
+
| `secrets` | el bundle de un servicio, sellado a una llave efímera y firmado |
|
|
122
|
+
|
|
123
|
+
Y emite `revoked` (autoborrado firmado) y `secrets.changed` (aviso de rotación).
|
|
124
|
+
|
|
125
|
+
Lo que pasa queda anotado en `activity.log` (JSONL, rotado a ~1 MB): qué dispositivo
|
|
126
|
+
firmó, renovó o enroló, y qué se rechazó y por qué, **sin el contenido de lo
|
|
127
|
+
firmado**. Se lee con `dotrino-vault activity`.
|
|
128
|
+
|
|
129
|
+
## Instalación
|
|
130
|
+
|
|
131
|
+
Tres vías, y todas dejan la misma bóveda: **instalador de Linux** (queda como
|
|
132
|
+
servicio), **un comando** (`npx`, en cualquier sistema con Node) y **Docker**. En
|
|
133
|
+
Linux la primera es la cómoda; en Windows y macOS todavía no hay instalador de un
|
|
134
|
+
clic, así que van por las otras dos.
|
|
135
|
+
|
|
136
|
+
**Ubuntu / Debian — `.deb`:** descarga el `.deb` (versionado) desde
|
|
34
137
|
[Releases](https://github.com/imdotrino/dotrino-vault/releases/latest) y haz doble
|
|
35
138
|
clic, o en la terminal:
|
|
36
139
|
|
|
@@ -38,7 +141,14 @@ clic, o en la terminal:
|
|
|
38
141
|
sudo apt install ./dotrino-vault_*.deb
|
|
39
142
|
```
|
|
40
143
|
|
|
41
|
-
|
|
144
|
+
Deja los binarios en `/usr/bin` e instala la unidad `systemd --user` en
|
|
145
|
+
`/usr/lib/systemd/user`, habilitada para **todos** los usuarios de la máquina (cada
|
|
146
|
+
uno con su propia bóveda en su `$HOME`): te arranca sola en tu **próximo inicio de
|
|
147
|
+
sesión**. Para levantarla ya, `systemctl --user start dotrino-vault`; si estabas
|
|
148
|
+
**actualizando**, el servicio viejo sigue corriendo y hay que reiniciarlo con
|
|
149
|
+
`systemctl --user restart dotrino-vault`.
|
|
150
|
+
|
|
151
|
+
**Otro Linux x64 con systemd — tarball:**
|
|
42
152
|
|
|
43
153
|
```sh
|
|
44
154
|
tar xzf dotrino-vault-*-linux-x64.tar.gz
|
|
@@ -46,87 +156,155 @@ cd dotrino-vault-*-linux-x64
|
|
|
46
156
|
sh install.sh
|
|
47
157
|
```
|
|
48
158
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
159
|
+
Hace lo equivalente en tu `$HOME` (`~/.local/bin` + `~/.config/systemd/user`) y va un
|
|
160
|
+
paso más allá: lo **arranca en el acto** y activa `linger`, así el vault corre desde
|
|
161
|
+
el arranque de la máquina aunque no inicies sesión.
|
|
162
|
+
|
|
163
|
+
El binario del tarball y el `.deb` son **x64/amd64** y el instalador **necesita
|
|
164
|
+
systemd** (si no lo encuentra, se detiene y te dice cómo arrancar el daemon a mano).
|
|
165
|
+
Para ARM —una Raspberry— o para un Linux sin systemd, usa Docker o `npx`.
|
|
166
|
+
|
|
167
|
+
Ambos traen **Node embebido**: no necesitas instalar Node ni dependencias de npm. Lo
|
|
168
|
+
único que esperan del sistema es `libatomic1`, que casi todas las distribuciones
|
|
169
|
+
traen puesta; el `.deb` la declara y la instala sola si falta. Con el tarball, en un
|
|
170
|
+
sistema muy pelado (un contenedor mínimo), instálala tú: `sudo apt install
|
|
171
|
+
libatomic1`.
|
|
172
|
+
|
|
173
|
+
**Cualquier sistema con Node — un comando:**
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
npx -y @dotrino/vaultd # Node ≥ 20
|
|
177
|
+
npx -y @dotrino/vaultd --tui # la bóveda y su pantalla de control en la misma ventana
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Es la vía normal en **Windows y macOS**, y sirve igual en Linux. Arranca en primer
|
|
181
|
+
plano: la bóveda vive mientras dejes esa ventana abierta. Los comandos de control se
|
|
182
|
+
anteponen con `npx -p @dotrino/vaultd` (por ejemplo, `npx -p @dotrino/vaultd
|
|
183
|
+
dotrino-vault pair`). Si no tienes Node, el instalador del ecosistema lo baja en tu
|
|
184
|
+
carpeta, sin permisos de administrador, y corre el paquete:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
# Linux y macOS
|
|
188
|
+
curl -fsSL https://install.dotrino.com/install.sh | sh -s -- @dotrino/vaultd
|
|
189
|
+
# Windows (PowerShell)
|
|
190
|
+
& ([scriptblock]::Create((irm https://install.dotrino.com/install.ps1))) @dotrino/vaultd
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Docker (cualquier sistema, y la única vía *empaquetada* para ARM — el `.deb` y el
|
|
194
|
+
tarball son solo x64; `npx` también sirve en una Raspberry):**
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
docker volume create dotrino-vault
|
|
198
|
+
docker run -d --name dotrino-vault --restart unless-stopped \
|
|
199
|
+
-v dotrino-vault:/data ghcr.io/imdotrino/dotrino-vault
|
|
200
|
+
|
|
201
|
+
docker exec -it dotrino-vault dotrino-vault pair # conectar un aparato
|
|
202
|
+
```
|
|
52
203
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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).
|
|
204
|
+
La imagen se publica en GHCR en cada versión, para amd64 y arm64 (una Raspberry
|
|
205
|
+
encendida en casa es el caso normal, no el exótico). No abre ningún puerto y el CLI
|
|
206
|
+
entra por `docker exec` porque le habla al daemon por archivos del dir de datos, no
|
|
207
|
+
por un socket. **El volumen ES tu cuenta:** si lo borras, esa identidad no se
|
|
208
|
+
recupera.
|
|
61
209
|
|
|
62
210
|
> Sin firma de código: tu sistema puede advertir que el binario no está firmado. Es
|
|
63
211
|
> autohospedado y de código abierto; en Linux solo necesita permiso de ejecución (el
|
|
64
|
-
> instalador lo da).
|
|
212
|
+
> instalador lo da). Hay un build de Windows (`packaging/build-win.sh`) pero **no se
|
|
213
|
+
> publica**: se construye desde Linux y no se ha probado en un Windows de verdad, y
|
|
214
|
+
> la inyección del blob invalida la firma de Microsoft, así que saltaría SmartScreen.
|
|
215
|
+
|
|
216
|
+
**Dónde vive todo y cómo se para.** Tus datos —llave incluida— viven en
|
|
217
|
+
`~/.local/share/dotrino/vault` (Linux y macOS), `%LOCALAPPDATA%\Dotrino\vault`
|
|
218
|
+
(Windows) o `/data` dentro del volumen (Docker), siempre con permisos `0600`/`0700` y
|
|
219
|
+
un subdirectorio `p/<id>/` por perfil. Se mueve con `DOTRINO_VAULT_DIR`. Arrancar y
|
|
220
|
+
parar depende de cómo lo instalaste: `systemctl --user {start,stop,restart}
|
|
221
|
+
dotrino-vault`, `docker restart dotrino-vault`, o cerrar la ventana del `npx` y
|
|
222
|
+
volver a correrlo. El propio CLI te dice cuál te toca cuando el daemon no está.
|
|
65
223
|
|
|
66
224
|
### CLI de control
|
|
67
225
|
|
|
68
226
|
```sh
|
|
69
227
|
dotrino-vault tui # interfaz de terminal a pantalla completa (ver abajo)
|
|
70
228
|
dotrino-vault status # estado del servicio + fingerprint
|
|
71
|
-
dotrino-vault pair #
|
|
72
|
-
dotrino-vault
|
|
73
|
-
dotrino-vault
|
|
229
|
+
dotrino-vault pair # empareja: muestra el QR (más la URL y un código pegable) y espera
|
|
230
|
+
dotrino-vault pair --new-account [nombre] # estrena una cuenta VACÍA aquí y mete al dispositivo en ELLA
|
|
231
|
+
dotrino-vault pair --adopt [nombre] # al revés: la bóveda se queda con la cuenta que trae el aparato
|
|
232
|
+
dotrino-vault pair --save [archivo] # además, escribe la invitación en un .dpair para llevarla
|
|
233
|
+
dotrino-vault pending # qué dispositivo está esperando (su identificador) y cómo aprobarlo
|
|
234
|
+
dotrino-vault approve <código> # aprueba tecleando los 6 dígitos que MUESTRA el dispositivo
|
|
74
235
|
dotrino-vault reject <deviceId> # rechaza un dispositivo pendiente
|
|
75
236
|
dotrino-vault devices # lista dispositivos enrolados / revocados
|
|
237
|
+
dotrino-vault members # el acta del perfil: qué llaves son tuyas y qué puede cada una
|
|
238
|
+
dotrino-vault caps <ID> ±permiso # cambia lo que puede un dispositivo (+firma -guarda +lee …)
|
|
76
239
|
dotrino-vault revoke <nonce> # revoca un dispositivo (le ordena autoborrarse)
|
|
240
|
+
dotrino-vault activity [n] # bitácora de seguridad: firmas, renovaciones, enrolados, rechazos
|
|
77
241
|
dotrino-vault pair --service <ns> # empareja un SERVICIO (proxy, geo…) con acceso SOLO a sus secretos
|
|
78
242
|
dotrino-vault secret set <ns> <CLAVE> <valor> # guarda un secreto para ese servicio
|
|
79
243
|
dotrino-vault secret rm <ns> <CLAVE> # borra un secreto
|
|
80
244
|
dotrino-vault secret list # nombres de secretos (nunca valores)
|
|
81
|
-
dotrino-vault logs #
|
|
245
|
+
dotrino-vault logs # últimas 40 líneas del servicio (journalctl; solo donde hay systemd)
|
|
246
|
+
dotrino-vault version # versión instalada (status avisa si el daemon quedó viejo)
|
|
82
247
|
```
|
|
83
248
|
|
|
249
|
+
`approve` recibe el **código**, no el `deviceId`: la bóveda no conoce el código —solo
|
|
250
|
+
su compromiso— y lo aprende cuando lo tecleas. El que sí recibe `deviceId` es
|
|
251
|
+
`reject`.
|
|
252
|
+
|
|
253
|
+
El `ns` de un secreto va en minúsculas (`[a-z0-9-]`, hasta 32), la clave en
|
|
254
|
+
MAYÚSCULAS_CON_GUION_BAJO (hasta 64) y el valor es texto de hasta 8 KB.
|
|
255
|
+
|
|
84
256
|
### Interfaz de terminal (TUI)
|
|
85
257
|
|
|
86
|
-
|
|
87
|
-
|
|
258
|
+
Las bóvedas, los dispositivos y los secretos también se manejan desde una **interfaz
|
|
259
|
+
de terminal a pantalla completa**, sin memorizar subcomandos. El acta (`members`,
|
|
260
|
+
`caps`) y la bitácora (`activity`) todavía no están ahí: para eso, la CLI.
|
|
88
261
|
|
|
89
262
|
```sh
|
|
90
263
|
dotrino-vault tui # binario instalado
|
|
264
|
+
dotrino-vaultd --tui # la bóveda y su interfaz en la MISMA ventana
|
|
265
|
+
# (si ya hay una corriendo, se engancha a ella)
|
|
91
266
|
node bin/dotrino-vault-tui.js # en desarrollo (o: npm run tui)
|
|
92
267
|
```
|
|
93
268
|
|
|
94
|
-
Como la CLI, la TUI **no abre la identidad ni la red**: le
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
269
|
+
Como la CLI, la TUI **no abre la identidad ni la red**: le deja la orden al daemon en
|
|
270
|
+
un archivo del dir de datos (la señal es solo para que la atienda al instante; el
|
|
271
|
+
daemon vigila la carpeta igual, porque en Windows no hay señales). El daemon debe
|
|
272
|
+
estar corriendo; si no, la TUI ofrece arrancarlo con `S` — solo sabe hacerlo por
|
|
273
|
+
systemd, así que en Docker o fuera de Linux arráncalo tú.
|
|
98
274
|
|
|
99
|
-
**Navegación en dos niveles**, para que siempre sea explícito de qué bóveda son
|
|
100
|
-
|
|
275
|
+
**Navegación en dos niveles**, para que siempre sea explícito de qué bóveda son los
|
|
276
|
+
dispositivos/variables que estás viendo:
|
|
101
277
|
|
|
102
278
|
1. **Bóvedas** es la pantalla de entrada: lista tus perfiles (`↑↓` mover, `Enter`
|
|
103
279
|
**entrar** a uno — lo activa si no lo estaba). Ahí también **conectas un
|
|
104
|
-
dispositivo** con `p` (sin entrar: activa la bóveda elegida y abre la pregunta
|
|
105
|
-
|
|
106
|
-
|
|
280
|
+
dispositivo** con `p` (sin entrar: activa la bóveda elegida y abre la pregunta de
|
|
281
|
+
a qué cuenta entra), creas una bóveda nueva, renombras, borras y pones/quitas/usas
|
|
282
|
+
la contraseña (candado).
|
|
107
283
|
2. Al entrar caes en sus **pestañas horizontales**, que cambias con `←→`:
|
|
108
284
|
- **Dispositivos (pares):** verlos, **emparejar** uno nuevo, **aprobar** con el
|
|
109
|
-
código que muestra el dispositivo, **rechazar** y **revocar**.
|
|
110
|
-
bóveda **pregunta primero a qué cuenta entra el dispositivo** —a esta, o a una
|
|
111
|
-
cuenta nueva que se estrena para él— y recién después muestra el QR, que además
|
|
112
|
-
dice de qué cuenta salió. (En la CLI: `dotrino-vault pair --new-account
|
|
113
|
-
[nombre]`.)
|
|
285
|
+
código que muestra el dispositivo, **rechazar** y **revocar**.
|
|
114
286
|
- **Scopes y variables (secretos):** ver los scopes y sus variables (nunca los
|
|
115
|
-
valores), **agregar** una variable (con su scope) y **quitar** una variable o
|
|
116
|
-
|
|
287
|
+
valores), **agregar** una variable (con su scope) y **quitar** una variable o un
|
|
288
|
+
scope entero.
|
|
289
|
+
|
|
290
|
+
Al emparejar, la bóveda **pregunta primero a qué cuenta entra el dispositivo** y
|
|
291
|
+
recién después muestra el QR, que además dice de qué cuenta salió. La pregunta ofrece
|
|
292
|
+
una tercera opción, «adoptar la cuenta que trae el dispositivo», que aparece
|
|
293
|
+
**desactivada**: el dispositivo todavía no sabe entregar la suya. Desde la CLI ese
|
|
294
|
+
camino ya se inicia con `dotrino-vault pair --adopt`.
|
|
117
295
|
|
|
118
|
-
`Esc` desde las pestañas vuelve a la lista de bóvedas
|
|
119
|
-
|
|
120
|
-
|
|
296
|
+
`Esc` desde las pestañas vuelve a la lista de bóvedas; `q` sale desde cualquier
|
|
297
|
+
pantalla, salvo mientras escribes en un campo o respondes una confirmación: ahí se
|
|
298
|
+
sale con `Esc` o `Ctrl+C`.
|
|
121
299
|
|
|
122
|
-
**Idioma (`l`).** La TUI está en **español e inglés** y la tecla `l` conmuta entre
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
300
|
+
**Idioma (`l`).** La TUI está en **español e inglés** y la tecla `l` conmuta entre los
|
|
301
|
+
dos en cualquier pantalla (incluida la de "el daemon no está corriendo"). El idioma
|
|
302
|
+
se recuerda en `prefs.json` del dir de datos, y al arrancar se decide en este orden:
|
|
303
|
+
`DOTRINO_LANG` (si la pones, manda en esa ejecución) → `prefs.json` → el locale del
|
|
304
|
+
sistema (`LC_ALL`, `LC_MESSAGES`, `LANGUAGE` o `LANG`) → español.
|
|
127
305
|
|
|
128
|
-
**Las teclas NO cambian con el idioma**: son mnemónicos en **inglés** y valen igual
|
|
129
|
-
|
|
306
|
+
**Las teclas NO cambian con el idioma**: son mnemónicos en **inglés** y valen igual en
|
|
307
|
+
español (solo se traduce la palabra que las explica en la barra de ayuda).
|
|
130
308
|
|
|
131
309
|
| Tecla | Acción | Dónde |
|
|
132
310
|
|---|---|---|
|
|
@@ -136,44 +314,52 @@ en español (solo se traduce la palabra que las explica en la barra de ayuda).
|
|
|
136
314
|
| `d` | delete — borrar la bóveda | Bóvedas |
|
|
137
315
|
| `p` | **pair — conectar un dispositivo** (desde Bóvedas entra directo, sin `Enter`) | Bóvedas · Dispositivos |
|
|
138
316
|
| `c` | change password — poner/cambiar la contraseña | Bóvedas |
|
|
139
|
-
| `x` | quitar: contraseña · dispositivo pendiente · variable/scope |
|
|
317
|
+
| `x` | quitar: contraseña · dispositivo pendiente · variable/scope | Bóvedas · Dispositivos · Scopes · Emparejar |
|
|
140
318
|
| `u` / `k` | unlock / locK — candado de la bóveda | Bóvedas |
|
|
141
319
|
| `a` | approve — aprobar el dispositivo | Dispositivos · Emparejar |
|
|
142
320
|
| `v` | reVoke — revocar un dispositivo enrolado | Dispositivos |
|
|
143
321
|
| `y` | yes — confirmar (también se acepta `s`) | confirmaciones |
|
|
144
|
-
| `
|
|
322
|
+
| `n` / `Esc` / `Enter` | no — cancelar la confirmación | confirmaciones |
|
|
323
|
+
| `Supr` | lo mismo que `d` (Bóvedas), `v` (Dispositivos) y `x` (Scopes) | listas |
|
|
324
|
+
| `RePág` `AvPág` `Inicio` `Fin` | mover de 5 en 5 · ir al principio o al final (en Emparejar, scroll del QR) | listas |
|
|
325
|
+
| `Ctrl+U` / `Ctrl+W` | limpiar el campo / borrar la última palabra | al escribir |
|
|
326
|
+
| `b` / `Esc` | back — volver | pestañas · Emparejar · ¿a qué cuenta entra? |
|
|
327
|
+
| `S` / `R` | arrancar el servicio / volver a comprobarlo | daemon detenido |
|
|
145
328
|
| `l` | language — español ⇄ English | todas |
|
|
146
329
|
| `q` | quit — salir | todas |
|
|
147
330
|
|
|
148
331
|
### Varios perfiles en el mismo PC
|
|
149
332
|
|
|
150
333
|
Puedes tener varias identidades tuyas en la misma máquina (p. ej. personal y
|
|
151
|
-
trabajo). Cada perfil es **una
|
|
334
|
+
trabajo). Cada perfil es **una cuenta distinta**: su propia llave, sus propios
|
|
152
335
|
dispositivos, sus propios datos y secretos — nada se cruza entre ellos. **Todos
|
|
153
|
-
atienden a la vez**: el perfil «activo» solo decide a cuál va un comando cuando no
|
|
154
|
-
|
|
336
|
+
atienden a la vez**: el perfil «activo» solo decide a cuál va un comando cuando no lo
|
|
337
|
+
dices con `--profile`, no apaga a los demás.
|
|
155
338
|
|
|
156
339
|
```sh
|
|
157
340
|
dotrino-vault profile ls # lista los perfiles (* = el activo)
|
|
158
341
|
dotrino-vault profile add Trabajo # crea un perfil (identidad nueva, vacía)
|
|
159
342
|
dotrino-vault profile use Trabajo # elige el activo
|
|
160
343
|
dotrino-vault profile rename <nombre> # renombra
|
|
161
|
-
dotrino-vault profile rm Trabajo # BORRA el perfil y su identidad (
|
|
344
|
+
dotrino-vault profile rm Trabajo # BORRA el perfil y su identidad (te pide escribir su nombre)
|
|
162
345
|
|
|
163
|
-
dotrino-vault pair --profile Trabajo # cualquier comando acepta --profile
|
|
164
|
-
dotrino-vault devices --profile personal
|
|
346
|
+
dotrino-vault pair --profile Trabajo # cualquier comando acepta --profile (o -p), en cualquier posición
|
|
347
|
+
dotrino-vault devices --profile personal # vale el id o el nombre (sin distinguir mayúsculas)
|
|
165
348
|
```
|
|
166
349
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
350
|
+
No se borra el único perfil, ni una cuenta que manda esta bóveda mientras le queden
|
|
351
|
+
otros dispositivos: primero le pasas el mando a uno conectado.
|
|
352
|
+
|
|
353
|
+
Si ya usabas el vault antes de esto, tu identidad de siempre se convierte sola en el
|
|
354
|
+
primer perfil («Perfil 1»): la misma llave, los mismos dispositivos, nada que volver
|
|
355
|
+
a emparejar.
|
|
170
356
|
|
|
171
357
|
### Contraseña del perfil (opcional)
|
|
172
358
|
|
|
173
359
|
Cada perfil puede llevar contraseña. **Solo se pide para EDITAR el perfil** (cambiar
|
|
174
360
|
tu nombre, avatar o datos): tus dispositivos siguen firmando, leyendo y guardando
|
|
175
|
-
aunque el perfil esté bloqueado — así un reinicio del PC nunca deja tus apps
|
|
176
|
-
|
|
361
|
+
aunque el perfil esté bloqueado — así un reinicio del PC nunca deja tus apps muertas
|
|
362
|
+
esperando a que alguien teclee algo.
|
|
177
363
|
|
|
178
364
|
```sh
|
|
179
365
|
dotrino-vault profile password # pone o cambia la contraseña (te la pregunta)
|
|
@@ -184,66 +370,188 @@ dotrino-vault lock # vuelve a bloquear
|
|
|
184
370
|
|
|
185
371
|
El perfil se vuelve a bloquear al reiniciar el servicio. La contraseña **no se
|
|
186
372
|
guarda**: solo un verificador con sal (PBKDF2), igual que el candado del navegador.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
**
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
El
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
373
|
+
Tiene un mínimo de 4 caracteres y, tras 5 intentos fallidos, cada intento nuevo
|
|
374
|
+
espera cada vez más (hasta 5 minutos); la cuenta de fallos se guarda, así que
|
|
375
|
+
reiniciar no la borra.
|
|
376
|
+
|
|
377
|
+
Con el perfil bloqueado, la CLI **no** te pide la contraseña sobre la marcha:
|
|
378
|
+
`profile rename`, `profile rm` y `profile password` fallan con «perfil bloqueado» y
|
|
379
|
+
hay que correr `dotrino-vault unlock` antes. (La TUI sí la pide sola cuando hace
|
|
380
|
+
falta.)
|
|
381
|
+
|
|
382
|
+
Para que quede claro qué protege y qué no: evita que otro que se siente en tu máquina
|
|
383
|
+
—o un dispositivo tuyo comprometido— te reescriba el perfil; **no** cifra la llave en
|
|
384
|
+
el disco (de eso se encarga el cifrado en reposo, más abajo, que hoy tampoco usa la
|
|
385
|
+
contraseña).
|
|
386
|
+
|
|
387
|
+
## Emparejar un aparato
|
|
388
|
+
|
|
389
|
+
**Vincular tiene exactamente dos caminos, y pregunta la bóveda antes de enseñar el
|
|
390
|
+
QR:**
|
|
391
|
+
|
|
392
|
+
- **El aparato entra a una cuenta de la bóveda** — la que elijas, o una nueva que se
|
|
393
|
+
estrena para él (`pair --new-account`). El aparato estrena una llave; nada de lo que
|
|
394
|
+
ya tenía se sobrescribe.
|
|
395
|
+
- **La bóveda adopta la cuenta del aparato** (`pair --adopt`): la cuenta sigue siendo
|
|
396
|
+
la misma para todo el mundo —el mismo `profileId`, la misma reputación— y lo que
|
|
397
|
+
cambia es quién sella. El aparato se queda como un miembro más.
|
|
398
|
+
|
|
399
|
+
**No existe fusionar dos cuentas**, ni al vincular ni después. El modo viaja dentro de
|
|
400
|
+
la invitación, el dispositivo lo repite firmado y la bóveda rechaza el que no coincida
|
|
401
|
+
con el que abrió. Detalle en
|
|
402
|
+
[`docs/vinculacion-de-cuentas.md`](./docs/vinculacion-de-cuentas.md).
|
|
403
|
+
|
|
404
|
+
**Emparejamiento endurecido.** El dispositivo prueba posesión de su llave firmando el
|
|
405
|
+
enrolamiento, y la bóveda solo firma el certificado **después** de que teclees el
|
|
406
|
+
**código de 6 dígitos que muestra el dispositivo**. El código no viaja: el dispositivo
|
|
407
|
+
lo sortea, lo enseña en su pantalla y manda solo su **compromiso**
|
|
408
|
+
`SHA-256(código‖llave‖sesión)`. La bóveda no lo conoce —no lo puede mostrar ni
|
|
409
|
+
comparar por su cuenta—; lo aprende cuando lo tecleas, recompone el compromiso y solo
|
|
410
|
+
si coincide firma el certificado. Aprobar exige, entonces, haber ido a leer la
|
|
411
|
+
pantalla del dispositivo. Al entregar el certificado la bóveda devuelve el código, y
|
|
412
|
+
el dispositivo lo rechaza si no es el suyo: una bóveda falsa, que nunca lo vio, no
|
|
413
|
+
puede enrolarlo. Un código robado ya no alcanza para entrar, y la revocación de un
|
|
414
|
+
dispositivo le ordena **autoborrarse** (con firma de la llave que manda, no por un
|
|
415
|
+
mensaje cualquiera).
|
|
416
|
+
|
|
417
|
+
> El porqué y las amenazas que cierra están en
|
|
418
|
+
> [`docs/pairing-protocol.md`](./docs/pairing-protocol.md). Ojo: ese documento es la
|
|
419
|
+
> decisión de diseño original y en dos puntos quedó atrás del código — el código de
|
|
420
|
+
> aprobación lo genera el dispositivo, no lo deriva la bóveda, y se aprueba
|
|
421
|
+
> tecleándolo, no comparándolo en dos pantallas.
|
|
422
|
+
|
|
423
|
+
**La cita del proxy.** Lo que va en el QR no es la dirección de la bóveda: es una
|
|
424
|
+
**cita** que emite el proxy — 6 caracteres, un solo uso, 5 minutos de vida; los 2
|
|
425
|
+
primeros dicen qué proxy la emitió, para poder canjearla desde otro nodo. El
|
|
426
|
+
dispositivo la canjea, obtiene la dirección real de la conexión y recién entonces le
|
|
427
|
+
pregunta a la bóveda quién es, presentando el nonce de la sesión; la respuesta va
|
|
428
|
+
firmada y con ese nonce dentro de lo firmado, así que no sirve la de otro
|
|
429
|
+
emparejamiento. Se gana doble: el QR no deja impresa ninguna dirección permanente, y
|
|
430
|
+
la dirección de verdad —34 caracteres, para poder rutearse entre proxies— no tiene que
|
|
431
|
+
caber en un código que se dicta.
|
|
432
|
+
|
|
433
|
+
La invitación viaja **comprimida** (`lib/src/invite.js`) y tiene dos formas. La que se
|
|
434
|
+
emite hoy es la **corta**: 21 caracteres, un enlace de 51, porque no lleva la llave ni
|
|
435
|
+
el nombre de la cuenta. Eso deja el QR en 29 módulos, que en la terminal son **39×20**.
|
|
436
|
+
Si el proxy no sabe emitir citas, la bóveda cae sola a la forma **compacta**: 91
|
|
437
|
+
caracteres, enlace de 121 y un QR de 41 módulos (51×26). Ahí sí viaja la llave, y el
|
|
438
|
+
grueso del ahorro es ella: va el **punto comprimido** de la curva (33 bytes) y el
|
|
439
|
+
lector rearma la JWK con una plantilla, comprobando que sale **byte a byte** igual,
|
|
440
|
+
porque el proxy direcciona por esa string exacta. Si una llave no encaja en ninguna
|
|
441
|
+
plantilla, la invitación sale en su forma larga (base64 del JSON): se hace grande,
|
|
442
|
+
nunca incorrecta. Las tres formas son una sola palabra en base64url, así que el mismo
|
|
443
|
+
texto sirve para el QR y para pegar.
|
|
444
|
+
|
|
445
|
+
## Lo que guardas: el store del usuario
|
|
446
|
+
|
|
447
|
+
Un dispositivo enrolado con el permiso de guardar (`vault:store`) usa la bóveda como
|
|
448
|
+
su almacén: **hilos** de contenido (agregar, listar, borrar, exportar/importar), el
|
|
449
|
+
contador de **aperturas** que alimenta los "recientes" del hub, y tu **perfil** (apodo,
|
|
450
|
+
avatar, campos), del que la bóveda es la copia autoritativa — cada dispositivo lo
|
|
451
|
+
empuja al editarlo y lo jala al arrancar, así que ves el mismo perfil en todos.
|
|
452
|
+
Espeja el modelo de datos de `@dotrino/store`, guardado en `threads.json` dentro del
|
|
453
|
+
dir del perfil. Las lecturas se conforman con `vault:read`; escribir exige
|
|
454
|
+
`vault:store`.
|
|
455
|
+
|
|
456
|
+
El contenido que guardan las apps viaja **cifrado de punta a punta** con la clave de
|
|
457
|
+
contenido de tu perfil: el dispositivo cifra antes de enviar y la bóveda responde
|
|
458
|
+
cifrada con la misma clave. El proxy transporta el sobre pero no puede leerlo. Si
|
|
459
|
+
esta bóveda no tiene la clave de contenido del perfil, rechaza la operación en vez de
|
|
460
|
+
guardar en claro.
|
|
461
|
+
|
|
462
|
+
La excepción, dicha sin adornos: la sincronización del **perfil**
|
|
463
|
+
(`profileSet`/`profileGet`) todavía viaja **sin cifrar**, y una operación que llegue
|
|
464
|
+
en claro se guarda en claro. Es deuda, no diseño.
|
|
465
|
+
|
|
466
|
+
El candado de contraseña solo bloquea **editar el perfil**: guardar y leer contenido
|
|
467
|
+
siguen funcionando con el perfil bloqueado.
|
|
468
|
+
|
|
469
|
+
(El otro store, el «árbol de contenidos» de `vault.json`, es hoy un esqueleto: se
|
|
470
|
+
puede leer con `vault.get`, pero ningún mensaje del protocolo escribe nodos.)
|
|
471
|
+
|
|
472
|
+
## Cifrado en reposo
|
|
473
|
+
|
|
474
|
+
**Todo** lo que la bóveda guarda va **cifrado** con AES-256-GCM y una clave derivada de
|
|
475
|
+
material de **esta máquina** (`/etc/machine-id` en Linux, `MachineGuid` en Windows,
|
|
476
|
+
`IOPlatformUUID` en macOS) más un salt local: `identity.json` (la maestra),
|
|
477
|
+
`vault.json` (el árbol de contenido), `threads.json` (hilos, aperturas y tu perfil) y
|
|
478
|
+
`secrets.json` (los secretos de servicios). Copiar cualquiera de esos archivos a otro
|
|
479
|
+
equipo **no sirve de nada**.
|
|
480
|
+
|
|
481
|
+
En la máquina del **servicio** pasa lo mismo con `service-identity.json`
|
|
482
|
+
(`@dotrino/vault/env`), que lleva la llave privada de su dispositivo.
|
|
483
|
+
|
|
484
|
+
La migración es sola y sin pedir nada: un archivo de una instalación anterior se lee
|
|
485
|
+
igual estando en claro y queda cifrado en la primera escritura (la identidad se migra
|
|
486
|
+
al arrancar, verificando que puede volver a leerse antes de reemplazar el original).
|
|
487
|
+
Queda fuera a propósito `activity.log`, la bitácora de auditoría: no guarda payloads
|
|
488
|
+
(solo op, dispositivo y hora) y se quiere legible para diagnosticar.
|
|
489
|
+
|
|
490
|
+
Lo que **no** resuelve, dicho sin adornos: no protege contra alguien con acceso a esta
|
|
491
|
+
misma máquina como tu usuario o como root — puede leer el mismo material que leemos
|
|
492
|
+
nosotros. Es subir el listón (de «copiar un archivo» a «tener tu máquina»), no una
|
|
493
|
+
imposibilidad criptográfica. Y hoy la **contraseña del perfil no participa** en esa
|
|
494
|
+
clave, aunque `machineKey` ya la acepta. Todos los archivos conservan además sus
|
|
495
|
+
permisos `0600` dentro de un dir `0700`.
|
|
211
496
|
|
|
212
497
|
## Desarrollo
|
|
213
498
|
|
|
214
499
|
```sh
|
|
215
|
-
npm install
|
|
216
|
-
node bin/dotrino-vaultd.js
|
|
217
|
-
node bin/dotrino-vaultd.js --pair
|
|
218
|
-
|
|
500
|
+
npm install # Node ≥ 20
|
|
501
|
+
node bin/dotrino-vaultd.js # arranca el daemon (modo servicio)
|
|
502
|
+
node bin/dotrino-vaultd.js --pair # arranca + imprime un QR de emparejamiento
|
|
503
|
+
node bin/dotrino-vaultd.js --tui # la bóveda y su pantalla de control, misma ventana
|
|
504
|
+
npm test # 118 pruebas (node --test, sin dependencias)
|
|
505
|
+
|
|
506
|
+
bash packaging/build.sh # binario único SEA + tarball de Linux (dist/)
|
|
507
|
+
bash packaging/build-deb.sh # el .deb (requiere dpkg-deb; construye el binario si falta)
|
|
508
|
+
bash packaging/build-win.sh # .exe + .zip de Windows, cruzado desde Linux (sin probar)
|
|
509
|
+
docker build -t dotrino-vault . # la imagen (la misma que CI publica en GHCR)
|
|
219
510
|
```
|
|
220
511
|
|
|
512
|
+
Los tests cubren lo que duele si se rompe: el emparejamiento y la comprobación del
|
|
513
|
+
código antes de firmar, la compresión de la invitación, el cifrado en reposo, el
|
|
514
|
+
aislamiento entre perfiles, el freno de borrado del acta, los secretos de punta a
|
|
515
|
+
punta y la precedencia del vault sobre el `.env`; también el render y el bilingüe de
|
|
516
|
+
la TUI. **Tres son de extremo a extremo contra el proxy de verdad** y esperan el repo
|
|
517
|
+
hermano en `../dotrino-proxy` (`secrets.e2e`, `multiprofile.e2e`,
|
|
518
|
+
`pairing-cita.e2e`): no se saltan solos, así que sin ese repo al lado `npm test`
|
|
519
|
+
falla.
|
|
520
|
+
|
|
521
|
+
Etiquetar `vaultd-v<ver>` publica la imagen de Docker en GHCR; el `.deb` y el tarball
|
|
522
|
+
se suben a la release a mano.
|
|
523
|
+
|
|
221
524
|
### Enrolar y usar desde un dispositivo (Node, para testing)
|
|
222
525
|
|
|
223
526
|
```js
|
|
224
|
-
import { enroll, requestSign } from '
|
|
527
|
+
import { enroll, requestSign } from './src/client.js'
|
|
225
528
|
|
|
226
|
-
// 1)
|
|
227
|
-
|
|
529
|
+
// 1) lees la invitación LARGA del vault → { iss, proxy, token, sn }
|
|
530
|
+
// (este helper es solo para pruebas: no sabe canjear la cita de la invitación
|
|
531
|
+
// corta. Ese camino lo implementan `lib/src/service.js` y `@dotrino/identity`.)
|
|
532
|
+
const { device, cert, iss } = await enroll({
|
|
533
|
+
qr,
|
|
534
|
+
onChallenge: ({ deviceId, code }) => console.log(deviceId, '→ teclea en el vault:', code)
|
|
535
|
+
}) // GUARDA device (privada) + cert
|
|
228
536
|
|
|
229
|
-
// 2) le pides a la
|
|
537
|
+
// 2) le pides a la bóveda que firme algo (su llave nunca sale)
|
|
230
538
|
const { signature } = await requestSign({
|
|
231
539
|
masterPubkey: iss, proxyUrl: qr.proxy, device, cert,
|
|
232
540
|
payload: { hola: 'mundo' }
|
|
233
541
|
})
|
|
234
542
|
```
|
|
235
543
|
|
|
236
|
-
|
|
544
|
+
## Secretos de servicios
|
|
237
545
|
|
|
238
|
-
Los servicios (proxy, geo, bots…) **no llevan secretos de terceros en
|
|
239
|
-
se enrolan
|
|
240
|
-
`vault:secrets:<ns>`
|
|
546
|
+
Los servicios del ecosistema (proxy, geo, bots…) **no llevan secretos de terceros en
|
|
547
|
+
su `.env`**: se enrolan a la bóveda como un miembro más, con un cert limitado al scope
|
|
548
|
+
`vault:secrets:<ns>` y un `cn` que el acta reconoce, y al arrancar piden su bundle.
|
|
549
|
+
Esto es la cara Node del paquete hermano **`@dotrino/vault`**; la documentación
|
|
550
|
+
completa está en [`lib/README.md`](./lib/README.md).
|
|
241
551
|
|
|
242
|
-
Son **dos momentos distintos, a propósito**: el **enrolamiento**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
#### 1) Enrolamiento — comando previo, una vez por máquina
|
|
552
|
+
Son **dos momentos distintos, a propósito**: el **enrolamiento** es un comando previo
|
|
553
|
+
que corre un humano **una sola vez** por máquina; el **arranque** solo lee la
|
|
554
|
+
identidad ya guardada y no interactúa con nadie.
|
|
247
555
|
|
|
248
556
|
```bash
|
|
249
557
|
# en el VAULT (tu PC)
|
|
@@ -251,92 +559,130 @@ dotrino-vault pair --service proxy # invitación con scope SOLO
|
|
|
251
559
|
dotrino-vault secret set proxy TURN_KEY_ID …
|
|
252
560
|
|
|
253
561
|
# en la MÁQUINA del servicio (pega la invitación; te MUESTRA un código)
|
|
254
|
-
npx dotrino-env enroll --ns proxy
|
|
562
|
+
npx -p @dotrino/vault dotrino-env enroll --ns proxy # el bin vive en @dotrino/vault
|
|
255
563
|
|
|
256
|
-
# de vuelta en el VAULT:
|
|
257
|
-
dotrino-vault approve
|
|
564
|
+
# de vuelta en el VAULT: tecleas los 6 dígitos LEYÉNDOLOS de la pantalla del servicio
|
|
565
|
+
dotrino-vault approve 418027
|
|
258
566
|
```
|
|
259
567
|
|
|
260
568
|
Deja `~/.dotrino/service/<ns>/service-identity.json` (0600) con la llave del
|
|
261
|
-
dispositivo (generada ahí, nunca sale) + el cert. **
|
|
569
|
+
dispositivo (generada ahí, nunca sale) + el cert. **En la máquina del servicio no
|
|
570
|
+
queda ningún secreto**: los valores viven solo en memoria del proceso. En la
|
|
571
|
+
**bóveda** sí quedan en disco (`secrets.json`, 0600, en claro). Un agente tiene **una
|
|
572
|
+
sola** identidad y se la cede el vault: volver a enrolar **reemplaza** la anterior, que
|
|
573
|
+
es la forma de rotar la de un agente comprometido.
|
|
262
574
|
|
|
263
575
|
**Por qué NO se enrola en el primer arranque de la app:** el enrolamiento exige un
|
|
264
576
|
humano que **lea el código en la pantalla del servicio** — es lo único que impide que
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
madrugada intentaría re-enrolar. Separados, el arranque es determinista e idempotente:
|
|
270
|
-
solo **lee**; el enrolamiento **escribe** y consume una invitación de un solo uso.
|
|
271
|
-
|
|
272
|
-
Va donde va el `npm ci` al aprovisionar el VPS. (Para máquinas efímeras —Docker,
|
|
273
|
-
autoescalado— haría falta una invitación pre-provisionada de un solo uso con TTL corto
|
|
274
|
-
y auto-aprobación; **aún no está decidido ni implementado**.)
|
|
275
|
-
|
|
276
|
-
#### 2) Arranque — sin interacción, en cada reinicio
|
|
577
|
+
una bóveda falsa (que nunca vio el código) enrole la máquina. Un servicio arranca bajo
|
|
578
|
+
systemd/PM2, sin TTY y sin nadie mirando: el código acabaría en un log. Separados, el
|
|
579
|
+
arranque es determinista e idempotente: solo **lee**; el enrolamiento **escribe** y
|
|
580
|
+
consume una invitación de un solo uso.
|
|
277
581
|
|
|
278
582
|
```js
|
|
279
|
-
import '@dotrino/vault/config' // como `dotenv/config`, pero contra el vault
|
|
583
|
+
import '@dotrino/vault/config' // como `dotenv/config`, pero contra el vault
|
|
584
|
+
// ns: DOTRINO_NS, o el único enrolado en la máquina
|
|
280
585
|
console.log(process.env.TURN_KEY_ID)
|
|
281
586
|
```
|
|
282
587
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
588
|
+
**Precedencia — el vault MANDA** (desde `@dotrino/vault` 0.14.0): lo que venga del
|
|
589
|
+
vault **pisa** el `.env` y el entorno. No lo reemplaza (el `.env` sigue arrancando
|
|
590
|
+
cualquier máquina sin enrolar), pero tiene la última palabra sobre las claves que
|
|
591
|
+
administra. Es lo que hace barata la **rotación**: se cambia en un solo lugar y ningún
|
|
592
|
+
`.env` rancio olvidado en un VPS puede seguir ganando.
|
|
593
|
+
|
|
594
|
+
**Al rotar, el agente SE REINICIA.** Cuando guardas o borras un secreto, la bóveda
|
|
595
|
+
manda a los agentes de ese `ns` un aviso **firmado** —sin valores— y agrupa las
|
|
596
|
+
escrituras seguidas para que cargar cinco variables no provoque cinco reinicios. El
|
|
597
|
+
agente **no recarga en caliente: termina**, y lo levanta su supervisor. Así que **el
|
|
598
|
+
servicio tiene que correr bajo pm2 o systemd con `Restart=always`**: sin supervisor, la
|
|
599
|
+
primera rotación lo deja apagado. Sale en vez de recargar porque en JavaScript un
|
|
600
|
+
secreto no se puede borrar de la memoria (los strings son inmutables, no hay
|
|
601
|
+
`zeroize`) y una llave se rota casi siempre *porque se filtró*: un proceso nuevo
|
|
602
|
+
empieza con el heap limpio.
|
|
603
|
+
|
|
604
|
+
**Modos de fallo:**
|
|
605
|
+
|
|
606
|
+
- **Vault caído / proxy caído** → **espera** (reintento con backoff, para siempre).
|
|
607
|
+
Esperar al vault es la **regla**: arrancar igual sería operar con la configuración
|
|
608
|
+
vieja del `.env`, que es justo lo que el vault vino a dejar de ser. La **única**
|
|
609
|
+
excepción es el **proxio**, y no por importancia sino por estructura: el vault habla
|
|
610
|
+
con sus servicios *por* el proxio, así que un proxio que lo espera espera a alguien
|
|
611
|
+
que necesita el proxio escuchando. Para ese caso está `applyEnv`.
|
|
612
|
+
- **Sin enrolar, cert revocado o vencido, scope equivocado, o un acta que no reconoce
|
|
613
|
+
a ese miembro como el servicio `<ns>`** → **aborta en el acto**. No se arreglan
|
|
614
|
+
reintentando: hay que (re)enrolar. El cert del servicio vive 30 días y se renueva
|
|
615
|
+
**al pedir los secretos** —o sea, al arrancar— cuando le quedan menos de 7: no hay
|
|
616
|
+
temporizador. Así que «vencido» pasa tanto si el agente estuvo apagado más de un mes
|
|
617
|
+
como si estuvo encendido más de un mes sin reiniciarse.
|
|
618
|
+
|
|
619
|
+
Revocar el cert corta el acceso: la bóveda emite un `REVOKED` **firmado** y el agente
|
|
620
|
+
que está a la escucha se apaga en el acto y no vuelve. Un agente sin escucha
|
|
621
|
+
(`DOTRINO_ENV_WATCH=0`, o `applyEnv` a secas) conserva en memoria lo que ya había leído
|
|
622
|
+
hasta que alguien lo reinicie.
|
|
623
|
+
|
|
624
|
+
| Variable | Para qué |
|
|
625
|
+
|---|---|
|
|
626
|
+
| `DOTRINO_NS` | namespace de secretos; si falta, el único enrolado en la máquina |
|
|
627
|
+
| `DOTRINO_ENV_OVERRIDE=0` | por esta corrida, gana el entorno (el vault deja de pisar) |
|
|
628
|
+
| `DOTRINO_ENV_WATCH=0` | no escuchar avisos de cambio (el proceso no termina al rotar) |
|
|
629
|
+
| `DOTRINO_ENV_DIR` | dónde está `service-identity.json` de ese ns |
|
|
630
|
+
| `DOTRINO_ENV_HOME` | raíz de las identidades de servicio (por defecto `~/.dotrino/service`) |
|
|
631
|
+
| `DOTRINO_ENV_QUIET=1` | no imprimir la línea de arranque |
|
|
632
|
+
|
|
633
|
+
CLI de apoyo: `dotrino-env status` (qué hay enrolado aquí), `dotrino-env check` (los
|
|
634
|
+
**nombres** de los secretos, nunca los valores), `dotrino-env run -- <cmd>` (inyecta
|
|
635
|
+
los secretos en el entorno de un proceso que no es Node). Primer consumidor:
|
|
636
|
+
`dotrino-proxy` (TURN de Cloudflare).
|
|
309
637
|
|
|
310
638
|
## Alcance
|
|
311
639
|
|
|
312
|
-
- **v1 (este):**
|
|
313
|
-
|
|
314
|
-
**
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
640
|
+
- **v1 (este):** daemon headless en Node, **multi-perfil**. En **Linux** queda como
|
|
641
|
+
servicio `systemd --user` (binario único Node SEA: `.deb` y tarball); en **Windows y
|
|
642
|
+
macOS** se corre con `npx`, en primer plano; y en cualquier sistema, con **Docker**
|
|
643
|
+
(imagen amd64/arm64 en GHCR). Identidad **cifrada en reposo y ligada a esta
|
|
644
|
+
máquina**. Emparejamiento endurecido con los dos caminos de vinculación, acta del
|
|
645
|
+
perfil (miembros, capacidades y `cn` de servicio), firma delegada, renovación
|
|
646
|
+
automática del cert a los 30 días, store de hilos/aperturas/perfil **cifrado de punta
|
|
647
|
+
a punta**, secretos de servicios sellados a una llave efímera, y bitácora de
|
|
648
|
+
actividad. Contraseña opcional por perfil, **para editarlo** (verificador PBKDF2).
|
|
649
|
+
- **v2:** cifrado en reposo **con la contraseña del perfil** (hoy la clave sale solo de
|
|
650
|
+
la máquina) y atado a tu cuenta del sistema —DPAPI en Windows, Keychain en macOS— o
|
|
651
|
+
al TPM; cifrar también los secretos y el store; **instalador de un clic para Windows
|
|
652
|
+
y macOS**; UI de escritorio (Tauri) como cliente del daemon; firma de documentos con
|
|
653
|
+
sellado de tiempo (`dotrino-signer`).
|
|
321
654
|
|
|
322
655
|
## Estructura
|
|
323
656
|
|
|
324
|
-
- `src/vault.js` — núcleo de UN perfil
|
|
657
|
+
- `src/vault.js` — núcleo de UN perfil: identidad + transporte + el router de mensajes.
|
|
325
658
|
- `src/profiles.js` — registro multi-perfil (`profiles.json`, `p/<id>/`) + candado por contraseña.
|
|
326
|
-
- `src/manager.js` — corre todos los perfiles a la vez (uno por
|
|
327
|
-
- `src/daemon.js` — modo servicio: `state.json`,
|
|
659
|
+
- `src/manager.js` — corre todos los perfiles a la vez (uno por llave/conexión) y aplica el freno de borrado.
|
|
660
|
+
- `src/daemon.js` — modo servicio: `state.json`, control por archivos + señales, apagado limpio.
|
|
328
661
|
- `src/ctl.js` — CLI de control (habla con el daemon por archivos + señales, sin socket).
|
|
329
662
|
- `src/vaultControl.js` — API de control programática (misma vía que la CLI); la usa la TUI.
|
|
330
|
-
- `src/tui/` — interfaz de terminal
|
|
331
|
-
- `src/transport.js` — conexión headless al proxy + `identify` firmado.
|
|
332
|
-
- `src/
|
|
333
|
-
- `src/
|
|
334
|
-
- `src/
|
|
335
|
-
- `
|
|
663
|
+
- `src/tui/` — interfaz de terminal, sin dependencias (`term.js` primitivas ANSI/raw-mode; `app.js` pantallas; `i18n.js` los textos es/en).
|
|
664
|
+
- `src/transport.js` — conexión headless al proxy + `identify` firmado (con el acta).
|
|
665
|
+
- `src/threadStore.js` — el store del usuario: hilos, "recientes" y perfil (`threads.json`).
|
|
666
|
+
- `src/store.js` — árbol de contenidos (`vault.json`, versionado). Hoy es un esqueleto: se lee, nadie escribe.
|
|
667
|
+
- `src/secretsStore.js` — secretos por namespace de servicio (`secrets.json`), validados.
|
|
668
|
+
- `src/atrest.js` — cifrado en reposo de la identidad, ligado a esta máquina.
|
|
669
|
+
- `src/client.js` — helper de **dispositivo** (enrolar / pedir firma / leer), para pruebas.
|
|
670
|
+
- `src/version.js` — de dónde sale la versión (binario SEA / npm / repo), para que `status` avise si el daemon quedó viejo.
|
|
671
|
+
- `src/node-globals.js` — los globals de navegador que los paquetes del ecosistema esperan al correr headless.
|
|
672
|
+
- `src/protocol.js` — re-export de `lib/src/protocol.js`, la **fuente única** de tipos de mensaje y scopes.
|
|
673
|
+
- `src/qr.js` — QR ASCII. · `src/paths.js` — dirs de datos por sistema.
|
|
674
|
+
- `lib/` — el paquete npm **`@dotrino/vault`** (bóveda en el navegador + cliente de servicio `dotrino-env`), versionado y publicado **aparte** de la raíz. Dentro: `enroll.js` (el lado-bóveda del emparejamiento, compartido), `invite.js` (la invitación), `service.js`/`env.js`/`config.js`, `sealed.js`, `protocol.js`.
|
|
336
675
|
- `bin/sea-entry.js` — entrypoint del binario único (multicall daemon / `--ctl` / `--tui`).
|
|
337
|
-
- `bin/dotrino-
|
|
338
|
-
- `
|
|
339
|
-
- `packaging/` — `build.sh` (binario), `install.sh`/`uninstall.sh`, unit systemd.
|
|
340
|
-
- `
|
|
676
|
+
- `bin/dotrino-vault.js` — el CLI de control instalado por npm. · `bin/dotrino-vaultd.js` y `bin/dotrino-vault-tui.js` — entrypoints de desarrollo.
|
|
677
|
+
- `vendor/qrcode-generator.cjs` — el encoder de QR vendorizado (MIT): nada de JS de terceros en runtime.
|
|
678
|
+
- `packaging/` — `build.sh` (binario SEA + tarball), `build-deb.sh`, `build-win.sh`, `install.sh`/`uninstall.sh`, unit systemd.
|
|
679
|
+
- `Dockerfile` — la imagen que publica `.github/workflows/docker.yml` en GHCR.
|
|
680
|
+
- `test/` — las pruebas (`npm test`, `node --test`, sin dependencias).
|
|
681
|
+
- `web/` — `vault.dotrino.com` (Vite + Vue): la página pública **y la consola «Dónde vive tu perfil»**, la única pantalla del ecosistema donde se ven y gestionan los dispositivos de un perfil. Sirve además las rutas del QR (`/d` y `/dispositivos`). La publica `.github/workflows/deploy.yml`.
|
|
682
|
+
- `docs/` — las decisiones de diseño, que mandan sobre el código:
|
|
683
|
+
- [`acta-de-perfil.md`](./docs/acta-de-perfil.md) — el modelo vigente: un perfil es un conjunto de llaves con un acta firmada por un solo sellador.
|
|
684
|
+
- [`pairing-protocol.md`](./docs/pairing-protocol.md) — el emparejamiento endurecido: por qué el token dejó de ser autoridad suficiente.
|
|
685
|
+
- [`vinculacion-de-cuentas.md`](./docs/vinculacion-de-cuentas.md) — los dos caminos al conectar un aparato, y por qué no existe fusionar cuentas.
|
|
686
|
+
- [`store-identity-architecture.md`](./docs/store-identity-architecture.md) — por qué la bóveda del PC hace de store además de identidad.
|
|
341
687
|
|
|
342
688
|
Sin anuncios, sin cuentas, sin rastreo. MIT · parte de Dotrino.
|