@dotrino/vaultd 0.140.0 → 0.141.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/ctl.js +33 -2
- package/src/tui/app.js +160 -5
- package/src/tui/i18n.js +58 -4
- package/src/vaultControl.js +8 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.141.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Certificador personal de Dotrino: daemon headless que custodia la clave maestra y delega capacidades a tus dispositivos por el proxy. Tu CA propia.",
|
|
6
6
|
"bin": {
|
package/src/ctl.js
CHANGED
|
@@ -140,7 +140,7 @@ function cmdStatus () {
|
|
|
140
140
|
// corriendo es más viejo que el CLI, avisar (nos mordió 3 veces).
|
|
141
141
|
if (s.version && s.version !== VERSION) {
|
|
142
142
|
console.log(' ⚠ el servicio corre la versión %s (binario instalado: %s).', s.version, VERSION)
|
|
143
|
-
console.log('
|
|
143
|
+
console.log(' Si acabas de actualizar, se pone al día solo en uno o dos minutos. Si no: %s', RESTART_HINT)
|
|
144
144
|
}
|
|
145
145
|
console.log(' fingerprint : %s', s.fingerprint)
|
|
146
146
|
console.log(' proxy : %s', s.proxy)
|
|
@@ -1732,6 +1732,30 @@ async function cmdLogins (rest) {
|
|
|
1732
1732
|
* Instalar no reinicia nada a mano: el daemon vigila su propio binario y, al cambiar, se
|
|
1733
1733
|
* va para que systemd lo levante con el nuevo (`src/selfupdate.js`).
|
|
1734
1734
|
*/
|
|
1735
|
+
/**
|
|
1736
|
+
* ESPERA A QUE EL SERVICIO CORRA LA VERSIÓN INSTALADA, y lo dice. El servicio se reinicia
|
|
1737
|
+
* solo cuando ve el binario nuevo (`selfupdate.js`), pero espera a verlo quieto dos minutos
|
|
1738
|
+
* seguidos: decir «se reinicia solo, compruébalo con status» y que `status` contestara un
|
|
1739
|
+
* minuto después «reinicia para actualizarlo» era mandar a hacer a mano lo que ya iba a pasar.
|
|
1740
|
+
*/
|
|
1741
|
+
async function waitForService (version, { timeoutMs = 4 * 60_000 } = {}) {
|
|
1742
|
+
const s0 = ipcRead(stateFile, null)
|
|
1743
|
+
if (!s0 || !alive(s0.pid)) {
|
|
1744
|
+
console.log('El servicio no está corriendo: arráncalo y ya será la %s. %s', version, START_HINT)
|
|
1745
|
+
return false
|
|
1746
|
+
}
|
|
1747
|
+
process.stdout.write('El servicio la arranca solo en uno o dos minutos. Esperando… ')
|
|
1748
|
+
const t0 = Date.now()
|
|
1749
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
1750
|
+
const s = ipcRead(stateFile, null)
|
|
1751
|
+
if (s?.version === version && alive(s.pid)) { console.log('✓ ya corre la %s', version); return true }
|
|
1752
|
+
await sleep(3000)
|
|
1753
|
+
}
|
|
1754
|
+
console.log('')
|
|
1755
|
+
console.error('El servicio sigue sin correr la %s después de %d min. Reinícialo: %s', version, Math.round(timeoutMs / 60000), RESTART_HINT)
|
|
1756
|
+
return false
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1735
1759
|
async function cmdUpdate (args = []) {
|
|
1736
1760
|
const { latestRelease, assetFor, download, verifyArtifact, isNewer, isUserInstall, installUserRelease } = await import('./update.js')
|
|
1737
1761
|
const soloMirar = args.includes('--check')
|
|
@@ -1763,7 +1787,8 @@ async function cmdUpdate (args = []) {
|
|
|
1763
1787
|
process.exit(1)
|
|
1764
1788
|
}
|
|
1765
1789
|
console.log('firmada por el release de %s ✓', 'imdotrino/dotrino-vault')
|
|
1766
|
-
console.log('Instalada la %s.
|
|
1790
|
+
console.log('Instalada la %s.', res.version)
|
|
1791
|
+
if (!args.includes('--no-wait')) await waitForService(res.version)
|
|
1767
1792
|
return
|
|
1768
1793
|
}
|
|
1769
1794
|
|
|
@@ -2110,6 +2135,8 @@ function help () {
|
|
|
2110
2135
|
|
|
2111
2136
|
tui interfaz de terminal a pantalla completa (bóvedas, pares, secretos)
|
|
2112
2137
|
status | info estado del servicio + fingerprint + el id de cada perfil
|
|
2138
|
+
update [--check] trae la versión nueva (verificando la firma) y espera a que el
|
|
2139
|
+
servicio la arranque · --check solo mira · --no-wait no espera
|
|
2113
2140
|
pair [--save <f>] inicia un emparejamiento (QR + espera); --save escribe la invitación (.dpair)
|
|
2114
2141
|
pair --kms <config.json>
|
|
2115
2142
|
el sitio que se cree (--adopt o --new-account) NACE con su clave
|
|
@@ -2320,6 +2347,10 @@ async function cmdReplica (args) {
|
|
|
2320
2347
|
|
|
2321
2348
|
export async function runCtl (argv) {
|
|
2322
2349
|
const [cmd, ...rest] = takeProfileFlag(argv)
|
|
2350
|
+
// `--help` EN CUALQUIER COMANDO ENSEÑA LA AYUDA Y NO HACE NADA. Ningún subcomando lo miraba,
|
|
2351
|
+
// así que se ejecutaban igual: `pair --help` lanzaba una invitación real y `update --help`
|
|
2352
|
+
// instalaba la versión nueva (2026-10-05). Mirar cómo se usa algo no puede cambiar nada.
|
|
2353
|
+
if (rest.includes('--help') || rest.includes('-h')) return help()
|
|
2323
2354
|
switch (cmd) {
|
|
2324
2355
|
case 'tui': return cmdTui()
|
|
2325
2356
|
case 'profile': return cmdProfile(rest)
|
package/src/tui/app.js
CHANGED
|
@@ -290,12 +290,49 @@ function profileRows (st, t) {
|
|
|
290
290
|
const list = st.profiles?.profiles || []
|
|
291
291
|
return list.map((p) => {
|
|
292
292
|
const mark = p.current ? t.accent('●') : ' '
|
|
293
|
-
const lk = !p.protected
|
|
293
|
+
const lk = !p.protected
|
|
294
|
+
? t.muted(i.noPassword)
|
|
295
|
+
: `${p.locked ? t.warn(i.locked) : t.ok(i.unlocked)} ${t.muted('· ' + doorsSummary(i, p))}`
|
|
294
296
|
const name = p.current ? t.bold(p.name || i.noName) : (p.name || i.noName)
|
|
295
297
|
return { text: ` ${mark} ${name} ${t.muted(p.id)} ${t.muted(p.fingerprint || '—')} ${lk}`, sel: true, meta: p }
|
|
296
298
|
})
|
|
297
299
|
}
|
|
298
300
|
|
|
301
|
+
/** Una puerta dicha corta: «llave (toque) + contraseña». */
|
|
302
|
+
function doorLabel (i, d) {
|
|
303
|
+
const base = d.kind === 'password' ? i.doorPassword : d.kind === 'fido2' ? i.doorTouch : d.kind === 'chalresp' ? i.doorNoTouch : d.kind
|
|
304
|
+
return d.withPassword ? base + i.doorPlusPassword : base
|
|
305
|
+
}
|
|
306
|
+
/** Con qué se abre un perfil: «contraseña o llave (toque)». */
|
|
307
|
+
const doorsSummary = (i, p) => (p.doors || []).map((d) => doorLabel(i, d)).join(i.doorsOr) || i.doorPassword
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* CON QUÉ SE ABRE LA BÓVEDA (`docs/llaves-de-hardware.md` §2bis). Las puertas que tiene y las
|
|
311
|
+
* que se le pueden añadir. La contraseña se sigue poniendo con `c` en Perfiles; aquí se ve y
|
|
312
|
+
* se puede quitar como cualquier otra puerta.
|
|
313
|
+
*/
|
|
314
|
+
function doorRows (st, t) {
|
|
315
|
+
const i = L(st)
|
|
316
|
+
const p = (st.profiles?.profiles || []).find((x) => x.id === st.doorsFor)
|
|
317
|
+
if (!p) return [{ text: t.muted(' —'), sel: false }]
|
|
318
|
+
const rows = [{ text: t.muted(' ' + i.doorsIntro(p.name || p.id)), sel: false }, { text: '', sel: false }]
|
|
319
|
+
if (!p.protected) rows.push({ text: ' ' + t.muted(i.doorsNone), sel: false })
|
|
320
|
+
for (const d of p.doors || []) {
|
|
321
|
+
const when = d.createdAt ? new Date(d.createdAt).toISOString().slice(0, 10) : ''
|
|
322
|
+
rows.push({ text: ` ${t.bold(doorLabel(i, d))} ${t.muted(d.label || '')} ${t.muted(when)}`, sel: true, meta: { door: d } })
|
|
323
|
+
}
|
|
324
|
+
rows.push({ text: '', sel: false })
|
|
325
|
+
for (const [add, label, hint] of [
|
|
326
|
+
['touch', i.addTouchKey, i.addTouchKeyHint],
|
|
327
|
+
['notouch', i.addNoTouchKey, i.addNoTouchKeyHint],
|
|
328
|
+
['touchpwd', i.addKeyWithPassword, i.addKeyWithPasswordHint]
|
|
329
|
+
]) {
|
|
330
|
+
rows.push({ text: ' ' + t.accent(label), sel: true, meta: { add } })
|
|
331
|
+
rows.push({ text: t.muted(' ' + hint), sel: false })
|
|
332
|
+
}
|
|
333
|
+
return rows
|
|
334
|
+
}
|
|
335
|
+
|
|
299
336
|
function deviceRows (st, t) {
|
|
300
337
|
const i = L(st)
|
|
301
338
|
const rows = []
|
|
@@ -1137,7 +1174,7 @@ async function onKeyProfiles (term, st, key) {
|
|
|
1137
1174
|
mask: true,
|
|
1138
1175
|
onSubmit: async (pwd) => {
|
|
1139
1176
|
st.input = null
|
|
1140
|
-
if (pwd.length <
|
|
1177
|
+
if (pwd.length < 12) { flash(st, i.passwordTooShort, 'danger'); return }
|
|
1141
1178
|
setInput(st, {
|
|
1142
1179
|
label: i.repeatPassword,
|
|
1143
1180
|
mask: true,
|
|
@@ -1154,8 +1191,15 @@ async function onKeyProfiles (term, st, key) {
|
|
|
1154
1191
|
},
|
|
1155
1192
|
onCancel: () => { st.input = null }
|
|
1156
1193
|
}))
|
|
1194
|
+
} else if (ch === 'y' && cur) { // las llaves: con qué se abre la bóveda
|
|
1195
|
+
await ensureUnlocked(term, st, cur, async (p = cur) => {
|
|
1196
|
+
st.doorsFor = p.id
|
|
1197
|
+
st.sel.doors = 0
|
|
1198
|
+
st.scroll.doors = { value: 0 }
|
|
1199
|
+
st.screen = 'doors'
|
|
1200
|
+
})
|
|
1157
1201
|
} else if (ch === 'x' && cur) { // quitar contraseña
|
|
1158
|
-
if (!cur.
|
|
1202
|
+
if (!(cur.doors || []).some((d) => d.kind === 'password')) { flash(st, i.noPasswordSet, 'warn'); return true }
|
|
1159
1203
|
await ensureUnlocked(term, st, cur, async (p = cur) => {
|
|
1160
1204
|
const r = await guard(term, st, i.removingPassword, () => vc.removeProfilePassword(p.id, st.sessionPwd?.get(p.id)))
|
|
1161
1205
|
if (r.ok) { st.sessionPwd?.delete(p.id); flash(st, i.passwordRemoved); await refreshProfiles(term, st) }
|
|
@@ -1500,6 +1544,113 @@ function choosePairCaps (st, { profile, service = null, label = null, after = nu
|
|
|
1500
1544
|
return true
|
|
1501
1545
|
}
|
|
1502
1546
|
|
|
1547
|
+
async function onKeyDoors (term, st, key) {
|
|
1548
|
+
const i = L(st)
|
|
1549
|
+
const rows = doorRows(st, term.t)
|
|
1550
|
+
const sels = rows.filter((r) => r.sel).map((r) => r.meta)
|
|
1551
|
+
moveSel(st, key, 'doors', sels.length)
|
|
1552
|
+
const cur = sels[Math.min(st.sel.doors, sels.length - 1)]
|
|
1553
|
+
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
1554
|
+
const p = (st.profiles?.profiles || []).find((x) => x.id === st.doorsFor)
|
|
1555
|
+
if (key.name === 'escape' || ch === 'b' || !p) { st.screen = 'profiles'; st.doorsFor = null; return true }
|
|
1556
|
+
|
|
1557
|
+
if ((key.name === 'delete' || ch === 'd') && cur?.door) {
|
|
1558
|
+
const d = cur.door
|
|
1559
|
+
const ultima = (p.doors || []).length <= 1
|
|
1560
|
+
const what = d.label ? `${doorLabel(i, d)} · ${d.label}` : doorLabel(i, d)
|
|
1561
|
+
setConfirm(st, {
|
|
1562
|
+
text: ultima ? i.removeLastDoorConfirm(what) : i.removeDoorConfirm(what),
|
|
1563
|
+
onYes: () => ensureUnlocked(term, st, p, async (q = p) => {
|
|
1564
|
+
const r = await guard(term, st, i.removingDoor, () => vc.removeProfileDoor(q.id, d.kind === 'password' ? 'password' : d.id))
|
|
1565
|
+
if (r.ok) {
|
|
1566
|
+
if (d.kind === 'password') st.sessionPwd?.delete(q.id)
|
|
1567
|
+
flash(st, i.doorRemoved)
|
|
1568
|
+
st.sel.doors = 0
|
|
1569
|
+
await refreshProfiles(term, st)
|
|
1570
|
+
}
|
|
1571
|
+
}),
|
|
1572
|
+
onNo: () => { st.confirm = null }
|
|
1573
|
+
})
|
|
1574
|
+
return true
|
|
1575
|
+
}
|
|
1576
|
+
if (key.name === 'enter' && cur?.add) await ensureUnlocked(term, st, p, (q = p) => addKeyDoor(term, st, q, cur.add))
|
|
1577
|
+
return true
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* Da de alta la llave como puerta, avisando en la barra de estado de cada toque ANTES de que
|
|
1582
|
+
* la llave parpadee: si no se ve a tiempo, la llave rechaza la operación al medio minuto.
|
|
1583
|
+
*/
|
|
1584
|
+
async function addKeyDoor (term, st, p, mode) {
|
|
1585
|
+
const i = L(st)
|
|
1586
|
+
const busy = (msg) => { st.busy = msg; render(term, st) }
|
|
1587
|
+
const fail = (e) => { st.busy = null; flash(st, i.keyFailed(humanErr(e, st)), 'danger') }
|
|
1588
|
+
const send = async (spec) => {
|
|
1589
|
+
const r = await guard(term, st, i.savingDoor, () => vc.addProfileDoor(p.id, { ...spec, secret: Buffer.from(spec.secret).toString('base64') }))
|
|
1590
|
+
spec.secret.fill(0)
|
|
1591
|
+
if (r.ok) { flash(st, spec.kind === 'chalresp' ? i.doorAddedNoTouch : i.doorAdded); await refreshProfiles(term, st) }
|
|
1592
|
+
}
|
|
1593
|
+
// Llave + contraseña: se pide dos veces, y con el mínimo dicho antes de mandar nada.
|
|
1594
|
+
const withPassword = (spec) => setInput(st, {
|
|
1595
|
+
label: i.keyPasswordLabel,
|
|
1596
|
+
hint: i.keyPasswordHint,
|
|
1597
|
+
mask: true,
|
|
1598
|
+
onSubmit: (pwd) => {
|
|
1599
|
+
st.input = null
|
|
1600
|
+
if (pwd.length < 12) { spec.secret.fill(0); flash(st, i.passwordTooShort, 'danger'); return }
|
|
1601
|
+
setInput(st, {
|
|
1602
|
+
label: i.repeatPassword,
|
|
1603
|
+
mask: true,
|
|
1604
|
+
onSubmit: async (again) => {
|
|
1605
|
+
st.input = null
|
|
1606
|
+
if (again !== pwd) { spec.secret.fill(0); flash(st, i.passwordMismatch, 'danger'); return }
|
|
1607
|
+
await send({ ...spec, password: pwd })
|
|
1608
|
+
},
|
|
1609
|
+
onCancel: () => { st.input = null; spec.secret.fill(0) }
|
|
1610
|
+
})
|
|
1611
|
+
},
|
|
1612
|
+
onCancel: () => { st.input = null; spec.secret.fill(0) }
|
|
1613
|
+
})
|
|
1614
|
+
|
|
1615
|
+
if (mode === 'notouch') {
|
|
1616
|
+
let serial
|
|
1617
|
+
try {
|
|
1618
|
+
busy(i.lookingForKey)
|
|
1619
|
+
serial = await hw.ykSerial()
|
|
1620
|
+
if (!serial) throw Object.assign(new Error('no key'), { code: 'HWKEY_NO_DEVICE' })
|
|
1621
|
+
const ocupada = await hw.ykSlot2Busy()
|
|
1622
|
+
st.busy = null
|
|
1623
|
+
const programar = async (overwrite) => {
|
|
1624
|
+
try {
|
|
1625
|
+
busy(i.programmingSlot)
|
|
1626
|
+
await hw.ykProgramSlot2({ overwrite })
|
|
1627
|
+
const challenge = hw.newChallenge()
|
|
1628
|
+
const secret = await hw.ykChallenge({ challenge })
|
|
1629
|
+
st.busy = null
|
|
1630
|
+
await send({ kind: 'chalresp', challenge, serial, secret, label: `YubiKey ${serial}` })
|
|
1631
|
+
} catch (e) { fail(e) }
|
|
1632
|
+
}
|
|
1633
|
+
// Sobrescribir la ranura borra lo que tenga: eso lo decide la persona, aquí y en voz alta.
|
|
1634
|
+
if (ocupada) { setConfirm(st, { text: i.slotBusyConfirm(serial), onYes: () => programar(true), onNo: () => { st.confirm = null } }); return }
|
|
1635
|
+
await programar(false)
|
|
1636
|
+
} catch (e) { fail(e) }
|
|
1637
|
+
return
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
let spec
|
|
1641
|
+
try {
|
|
1642
|
+
busy(i.touchKeyStep(1))
|
|
1643
|
+
const { credId, device } = await hw.fido2MakeCredential({ label: 'dotrino-vault' })
|
|
1644
|
+
const hsalt = hw.newHsalt()
|
|
1645
|
+
busy(i.touchKeyStep(2))
|
|
1646
|
+
const secret = await hw.fido2Secret({ credId, hsalt })
|
|
1647
|
+
st.busy = null
|
|
1648
|
+
spec = { kind: 'fido2', credId, hsalt, secret, label: device || 'YubiKey' }
|
|
1649
|
+
} catch (e) { return fail(e) }
|
|
1650
|
+
if (mode === 'touchpwd') return withPassword(spec)
|
|
1651
|
+
await send(spec)
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1503
1654
|
async function onKeyPairMode (term, st, key) {
|
|
1504
1655
|
const i = L(st)
|
|
1505
1656
|
const rows = pairModeRows(st, term.t)
|
|
@@ -2129,6 +2280,7 @@ const helpSegs = (i, screen, st = {}) => {
|
|
|
2129
2280
|
logins: i.helpLogins,
|
|
2130
2281
|
pairing: i.helpPairing,
|
|
2131
2282
|
pairmode: i.helpPairMode,
|
|
2283
|
+
doors: i.helpDoors,
|
|
2132
2284
|
paircaps: i.helpPairCaps,
|
|
2133
2285
|
join: i.helpJoin,
|
|
2134
2286
|
me: i.helpMe,
|
|
@@ -2160,6 +2312,7 @@ const title = (i, screen) => ({
|
|
|
2160
2312
|
profiles: i.titleProfiles,
|
|
2161
2313
|
pairing: i.titlePairing,
|
|
2162
2314
|
pairmode: i.titlePairMode,
|
|
2315
|
+
doors: i.titleDoors,
|
|
2163
2316
|
paircaps: i.titlePairCaps,
|
|
2164
2317
|
join: i.titleJoin,
|
|
2165
2318
|
caps: i.titleCaps,
|
|
@@ -2279,6 +2432,7 @@ function render (term, st) {
|
|
|
2279
2432
|
else if (st.screen === 'caps') body = renderList(capsRows(st, t), st.sel.caps || 0, contentH, cols, t, scrollRef)
|
|
2280
2433
|
else if (st.screen === 'devvars') body = renderList(devVarRows(st, t), st.sel.devvars || 0, contentH, cols, t, scrollRef)
|
|
2281
2434
|
else if (st.screen === 'pairmode') body = renderList(pairModeRows(st, t), st.sel.pairmode, contentH, cols, t, scrollRef)
|
|
2435
|
+
else if (st.screen === 'doors') body = renderList(doorRows(st, t), st.sel.doors || 0, contentH, cols, t, scrollRef)
|
|
2282
2436
|
else if (st.screen === 'paircaps') body = renderList(pairCapsRows(st, t), st.sel.paircaps || 0, contentH, cols, t, scrollRef)
|
|
2283
2437
|
else if (st.screen === 'pairing') {
|
|
2284
2438
|
const pb = pairingBody(st, t, cols, contentH)
|
|
@@ -2362,7 +2516,7 @@ export async function runTui () {
|
|
|
2362
2516
|
const st = {
|
|
2363
2517
|
screen: 'profiles', // se arranca en la lista de bóvedas: hay que ENTRAR a una
|
|
2364
2518
|
lang: loadLang(), // es/en — se conmuta con `l` y se recuerda en prefs.json
|
|
2365
|
-
sel: { profiles: 0, devices: 0, secrets: 0, pairmode: 0, paircaps: 0, devvars: 0, logins: 0 },
|
|
2519
|
+
sel: { profiles: 0, devices: 0, secrets: 0, pairmode: 0, paircaps: 0, devvars: 0, logins: 0, doors: 0 },
|
|
2366
2520
|
// Permisos tocados y sin guardar, por llave del aparato (ver `setCapsDraft`).
|
|
2367
2521
|
capsDrafts: {},
|
|
2368
2522
|
// Las bóvedas que ha abierto ESTA sesión, para volver a cerrarlas al salir.
|
|
@@ -2467,6 +2621,7 @@ export async function runTui () {
|
|
|
2467
2621
|
else if (st.screen === 'caps') running = await onKeyCaps(term, st, key)
|
|
2468
2622
|
else if (st.screen === 'devvars') running = await onKeyDevVars(term, st, key)
|
|
2469
2623
|
else if (st.screen === 'pairmode') running = await onKeyPairMode(term, st, key)
|
|
2624
|
+
else if (st.screen === 'doors') running = await onKeyDoors(term, st, key)
|
|
2470
2625
|
else if (st.screen === 'paircaps') running = await onKeyPairCaps(term, st, key)
|
|
2471
2626
|
else if (st.screen === 'pairing') running = await onKeyPairing(term, st, key)
|
|
2472
2627
|
else if (st.screen === 'join') running = await onKeyJoin(term, st, key)
|
|
@@ -2484,4 +2639,4 @@ export async function runTui () {
|
|
|
2484
2639
|
}
|
|
2485
2640
|
|
|
2486
2641
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
2487
|
-
export const __test = { render, pairCapsRows, onKeyPairCaps, onKeyDevices, draftChanges, saveCapsDrafts, onKeySecrets, loginRows, onKeyLogins, refreshLogins, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, onKeyCaps, pairModeRows, pairingBody, scrollBody, fitHelp, wrapHelp, wrapWords, joinBody, onKeyJoin, promptJoin, onKeyProfiles, onInputKey, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
|
|
2642
|
+
export const __test = { render, doorRows, onKeyDoors, pairCapsRows, onKeyPairCaps, onKeyDevices, draftChanges, saveCapsDrafts, onKeySecrets, loginRows, onKeyLogins, refreshLogins, activeLocked, autoLockedIds, autoLockWakeIn, forgetAutoLocked, autoLockMin, refreshAll, ensureUnlocked, profileRows, deviceRows, secretRows, devVarRows, meRows, capsRows, onKeyCaps, pairModeRows, pairingBody, scrollBody, fitHelp, wrapHelp, wrapWords, joinBody, onKeyJoin, promptJoin, onKeyProfiles, onInputKey, toggleLang, mergeMembersAndCerts, seguirAqui, resetToque: () => { ultimoToque = 0 } }
|
package/src/tui/i18n.js
CHANGED
|
@@ -167,10 +167,36 @@ const es = {
|
|
|
167
167
|
titleProfiles: 'Bóvedas',
|
|
168
168
|
titlePairing: 'Emparejar un dispositivo',
|
|
169
169
|
titlePairMode: 'Emparejar: ¿a qué cuenta entra?',
|
|
170
|
+
titleDoors: 'Con qué se abre la bóveda',
|
|
171
|
+
doorsIntro: (name) => `Con qué se abre «${name}». Abre cualquiera de estas.`,
|
|
172
|
+
doorsNone: 'Sin candado: se abre sola en esta máquina.',
|
|
173
|
+
doorPassword: 'contraseña',
|
|
174
|
+
doorTouch: 'llave (toque)',
|
|
175
|
+
doorNoTouch: 'llave (sin toque)',
|
|
176
|
+
doorPlusPassword: ' + contraseña',
|
|
177
|
+
doorsOr: ' o ',
|
|
178
|
+
addTouchKey: '+ Llave de seguridad con toque',
|
|
179
|
+
addTouchKeyHint: 'Para abrir hay que tocarla. Es la recomendada.',
|
|
180
|
+
addNoTouchKey: '+ Llave de seguridad sin toque',
|
|
181
|
+
addNoTouchKeyHint: 'Basta con que esté enchufada: cualquier programa de esta máquina puede usarla. Usa la ranura 2 de la YubiKey.',
|
|
182
|
+
addKeyWithPassword: '+ Llave con toque y contraseña',
|
|
183
|
+
addKeyWithPasswordHint: 'Hacen falta las dos para abrir.',
|
|
184
|
+
touchKeyStep: (n) => `Toca la llave de seguridad (${n} de 2)…`,
|
|
185
|
+
programmingSlot: 'Programando la ranura 2 de la llave…',
|
|
186
|
+
slotBusyConfirm: (serial) => `La ranura 2 de la YubiKey ${serial} ya está programada. ¿Sobrescribirla? Se BORRA lo que tenga.`,
|
|
187
|
+
keyPasswordLabel: 'Contraseña para esta llave',
|
|
188
|
+
keyPasswordHint: 'esta llave abrirá solo junto con esta contraseña (mínimo 12 caracteres)',
|
|
189
|
+
savingDoor: 'Guardando la llave…',
|
|
190
|
+
doorAdded: 'Llave añadida. Deja otra forma de abrir de repuesto: perder la única es perder la bóveda.',
|
|
191
|
+
doorAddedNoTouch: 'Llave sin toque añadida. Desenchúfala cuando no la uses. Deja otra forma de abrir de repuesto.',
|
|
192
|
+
removeDoorConfirm: (what) => `¿Quitar «${what}» como forma de abrir?`,
|
|
193
|
+
removeLastDoorConfirm: (what) => `¿Quitar «${what}»? Es la última: la bóveda quedará sin candado.`,
|
|
194
|
+
removingDoor: 'Quitando…',
|
|
195
|
+
doorRemoved: 'Quitada',
|
|
170
196
|
titleJoin: 'Entrar en la cuenta de otra bóveda',
|
|
171
197
|
|
|
172
198
|
// bóvedas (perfiles)
|
|
173
|
-
noPassword: 'sin
|
|
199
|
+
noPassword: 'sin candado',
|
|
174
200
|
noPasswordWarn: 'Este perfil no tiene contraseña: una copia de este disco abre las variables privadas.',
|
|
175
201
|
pendingSeal: (owner, kind, who) => kind === 'rotate' ? `${owner}: sin rotar (salió un aparato). Guarda una variable con la contraseña.` : `${owner}: sin llave todavía en ${who}. Se reparte sola con otro aparato del grupo encendido, o al abrir la bóveda.`,
|
|
176
202
|
locked: '🔒 bloqueada',
|
|
@@ -379,7 +405,7 @@ const es = {
|
|
|
379
405
|
// INGLÉS: new/rename/delete/password/unlock/locK/pair/approve/revoke/refresh/
|
|
380
406
|
// language/quit); lo único que se traduce es la palabra que las explica.
|
|
381
407
|
// Segmentos, no una línea: si no caben en una, el render los reparte en varias.
|
|
382
|
-
helpProfiles: ['↑↓', 'Enter entrar', 'p emparejar', 'j unirse', 'n nueva', 'r renombrar', 'd borrar', 'c clave', 'x quitar-clave', 'u desbloq', 'k bloquear', 'l English', 'q salir'],
|
|
408
|
+
helpProfiles: ['↑↓', 'Enter entrar', 'p emparejar', 'j unirse', 'n nueva', 'r renombrar', 'd borrar', 'c clave', 'x quitar-clave', 'y llaves', 'u desbloq', 'k bloquear', 'l English', 'q salir'],
|
|
383
409
|
// La barra dice lo que se PUEDE hacer AHORA, no todo lo que existe: aprobar/rechazar solo
|
|
384
410
|
// valen si hay alguien esperando, y renombrar/revocar solo si hay un aparato seleccionado.
|
|
385
411
|
// Anunciar teclas muertas confunde y además las quema para otros usos.
|
|
@@ -416,6 +442,7 @@ const es = {
|
|
|
416
442
|
revealNoPwd: 'Este perfil no tiene contraseña: se abre con la llave de esta máquina.',
|
|
417
443
|
helpPairing: ['a aprobar', 'x rechazar', 'r reiniciar', '↑↓ scroll', 'Esc atrás', 'l English'],
|
|
418
444
|
helpPairMode: ['↑↓', 'Enter elegir', 'Esc atrás', 'l English', 'q salir'],
|
|
445
|
+
helpDoors: ['↑↓', 'Enter añadir', 'd quitar', 'Esc atrás', 'l English', 'q salir'],
|
|
419
446
|
helpJoin: ['Esc bóvedas', 'l English', 'q salir'],
|
|
420
447
|
|
|
421
448
|
// pantalla "daemon caído"
|
|
@@ -584,9 +611,35 @@ const en = {
|
|
|
584
611
|
titleProfiles: 'Vaults',
|
|
585
612
|
titlePairing: 'Pair a device',
|
|
586
613
|
titlePairMode: 'Pairing: which account does it join?',
|
|
614
|
+
titleDoors: 'What opens this vault',
|
|
615
|
+
doorsIntro: (name) => `What opens “${name}”. Any of these opens it.`,
|
|
616
|
+
doorsNone: 'No lock: it opens by itself on this machine.',
|
|
617
|
+
doorPassword: 'password',
|
|
618
|
+
doorTouch: 'key (touch)',
|
|
619
|
+
doorNoTouch: 'key (no touch)',
|
|
620
|
+
doorPlusPassword: ' + password',
|
|
621
|
+
doorsOr: ' or ',
|
|
622
|
+
addTouchKey: '+ Security key with touch',
|
|
623
|
+
addTouchKeyHint: 'You have to touch it to open. The recommended one.',
|
|
624
|
+
addNoTouchKey: '+ Security key without touch',
|
|
625
|
+
addNoTouchKeyHint: 'Being plugged in is enough: any program on this machine can use it. Uses slot 2 of the YubiKey.',
|
|
626
|
+
addKeyWithPassword: '+ Key with touch and password',
|
|
627
|
+
addKeyWithPasswordHint: 'Both are needed to open.',
|
|
628
|
+
touchKeyStep: (n) => `Touch the security key (${n} of 2)…`,
|
|
629
|
+
programmingSlot: 'Programming slot 2 of the key…',
|
|
630
|
+
slotBusyConfirm: (serial) => `Slot 2 of YubiKey ${serial} is already programmed. Overwrite it? What it holds is ERASED.`,
|
|
631
|
+
keyPasswordLabel: 'Password for this key',
|
|
632
|
+
keyPasswordHint: 'this key will open only together with this password (at least 12 characters)',
|
|
633
|
+
savingDoor: 'Saving the key…',
|
|
634
|
+
doorAdded: 'Key added. Keep a spare way in: losing the only one means losing the vault.',
|
|
635
|
+
doorAddedNoTouch: 'No-touch key added. Unplug it when not in use. Keep a spare way in.',
|
|
636
|
+
removeDoorConfirm: (what) => `Remove “${what}” as a way in?`,
|
|
637
|
+
removeLastDoorConfirm: (what) => `Remove “${what}”? It is the last one: the vault will be left without a lock.`,
|
|
638
|
+
removingDoor: 'Removing…',
|
|
639
|
+
doorRemoved: 'Removed',
|
|
587
640
|
titleJoin: 'Join the account of another vault',
|
|
588
641
|
|
|
589
|
-
noPassword: 'no
|
|
642
|
+
noPassword: 'no lock',
|
|
590
643
|
noPasswordWarn: 'This profile has no password: a copy of this disk opens the private variables.',
|
|
591
644
|
pendingSeal: (owner, kind, who) => kind === 'rotate' ? `${owner}: not rotated (a device left). Save a variable with the password.` : `${owner}: no key yet on ${who}. It is handed out by another device of the group that is on, or when the vault is opened.`,
|
|
592
645
|
locked: '🔒 locked',
|
|
@@ -773,7 +826,7 @@ const en = {
|
|
|
773
826
|
helpInput: 'Enter confirm · Esc cancel · Ctrl-U clear',
|
|
774
827
|
helpConfirm: 'y confirm · n/Esc cancel',
|
|
775
828
|
|
|
776
|
-
helpProfiles: ['↑↓', 'Enter open', 'p pair', 'j join', 'n new', 'r rename', 'd delete', 'c password', 'x drop-password', 'u unlock', 'k lock', 'l Español', 'q quit'],
|
|
829
|
+
helpProfiles: ['↑↓', 'Enter open', 'p pair', 'j join', 'n new', 'r rename', 'd delete', 'c password', 'x drop-password', 'y keys', 'u unlock', 'k lock', 'l Español', 'q quit'],
|
|
777
830
|
helpDevices: ({ pending, hasDevices, isService, drafts } = {}) => [
|
|
778
831
|
'←→ tab', '↑↓', 'p pair',
|
|
779
832
|
...(drafts ? ['g save permissions'] : []),
|
|
@@ -804,6 +857,7 @@ const en = {
|
|
|
804
857
|
revealNoPwd: 'This profile has no password: it opens with this machine key.',
|
|
805
858
|
helpPairing: ['a approve', 'x reject', 'r restart', '↑↓ scroll', 'Esc back', 'l Español'],
|
|
806
859
|
helpPairMode: ['↑↓', 'Enter choose', 'Esc back', 'l Español', 'q quit'],
|
|
860
|
+
helpDoors: ['↑↓', 'Enter add', 'd remove', 'Esc back', 'l Español', 'q quit'],
|
|
807
861
|
helpJoin: ['Esc vaults', 'l Español', 'q quit'],
|
|
808
862
|
|
|
809
863
|
downTitle: 'The vault daemon is not running.',
|
package/src/vaultControl.js
CHANGED
|
@@ -207,12 +207,12 @@ async function waitFor (name, { tries = 60, interval = 100, req = null, since =
|
|
|
207
207
|
* OPERANDO (para use/rm/rename) como el DESTINO (para unlock/lock/password). La
|
|
208
208
|
* contraseña viaja en un archivo 0600 que el daemon borra al leerlo.
|
|
209
209
|
*/
|
|
210
|
-
async function profileOp (op, { profile,
|
|
210
|
+
async function profileOp (op, { profile, ...fields } = {}) {
|
|
211
211
|
requireAlive() // nunca escribir la contraseña si no hay quien la consuma
|
|
212
212
|
rm(F.profilesList)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
// TODO lo que se pase viaja, no una lista fija: con la lista fija se perdían en silencio
|
|
214
|
+
// `current` (cambiar la contraseña) y `door`/`secret` (abrir con la llave de seguridad).
|
|
215
|
+
const extra = Object.fromEntries(Object.entries(fields).filter(([, v]) => v != null))
|
|
216
216
|
const since = Date.now()
|
|
217
217
|
const id = await writeReq(F.profileReq, { op, ...extra }, profile)
|
|
218
218
|
signalOrCleanup('SIGUSR2', [F.profileReq])
|
|
@@ -247,6 +247,10 @@ export const touchProfile = (profile) => profileOp('touch', { profile })
|
|
|
247
247
|
export const setProfilePassword = (profile, password, current) => profileOp('password-set', { profile, password, ...(current ? { current } : {}) })
|
|
248
248
|
// Quitarla también la pide: se re-sella a la llave de la máquina antes de que se vaya.
|
|
249
249
|
export const removeProfilePassword = (profile, password) => profileOp('password-rm', { profile, password })
|
|
250
|
+
// LLAVES DE SEGURIDAD: `spec` lleva lo que devolvió la llave (`secret` en base64), nunca nada
|
|
251
|
+
// que la abra sin ella. `door` es el id de la puerta, o 'password' para la contraseña.
|
|
252
|
+
export const addProfileDoor = (profile, spec) => profileOp('door-add', { profile, spec })
|
|
253
|
+
export const removeProfileDoor = (profile, door) => profileOp('door-rm', { profile, door })
|
|
250
254
|
|
|
251
255
|
// ---------------------------------------------------------------------------
|
|
252
256
|
// Inicios de sesión con contraseña (`docs/temporary-access.md`)
|