@dotrino/vaultd 0.81.0 → 0.82.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vaultd",
3
- "version": "0.81.0",
3
+ "version": "0.82.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": {
@@ -796,6 +796,19 @@ export function openSecretsStore (dir, { sealer = null, recipients = null, signe
796
796
  * `null` en cualquiera de las dos significa «la del perfil sin contraseña».
797
797
  */
798
798
  async rekeyRecovery (...a) { return enFila(() => this._rekeyRecovery(...a)) },
799
+ /**
800
+ * ¿ESTA llave abre la copia de recuperación? Lanza si no.
801
+ *
802
+ * Sirve para saber con cuál está cerrada sin tocar nada — que es lo que hace falta para
803
+ * decidir si hay que migrarla. Preguntarlo intentando reenvolver un cajón mezcla dos
804
+ * cosas y deja el fallo diciendo «wrong password» sobre el cajón, que no es donde está.
805
+ */
806
+ async recoveryOpensWith (key) {
807
+ needSealer('check the recovery key')
808
+ if (!data.recovery?.priv) throw new NeedsPassword('this store has no recovery key yet')
809
+ await sealer.openMaster(data.recovery.priv, keyOr(key))
810
+ return true
811
+ },
799
812
  /** @private */
800
813
  async _rekeyRecovery (oldKey, newKey) {
801
814
  if (isLegacy()) return { rekeyed: false, reason: 'v3' }
package/src/vault.js CHANGED
@@ -1826,6 +1826,21 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
1826
1826
  *
1827
1827
  * @returns {Promise<{ done: number, asked: number }>}
1828
1828
  */
1829
+ /**
1830
+ * PEDIRLE A UN HERMANO QUE ENVUELVA — Y QUE SE LO ENTREGUE A LA BÓVEDA (§8.11).
1831
+ *
1832
+ * Regla del dueño (2026-09-01): «un servicio reparte la llave AL VAULT para que este la
1833
+ * reparta después, pero no se la entrega directo; si lo hace sin pasar por el vault está
1834
+ * mal». Es lo que hace este camino, y conviene verlo en el orden de las líneas:
1835
+ *
1836
+ * 1. la BÓVEDA le pide el sobre al hermano (`sendByPubkey` → `REWRAP`);
1837
+ * 2. el hermano le contesta A ELLA, nunca al recién llegado;
1838
+ * 3. la BÓVEDA lo guarda (`putWrap`) y a partir de ahí lo sirve ella.
1839
+ *
1840
+ * El destinatario no habla con nadie, y la bóveda nunca ve la llave en claro: el sobre
1841
+ * llega ya cerrado a la pública del destinatario. Por eso esto funciona con el perfil
1842
+ * cerrado, que es justo cuando hace falta.
1843
+ */
1829
1844
  async function delegateRewrap (owner, targetPub, { timeoutMs = 15000 } = {}) {
1830
1845
  const ns = owner.startsWith('ns:') ? owner.slice(3) : null
1831
1846
  if (!ns) return { done: 0, asked: 0 }
@@ -1935,7 +1950,50 @@ export async function startVault ({ dir = dataDir(), proxyUrl, log = console.log
1935
1950
  return r
1936
1951
  }
1937
1952
 
1953
+ /**
1954
+ * MIGRACIÓN, CON FECHA: la copia de recuperación pudo quedar sellada con la llave de la
1955
+ * MÁQUINA aunque el perfil tenga contraseña.
1956
+ *
1957
+ * Pasa por un camino perfectamente normal: escribir una variable NO pide la frase (§8.1),
1958
+ * así que si el cajón se estrenó con el perfil cerrado, `ensureRecovery` la selló con lo
1959
+ * único que había — la llave de la máquina. A partir de ahí, abrir el perfil y reenvolver
1960
+ * pasa la llave de la CONTRASEÑA, que no abre ese sobre: «wrong password», el llavero no
1961
+ * se rehace, y los servicios se quedan sin poder leer sus cajones para siempre. Es
1962
+ * exactamente lo que le pasó a un perfil real el 2026-09-01.
1963
+ *
1964
+ * Aquí se abre con la de la máquina y se vuelve a cerrar con la de la contraseña, que es
1965
+ * el `rekey` que aquel `password-set` no llegó a hacer. NO es un repliegue: no decide un
1966
+ * permiso, no deja pasar a nadie, y **se acaba** — al primer desbloqueo el sobre queda
1967
+ * bajo la frase y esta rama no vuelve a entrar. Y va en el sentido estricto: el material
1968
+ * de la llave de máquina vive en este mismo disco, así que pasar a la contraseña PROTEGE
1969
+ * más de lo que había, no menos.
1970
+ *
1971
+ * Se puede quitar cuando no queden perfiles de antes de 0.81.0 (2027-03-01).
1972
+ */
1973
+ const MIGRACION_RECUPERACION_HASTA = Date.parse('2027-03-01T00:00:00Z')
1974
+ async function migrarRecuperacionALaFrase (adminKey) {
1975
+ if (!adminKey || Date.now() > MIGRACION_RECUPERACION_HASTA) return false
1976
+ try {
1977
+ // Si la frase ya la abre, no hay nada que migrar.
1978
+ await secrets.recoveryOpensWith(adminKey)
1979
+ return false
1980
+ } catch (_) {}
1981
+ try {
1982
+ const r = await secrets.rekeyRecovery(null, adminKey) // `null` = la llave de la máquina
1983
+ if (!r?.rekeyed) return false
1984
+ log('[vault] the recovery copy was sealed with this machine\'s key: re-sealed under the profile password')
1985
+ audit('secret.recovery-rekeyed', {})
1986
+ return true
1987
+ } catch (e) {
1988
+ log(`[vault] the recovery copy does not open with the password nor with this machine's key (${e.message}): the drawers cannot be re-wrapped`)
1989
+ return false
1990
+ }
1991
+ }
1992
+
1938
1993
  async function resealAll (adminKey = null) {
1994
+ // ANTES de reenvolver nada: si la copia de recuperación se quedó bajo la llave de la
1995
+ // máquina, se pasa a la frase. Sin esto, todo lo de abajo falla con «wrong password».
1996
+ if (adminKey) await migrarRecuperacionALaFrase(adminKey)
1939
1997
  const out = { drawers: 0, wrapped: 0, dropped: 0, sinLlave: [], failed: [] }
1940
1998
  for (const owner of secrets.owners?.() || []) {
1941
1999
  const before = new Set(secrets.recipientsIn(owner))