@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 +1 -1
- package/src/secretsStore.js +13 -0
- package/src/vault.js +58 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vaultd",
|
|
3
|
-
"version": "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": {
|
package/src/secretsStore.js
CHANGED
|
@@ -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))
|