@dotrino/vaultd 0.124.1 → 0.125.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/lib/src/passwordLogins.js +24 -15
- package/package.json +2 -2
- package/src/ctl.js +13 -2
- package/src/daemon.js +1 -1
- package/src/vault.js +4 -17
- package/src/vaultControl.js +3 -6
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { server as opaquePorDefecto, suiteId as suitePorDefecto } from '@dotrino/opaque'
|
|
28
28
|
import { pubkeyId } from '@dotrino/identity/capabilities'
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
29
|
+
import { DEVICE_CAPS, capScope } from '@dotrino/identity/acta'
|
|
30
|
+
import { deviceIdOf } from './enroll.js'
|
|
31
31
|
import { bytesToB64url, b64urlToBytes } from './b64.js'
|
|
32
32
|
|
|
33
33
|
/** Un usuario es la parte de antes de la `@` en `nombre@AB12-CD34-EF56`. */
|
|
@@ -445,24 +445,35 @@ export function createLoginDesk ({ load, save, now = () => Date.now(), opaque =
|
|
|
445
445
|
* @param {object} opts
|
|
446
446
|
* `identity` la identidad que firma (`signDelegation`, `admitMember`)
|
|
447
447
|
* `logins` el escritorio de `createLoginDesk`
|
|
448
|
-
* `
|
|
449
|
-
*
|
|
448
|
+
* `caps` los PERMISOS del acta que lleva el aparato, cualquiera de `DEVICE_CAPS`;
|
|
449
|
+
* por defecto firmar, leer y guardar
|
|
450
450
|
*/
|
|
451
451
|
export async function registerLogin ({
|
|
452
452
|
identity, logins, user, upload, pub, encPub = null, label = '', blob,
|
|
453
|
-
|
|
453
|
+
caps = ['sign', 'read', 'store'], scope, replace = false
|
|
454
454
|
} = {}) {
|
|
455
455
|
if (replace) {
|
|
456
456
|
await logins.registerFinish({ user, upload, blob, replace: true })
|
|
457
457
|
return { ok: true, user, replaced: true }
|
|
458
458
|
}
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
// CUALQUIER PERMISO, y todos por el mismo camino (dueño, 2026-09-21: «al crear debe
|
|
460
|
+
// poder asignársele cualquier permiso»; regla dura «simplificar los procesos»). Antes
|
|
461
|
+
// llegaban como scopes del certificado con `unattended` aparte, `passwords` se rechazaba
|
|
462
|
+
// con `passwords-not-yet` y `passkeys` —que no tiene scope— no se podía asignar.
|
|
463
|
+
//
|
|
464
|
+
// `scope` ya no se acepta, y se dice: ignorarlo daría al aparato los permisos por defecto
|
|
465
|
+
// en vez de los que se eligieron, y nadie lo notaría.
|
|
466
|
+
if (scope !== undefined) {
|
|
467
|
+
throw Object.assign(new Error('registerLogin takes `caps` (account record permissions), not `scope`'), { code: 'use-caps' })
|
|
465
468
|
}
|
|
469
|
+
const pedidos = [...new Set(Array.isArray(caps) ? caps : [])]
|
|
470
|
+
const raros = pedidos.filter((c) => !DEVICE_CAPS.includes(c))
|
|
471
|
+
if (!pedidos.length || raros.length) {
|
|
472
|
+
throw Object.assign(new Error(`not device permissions: ${raros.join(', ') || '(none)'} — expected any of ${DEVICE_CAPS.join(', ')}`), { code: 'bad-caps' })
|
|
473
|
+
}
|
|
474
|
+
// El certificado lleva el scope de los permisos que lo tienen; los demás (`unattended`,
|
|
475
|
+
// `passkeys`) viven solo en el acta, que es quien decide.
|
|
476
|
+
const scopes = pedidos.map((c) => capScope(c)).filter(Boolean)
|
|
466
477
|
if (typeof identity?.admitMember !== 'function') {
|
|
467
478
|
throw Object.assign(new Error('this vault cannot add devices to the account record: no login was created'), { code: 'admit-unavailable' })
|
|
468
479
|
}
|
|
@@ -470,11 +481,9 @@ export async function registerLogin ({
|
|
|
470
481
|
await logins.registerFinish({ user, upload, pub, encPub, deviceId, label, blob })
|
|
471
482
|
try {
|
|
472
483
|
const { cert } = await identity.signDelegation(pub, scopes, { label: label || `login:${user}` })
|
|
473
|
-
|
|
474
|
-
const caps = [...new Set([...scopeToCaps(scopes), ...(unattended ? ['unattended'] : [])])]
|
|
475
|
-
await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn, caps, cert })
|
|
484
|
+
await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn: null, caps: pedidos, cert })
|
|
476
485
|
logins.setCert({ user, cert, iss: identity.me?.publickey || null })
|
|
477
|
-
return { ok: true, user, deviceId, cert, caps }
|
|
486
|
+
return { ok: true, user, deviceId, cert, caps: pedidos }
|
|
478
487
|
} catch (e) {
|
|
479
488
|
// NADA DE MEDIAS ALTAS: si no entra en el acta, no queda un usuario que pueda entrar a
|
|
480
489
|
// una cuenta que no lo reconoce. Se deshace y se dice.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.125.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@dotrino/compat": "0.4.0",
|
|
25
25
|
"@dotrino/identity": "0.92.0",
|
|
26
26
|
"@dotrino/opaque": "0.1.0",
|
|
27
|
-
"@dotrino/passmanager": "0.
|
|
27
|
+
"@dotrino/passmanager": "0.17.0",
|
|
28
28
|
"@dotrino/proxy-client": "0.23.1",
|
|
29
29
|
"@dotrino/roadmap": "0.3.3",
|
|
30
30
|
"@dotrino/store": "0.11.0",
|
package/src/ctl.js
CHANGED
|
@@ -1620,7 +1620,18 @@ async function cmdLogins (rest) {
|
|
|
1620
1620
|
}
|
|
1621
1621
|
case 'add': {
|
|
1622
1622
|
needUser()
|
|
1623
|
-
|
|
1623
|
+
// `±permiso` se lee con las MISMAS palabras que `caps <ID> ±permiso`, sobre los de
|
|
1624
|
+
// siempre (firma, lee, guarda); lo demás es el nombre del equipo. Antes el texto de
|
|
1625
|
+
// ayuda lo anunciaba y aquí se juntaba todo como nombre.
|
|
1626
|
+
const caps = new Set(['sign', 'read', 'store'])
|
|
1627
|
+
const nombre = []
|
|
1628
|
+
for (const a of args.slice(1)) {
|
|
1629
|
+
if (a[0] !== '+' && a[0] !== '-') { nombre.push(a); continue }
|
|
1630
|
+
const cap = CAP_BY_WORD[a.slice(1).toLowerCase()]
|
|
1631
|
+
if (!cap) { console.error('permiso no reconocido: %s', a); process.exit(2) }
|
|
1632
|
+
if (a[0] === '+') caps.add(cap); else caps.delete(cap)
|
|
1633
|
+
}
|
|
1634
|
+
const label = nombre.join(' ').trim() || 'equipo prestado'
|
|
1624
1635
|
const { client: opaqueClient } = await import('@dotrino/opaque')
|
|
1625
1636
|
const { makeDeviceKey, makeDeviceEncKey } = await import('@dotrino/identity/capabilities')
|
|
1626
1637
|
const { sealDeviceKeys } = await import('../lib/src/passwordLogins.js')
|
|
@@ -1635,7 +1646,7 @@ async function cmdLogins (rest) {
|
|
|
1635
1646
|
const enc = await makeDeviceEncKey()
|
|
1636
1647
|
const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.privateJwk })
|
|
1637
1648
|
const r = await loginsRequest('register-finish', {
|
|
1638
|
-
user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob
|
|
1649
|
+
user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob, caps: [...caps]
|
|
1639
1650
|
})
|
|
1640
1651
|
const { loginAddress } = await import('../lib/src/passwordLogins.js')
|
|
1641
1652
|
console.log('\nListo. Se entra con esta dirección y esa contraseña:')
|
package/src/daemon.js
CHANGED
|
@@ -402,7 +402,7 @@ export async function runDaemon () {
|
|
|
402
402
|
case 'register-begin': return vault.loginRegisterBegin({ user: req.user, request: req.request, replace: !!req.replace })
|
|
403
403
|
case 'register-finish': return vault.loginRegisterFinish({
|
|
404
404
|
user: req.user, upload: req.upload, pub: req.pub, encPub: req.encPub || null,
|
|
405
|
-
label: req.label || '', blob: req.blob,
|
|
405
|
+
label: req.label || '', blob: req.blob, ...(req.caps ? { caps: req.caps } : {}), replace: !!req.replace
|
|
406
406
|
})
|
|
407
407
|
// Entrar desde la propia máquina de la bóveda. Lo usa `logins passwd`: para volver a
|
|
408
408
|
// cerrar el paquete de llaves hay que ABRIRLO antes, y eso solo puede hacerlo quien
|
package/src/vault.js
CHANGED
|
@@ -27,7 +27,7 @@ import { startSealersPublisher } from './sealers.js'
|
|
|
27
27
|
import { assertKeyOwnsDir } from './keyowner.js'
|
|
28
28
|
import { openStore } from './store.js'
|
|
29
29
|
import { openThreadStore, STORE_READ_METHODS, PROFILE_EDIT_METHODS } from './threadStore.js'
|
|
30
|
-
import { openSecretsStore, assertVar, RECOVERY as RECOVERY_WRAP,
|
|
30
|
+
import { openSecretsStore, assertVar, RECOVERY as RECOVERY_WRAP, PROFILE_OWNER } from './secretsStore.js'
|
|
31
31
|
import { openSubacta } from './subacta.js'
|
|
32
32
|
import { makeEphemeralKey, openSealed } from '../lib/src/sealed.js'
|
|
33
33
|
import { VERSION } from './version.js'
|
|
@@ -2785,8 +2785,8 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2785
2785
|
}
|
|
2786
2786
|
|
|
2787
2787
|
/**
|
|
2788
|
-
*
|
|
2789
|
-
* envoltura que falta.
|
|
2788
|
+
* TODO lo que está a deber, por cajón: las rotaciones anotadas y, CALCULADO, cada
|
|
2789
|
+
* envoltura que falta. La falta de una envoltura no se anota nunca: se mira. Una nota
|
|
2790
2790
|
* se desincroniza en cuanto alguien salda la deuda por un camino que no pasa por donde
|
|
2791
2791
|
* se anotó (pasó dos veces: el reparto por el hermano y el rehacer al abrir), y
|
|
2792
2792
|
* entonces la consola avisa de algo que ya no existe. Lo que se calcula no miente.
|
|
@@ -2796,11 +2796,6 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
2796
2796
|
const out = rotationsDue()
|
|
2797
2797
|
for (const m of await incompleteMembers()) {
|
|
2798
2798
|
for (const [owner, keys] of Object.entries(m.owners)) {
|
|
2799
|
-
// El perfil tampoco va aquí: esto es lo que se enseña como «variables sin
|
|
2800
|
-
// entregar». Lo que un aparato no puede abrir del perfil sigue a la vista en SU
|
|
2801
|
-
// fila (`incomplete`), y `secret settle` y abrir la bóveda lo saldan igual, porque
|
|
2802
|
-
// leen `incompleteMembers` y no esto.
|
|
2803
|
-
if (owner === PROFILE_OWNER) continue
|
|
2804
2799
|
if (out[owner]?.kind === 'rotate') continue
|
|
2805
2800
|
out[owner] = out[owner] || { kind: 'rewrap', members: [] }
|
|
2806
2801
|
out[owner].members.push({ pub: m.pub, keys })
|
|
@@ -3414,15 +3409,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
3414
3409
|
// sobre, «pública» dejó de significar «en claro»: dice a quién se le despacha sin
|
|
3415
3410
|
// aprobación, nada más. Para ver un valor hay que poder abrirlo, igual que cualquiera —
|
|
3416
3411
|
// y en un cajón con dueño, quien administra deliberadamente no puede.
|
|
3417
|
-
|
|
3418
|
-
// EL PERFIL NO ES UN GRUPO DE VARIABLES (dueño, 2026-09-21). `@me` vive en este
|
|
3419
|
-
// almacén porque se guarda igual —mismo sobre, mismo llavero—, pero no es configuración
|
|
3420
|
-
// de nadie: salía en `secret list` como «scope vault:secrets:@me», un scope que por
|
|
3421
|
-
// diseño no puede existir, y en la pantalla de variables como un grupo más. El perfil
|
|
3422
|
-
// tiene su sitio (`dotrino-vault me`, profile.dotrino.com).
|
|
3423
|
-
const variables = secrets.list()
|
|
3424
|
-
delete variables[PROFILE_NS]
|
|
3425
|
-
return variables
|
|
3412
|
+
return secrets.list()
|
|
3426
3413
|
}
|
|
3427
3414
|
/** Cambiar SOLO quién puede ver el valor (no toca el valor ni avisa: el servicio lee lo mismo). */
|
|
3428
3415
|
async function setSecretVisibility (ns, key, isPublic, adminKey) {
|
package/src/vaultControl.js
CHANGED
|
@@ -291,7 +291,6 @@ export async function listLogins (profile) {
|
|
|
291
291
|
export async function addLogin ({ user, password, label = 'equipo prestado', caps = ['sign', 'read', 'store'] } = {}) {
|
|
292
292
|
const { client: opaque } = await import('@dotrino/opaque')
|
|
293
293
|
const { makeDeviceKey, makeDeviceEncKey } = await import('@dotrino/identity/capabilities')
|
|
294
|
-
const { capScope } = await import('@dotrino/identity/acta')
|
|
295
294
|
const { sealDeviceKeys } = await import('../lib/src/passwordLogins.js')
|
|
296
295
|
const start = opaque.registrationStart({ password })
|
|
297
296
|
const begun = await loginsOp('register-begin', { user, request: start.request })
|
|
@@ -299,12 +298,10 @@ export async function addLogin ({ user, password, label = 'equipo prestado', cap
|
|
|
299
298
|
const device = await makeDeviceKey({ label })
|
|
300
299
|
const enc = await makeDeviceEncKey()
|
|
301
300
|
const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.privateJwk })
|
|
302
|
-
//
|
|
303
|
-
//
|
|
301
|
+
// Los PERMISOS del acta, tal cual: cualquiera, y todos por el mismo camino. Traducirlos a
|
|
302
|
+
// los scopes del certificado es cosa del pilar (`registerLogin`), no de esta pantalla.
|
|
304
303
|
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')
|
|
304
|
+
user, upload: fin.upload, pub: device.publickey, encPub: enc.publickey, label, blob, caps
|
|
308
305
|
})
|
|
309
306
|
}
|
|
310
307
|
|