@dotrino/vaultd 0.139.0 → 0.140.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/package.json +2 -2
- package/src/ctl.js +157 -10
- package/src/daemon.js +40 -29
- package/src/hwkeys.js +247 -0
- package/src/manager.js +51 -3
- package/src/profiles.js +324 -64
- package/src/threadStore.js +8 -3
- package/src/tui/app.js +47 -8
- package/src/tui/i18n.js +28 -0
- package/src/vault.js +1 -0
- package/src/vaultControl.js +2 -1
package/src/tui/app.js
CHANGED
|
@@ -36,6 +36,7 @@ import { createTerm, widthOf } from './term.js'
|
|
|
36
36
|
import { qrToString } from '../qr.js'
|
|
37
37
|
import { dict, otherLang, loadLang, saveLang } from './i18n.js'
|
|
38
38
|
import * as vc from '../vaultControl.js'
|
|
39
|
+
import * as hw from '../hwkeys.js'
|
|
39
40
|
import { VERSION } from '../version.js'
|
|
40
41
|
import { DEVICE_CAPS } from '@dotrino/identity/acta'
|
|
41
42
|
|
|
@@ -77,8 +78,17 @@ function humanErr (e, st) {
|
|
|
77
78
|
JOIN_BUSY: t.errJoinBusy,
|
|
78
79
|
APPROVE_FAILED: t.errWrongCode,
|
|
79
80
|
MASTER_WITH_MEMBERS: t.errMasterWithMembers,
|
|
80
|
-
PROFILE_LOCKED: t.errProfileLocked
|
|
81
|
+
PROFILE_LOCKED: t.errProfileLocked,
|
|
82
|
+
NEEDS_SECURITY_KEY: t.errNeedsKey,
|
|
83
|
+
NEEDS_PASSWORD: t.errNeedsPassword,
|
|
84
|
+
MASTER_SEALED_ELSEWHERE: t.errMasterSealedElsewhere,
|
|
85
|
+
HWKEY_NO_DEVICE: t.hwNoDevice,
|
|
86
|
+
HWKEY_NOT_TOUCHED: t.hwNotTouched,
|
|
87
|
+
HWKEY_WRONG_KEY: t.hwWrongKey,
|
|
88
|
+
HWKEY_WRONG_PIN: t.hwWrongPin,
|
|
89
|
+
HWKEY_PIN_BLOCKED: t.hwPinBlocked
|
|
81
90
|
}
|
|
91
|
+
if (e?.code === 'HWKEY_TOOLS_MISSING') return t.hwToolsMissing(e.tool, e.pkg)
|
|
82
92
|
return byCode[e?.code] || e?.message || String(e)
|
|
83
93
|
}
|
|
84
94
|
|
|
@@ -957,7 +967,7 @@ async function reunlockSilently (term, st, p, api = vc) {
|
|
|
957
967
|
* que dura la sesión, no hasta que alguien reinicie el servicio. Lo que ya estaba abierto
|
|
958
968
|
* antes de entrar no se toca — no lo abrió esta pantalla, no le toca cerrarlo.
|
|
959
969
|
*/
|
|
960
|
-
async function ensureUnlocked (term, st, p, thenFn, reason = null, api = vc) {
|
|
970
|
+
async function ensureUnlocked (term, st, p, thenFn, reason = null, api = vc, key = undefined) {
|
|
961
971
|
if (!p.protected || !p.locked) return thenFn()
|
|
962
972
|
// Cerrada, pero la contraseña ya se tecleó en esta sesión: se reabre sin molestar.
|
|
963
973
|
if (await reunlockSilently(term, st, p, api)) {
|
|
@@ -965,8 +975,34 @@ async function ensureUnlocked (term, st, p, thenFn, reason = null, api = vc) {
|
|
|
965
975
|
return thenFn(fresh)
|
|
966
976
|
}
|
|
967
977
|
const i = L(st)
|
|
978
|
+
const fresh = () => (st.profiles?.profiles || []).find((x) => x.id === p.id) || p
|
|
979
|
+
const hasPassword = (p.doors || []).some((d) => d.kind === 'password')
|
|
980
|
+
// CON LA LLAVE DE SEGURIDAD, si la bóveda tiene una: se le pide antes que la contraseña.
|
|
981
|
+
// Una vez por intento (`key === undefined`); si no abre, se cae a la contraseña diciendo
|
|
982
|
+
// por qué, o se para si la bóveda no tiene contraseña.
|
|
983
|
+
const doors = hw.hardwareDoors(p)
|
|
984
|
+
if (key === undefined && doors.length) {
|
|
985
|
+
key = null
|
|
986
|
+
st.busy = i.lookingForKey
|
|
987
|
+
render(term, st)
|
|
988
|
+
try {
|
|
989
|
+
key = await hw.secretForDoors(doors, { onTouch: () => { st.busy = i.touchKey; render(term, st) } })
|
|
990
|
+
} catch (e) { reason = i.keyFailed(humanErr(e, st)) } finally { st.busy = null }
|
|
991
|
+
if (key && !key.withPassword) {
|
|
992
|
+
const r = await guard(term, st, i.unlocking, () => api.unlockProfile(p.id, null, { door: key.door, secret: Buffer.from(key.secret).toString('base64') }))
|
|
993
|
+
key.secret.fill(0)
|
|
994
|
+
if (r.ok) {
|
|
995
|
+
st.unlockedHere?.add(p.id)
|
|
996
|
+
await refreshProfiles(term, st, api)
|
|
997
|
+
return thenFn(fresh())
|
|
998
|
+
}
|
|
999
|
+
reason = humanErr(r.e, st)
|
|
1000
|
+
key = null
|
|
1001
|
+
}
|
|
1002
|
+
if (!key && !hasPassword) { flash(st, reason || i.plugKey, 'danger'); render(term, st); return }
|
|
1003
|
+
}
|
|
968
1004
|
setInput(st, {
|
|
969
|
-
label: i.passwordOf(p.name || p.id),
|
|
1005
|
+
label: key ? i.passwordWithKey(p.name || p.id) : i.passwordOf(p.name || p.id),
|
|
970
1006
|
mask: true,
|
|
971
1007
|
// Si la anterior fue rechazada, el motivo se queda AQUÍ, pegado al campo, en vez de
|
|
972
1008
|
// irse en un aviso de cuatro segundos que se lleva el siguiente redibujado. Eso era lo
|
|
@@ -974,15 +1010,18 @@ async function ensureUnlocked (term, st, p, thenFn, reason = null, api = vc) {
|
|
|
974
1010
|
hint: reason || i.passwordToEdit,
|
|
975
1011
|
onSubmit: async (pwd) => {
|
|
976
1012
|
st.input = null
|
|
977
|
-
const
|
|
1013
|
+
const extra = key ? { door: key.door, secret: Buffer.from(key.secret).toString('base64') } : null
|
|
1014
|
+
const r = await guard(term, st, i.unlocking, () => api.unlockProfile(p.id, pwd, extra))
|
|
978
1015
|
// Rechazada: se vuelve a pedir en el acto, diciendo por qué. Cerrar el campo obligaba
|
|
979
1016
|
// a adivinar qué había pasado y a empezar de nuevo.
|
|
980
|
-
if (!r.ok) return ensureUnlocked(term, st, p, thenFn, humanErr(r.e, st), api)
|
|
1017
|
+
if (!r.ok) return ensureUnlocked(term, st, p, thenFn, humanErr(r.e, st), api, key)
|
|
1018
|
+
if (key) key.secret.fill(0)
|
|
981
1019
|
st.unlockedHere?.add(p.id)
|
|
982
|
-
|
|
1020
|
+
// Solo la contraseña sola se recuerda para reabrir en silencio: la que va con llave
|
|
1021
|
+
// necesita la llave otra vez, que es justo su gracia.
|
|
1022
|
+
if (!key) st.sessionPwd?.set(p.id, pwd) // vale para toda la sesión (ver reunlockSilently)
|
|
983
1023
|
await refreshProfiles(term, st, api)
|
|
984
|
-
|
|
985
|
-
await thenFn(fresh)
|
|
1024
|
+
await thenFn(fresh())
|
|
986
1025
|
},
|
|
987
1026
|
onCancel: () => { st.input = null }
|
|
988
1027
|
})
|
package/src/tui/i18n.js
CHANGED
|
@@ -176,6 +176,20 @@ const es = {
|
|
|
176
176
|
locked: '🔒 bloqueada',
|
|
177
177
|
unlocked: '🔓 abierta',
|
|
178
178
|
passwordOf: (name) => `Contraseña de "${name}"`,
|
|
179
|
+
passwordWithKey: (name) => `Contraseña de "${name}" (su llave la pide también)`,
|
|
180
|
+
lookingForKey: 'Buscando la llave de seguridad…',
|
|
181
|
+
touchKey: 'Toca la llave de seguridad…',
|
|
182
|
+
keyFailed: (why) => `La llave no abrió: ${why}`,
|
|
183
|
+
plugKey: 'Esta bóveda se abre con su llave de seguridad: enchúfala y vuelve a intentarlo',
|
|
184
|
+
errNeedsKey: 'Esta bóveda no se abre solo con contraseña: usa su llave de seguridad',
|
|
185
|
+
errNeedsPassword: 'Esta llave pide también la contraseña',
|
|
186
|
+
errMasterSealedElsewhere: 'La llave maestra quedó cerrada con una contraseña anterior: pon esa misma para recuperarla',
|
|
187
|
+
hwToolsMissing: (tool, pkg) => `falta ${tool} (instala ${pkg})`,
|
|
188
|
+
hwNoDevice: 'no hay ninguna llave enchufada',
|
|
189
|
+
hwNotTouched: 'no se tocó la llave a tiempo',
|
|
190
|
+
hwWrongKey: 'esta llave no es la de esta bóveda',
|
|
191
|
+
hwWrongPin: 'el PIN de la llave no es ese',
|
|
192
|
+
hwPinBlocked: 'el PIN de la llave está bloqueado',
|
|
179
193
|
passwordToEdit: 'necesaria para abrir la bóveda: sin ella no se ve ni se toca',
|
|
180
194
|
unlocking: 'Desbloqueando…',
|
|
181
195
|
loading: 'Cargando…',
|
|
@@ -578,6 +592,20 @@ const en = {
|
|
|
578
592
|
locked: '🔒 locked',
|
|
579
593
|
unlocked: '🔓 unlocked',
|
|
580
594
|
passwordOf: (name) => `Password for "${name}"`,
|
|
595
|
+
passwordWithKey: (name) => `Password for "${name}" (its key asks for it too)`,
|
|
596
|
+
lookingForKey: 'Looking for the security key…',
|
|
597
|
+
touchKey: 'Touch the security key…',
|
|
598
|
+
keyFailed: (why) => `The key did not open it: ${why}`,
|
|
599
|
+
plugKey: 'This vault opens with its security key: plug it in and try again',
|
|
600
|
+
errNeedsKey: 'This vault does not open with a password alone: use its security key',
|
|
601
|
+
errNeedsPassword: 'This key also needs the password',
|
|
602
|
+
errMasterSealedElsewhere: 'The master key was left sealed with an earlier password: set that same one to recover it',
|
|
603
|
+
hwToolsMissing: (tool, pkg) => `${tool} is missing (install ${pkg})`,
|
|
604
|
+
hwNoDevice: 'no key is plugged in',
|
|
605
|
+
hwNotTouched: 'the key was not touched in time',
|
|
606
|
+
hwWrongKey: 'this key is not the one for this vault',
|
|
607
|
+
hwWrongPin: 'wrong key PIN',
|
|
608
|
+
hwPinBlocked: 'the key PIN is blocked',
|
|
581
609
|
passwordToEdit: 'needed to open this vault: without it, nothing is shown or touched',
|
|
582
610
|
unlocking: 'Unlocking…',
|
|
583
611
|
loading: 'Loading…',
|
package/src/vault.js
CHANGED
|
@@ -134,6 +134,7 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
|
|
|
134
134
|
audit('resealed', { envelopes: r.changed, entries: r.entries, gen: mine.gen })
|
|
135
135
|
log(`[vault] ${why}: resealed ${r.changed} envelope(s) in ${r.entries} entr${r.entries === 1 ? 'y' : 'ies'} with content key generation ${mine.gen}`)
|
|
136
136
|
}
|
|
137
|
+
if (r.skipped) log(`[vault] ${why}: ${r.skipped} unmarked envelope(s) that the account key does not open were left as they are`)
|
|
137
138
|
return r
|
|
138
139
|
}
|
|
139
140
|
const approvals = createApprovals()
|
package/src/vaultControl.js
CHANGED
|
@@ -229,7 +229,8 @@ export const addProfile = (name) => profileOp('add', { name })
|
|
|
229
229
|
export const useProfile = (profile) => profileOp('use', { profile })
|
|
230
230
|
export const renameProfile = (profile, name) => profileOp('rename', { profile, name })
|
|
231
231
|
export const removeProfile = (profile) => profileOp('rm', { profile })
|
|
232
|
-
|
|
232
|
+
// `key`: lo que devolvió la llave de seguridad para una puerta, `{ door, secret }` (base64).
|
|
233
|
+
export const unlockProfile = (profile, password, key = null) => profileOp('unlock', { profile, password, ...(key ? { door: key.door, secret: key.secret } : {}) })
|
|
233
234
|
export const lockProfile = (profile) => profileOp('lock', { profile })
|
|
234
235
|
/**
|
|
235
236
|
* «Sigo aquí»: estira el plazo del bloqueo automático sin hacer nada más.
|