@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/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 r = await guard(term, st, i.unlocking, () => api.unlockProfile(p.id, pwd))
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
- st.sessionPwd?.set(p.id, pwd) // vale para toda la sesión (ver reunlockSilently)
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
- const fresh = (st.profiles.profiles || []).find((x) => x.id === p.id) || p
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()
@@ -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
- export const unlockProfile = (profile, password) => profileOp('unlock', { profile, password })
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.