@dotrino/vaultd 0.122.0 → 0.123.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.
@@ -49,7 +49,11 @@ const F = {
49
49
  secretReq: 'secret-request.json',
50
50
  profileReq: 'profile-request.json',
51
51
  meReq: 'me-request.json',
52
- dumpReq: 'dump-request.json'
52
+ dumpReq: 'dump-request.json',
53
+ // ENTRAR CON USUARIO Y CONTRASEÑA. La respuesta NO se llama `logins.json`: ese nombre ya
54
+ // es el almacén del escritorio dentro del dir del perfil, y pisarlo los borraría.
55
+ loginsReq: 'logins-request.json',
56
+ loginsList: 'logins-list.json'
53
57
  }
54
58
 
55
59
  const p = (name) => path.join(dir, name)
@@ -242,6 +246,96 @@ export const setProfilePassword = (profile, password, current) => profileOp('pas
242
246
  // Quitarla también la pide: se re-sella a la llave de la máquina antes de que se vaya.
243
247
  export const removeProfilePassword = (profile, password) => profileOp('password-rm', { profile, password })
244
248
 
249
+ // ---------------------------------------------------------------------------
250
+ // Inicios de sesión con contraseña (`docs/temporary-access.md`)
251
+ // ---------------------------------------------------------------------------
252
+
253
+ /**
254
+ * Un viaje al mostrador de inicios de sesión del daemon. Es el MISMO canal que usa el CLI
255
+ * (`dotrino-vault logins`) —una pieza, no dos— y por eso la pantalla de la TUI y el
256
+ * comando no se pueden desincronizar.
257
+ *
258
+ * La respuesta se BORRA al leerla: puede traer el paquete de llaves de un aparato.
259
+ */
260
+ export async function loginsOp (op, extra = {}) {
261
+ requireAlive()
262
+ rm(F.loginsList)
263
+ const since = Date.now()
264
+ const id = await writeReq(F.loginsReq, { op, ...extra })
265
+ signalOrCleanup('SIGUSR2', [F.loginsReq])
266
+ const d = await waitFor(F.loginsList, { req: id, since })
267
+ rm(F.loginsList)
268
+ if (!d) throw coded('the daemon did not reply', 'NO_REPLY')
269
+ if (d.ok === false) {
270
+ // `too-many-tries` trae cuánto falta, y `PROFILE_LOCKED` se arregla abriendo la bóveda:
271
+ // los dos cambian lo que hay que hacer, así que viajan con su dato.
272
+ throw Object.assign(coded(d.error || 'logins: the vault refused', d.code), { waitMs: d.waitMs })
273
+ }
274
+ return d
275
+ }
276
+
277
+ /** Los inicios de sesión de este perfil, con la dirección ya montada. */
278
+ export async function listLogins (profile) {
279
+ const { logins = [], fingerprint } = await loginsOp('list', profile ? { profile } : {})
280
+ const { loginAddress } = await import('../lib/src/passwordLogins.js')
281
+ return logins.map((l) => ({ ...l, address: fingerprint ? loginAddress(l.user, fingerprint) : '' }))
282
+ }
283
+
284
+ /**
285
+ * ALTA. Las llaves del aparato NACEN aquí, en esta terminal, y salen ya cerradas con lo
286
+ * que deriva de la contraseña: lo que la bóveda guarda es un paquete que no puede abrir.
287
+ *
288
+ * Se pueden tener VARIOS por perfil, cada uno con sus permisos: son miembros del acta como
289
+ * cualquier otro aparato.
290
+ */
291
+ export async function addLogin ({ user, password, label = 'equipo prestado', caps = ['sign', 'read', 'store'] } = {}) {
292
+ const { client: opaque } = await import('@dotrino/opaque')
293
+ const { makeDeviceKey, makeDeviceEncKey } = await import('@dotrino/identity/capabilities')
294
+ const { capScope } = await import('@dotrino/identity/acta')
295
+ const { sealDeviceKeys } = await import('../lib/src/passwordLogins.js')
296
+ const start = opaque.registrationStart({ password })
297
+ const begun = await loginsOp('register-begin', { user, request: start.request })
298
+ const fin = opaque.registrationFinish({ state: start.state, response: begun.response, password })
299
+ const device = await makeDeviceKey({ label })
300
+ const enc = await makeDeviceEncKey()
301
+ const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.privateJwk })
302
+ // El acta habla de PERMISOS y el papel de SCOPES: los traduce el pilar, no esta pantalla.
303
+ // `unattended` no es un scope —es «no me pidas aprobación»— y por eso viaja aparte.
304
+ return loginsOp('register-finish', {
305
+ user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob,
306
+ scope: caps.filter((c) => c !== 'unattended').map((c) => capScope(c)).filter(Boolean),
307
+ unattended: caps.includes('unattended')
308
+ })
309
+ }
310
+
311
+ /**
312
+ * Cambiar la contraseña es ABRIR y volver a cerrar, así que hace falta la vieja: el
313
+ * paquete de llaves está cerrado con lo que salía de ella, y sin abrirlo no hay qué
314
+ * volver a cerrar. Perderla no se arregla desde aquí — se quita el aparato y se crea otro.
315
+ */
316
+ export async function passwdLogin ({ user, oldPassword, newPassword } = {}) {
317
+ const { client: opaque } = await import('@dotrino/opaque')
318
+ const { sealDeviceKeys, openDeviceKeys } = await import('../lib/src/passwordLogins.js')
319
+ const start = opaque.loginStart({ password: oldPassword })
320
+ const begun = await loginsOp('login-begin', { user, request: start.request })
321
+ let fin
322
+ try { fin = opaque.loginFinish({ state: start.state, response: begun.response, password: oldPassword }) }
323
+ catch (_) { throw coded('wrong password', 'login-failed') }
324
+ const entrada = await loginsOp('login-end', { lid: begun.lid, finalization: fin.finalization, label: 'la consola' })
325
+ const keys = await openDeviceKeys(fin.exportKey, entrada.blob)
326
+ const reg = opaque.registrationStart({ password: newPassword })
327
+ const { response } = await loginsOp('register-begin', { user, request: reg.request, replace: true })
328
+ const nueva = opaque.registrationFinish({ state: reg.state, response, password: newPassword })
329
+ return loginsOp('register-finish', {
330
+ user, upload: nueva.upload, blob: await sealDeviceKeys(nueva.exportKey, keys), replace: true
331
+ })
332
+ }
333
+
334
+ export const closeLoginSession = (user, sid = null) => loginsOp('close', { user, ...(sid ? { sid } : {}) })
335
+ export const unblockLogin = (user) => loginsOp('unblock', { user })
336
+ /** Quitarlo lo saca del mostrador **y su llave del acta**: las dos cosas, o ninguna. */
337
+ export const removeLogin = (user) => loginsOp('rm', { user })
338
+
245
339
  // ---------------------------------------------------------------------------
246
340
  // Volcado de dispositivos + secretos de un perfil
247
341
  // ---------------------------------------------------------------------------