@dotrino/identity 0.118.0 → 0.120.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/README.md +16 -0
- package/package.json +4 -1
- package/src/index.js +14 -0
- package/vault/acta.js +3 -3
- package/vault/channel.js +77 -0
- package/vault/core.js +2 -2
- package/vault/index.html +1 -0
- package/vault/keyid.js +3 -4
- package/vault/remote.js +2 -2
- package/vault/vault.js +41 -2
- package/vault/vendor/passmanager/VERSION.txt +1 -1
- package/vault/vendor/passmanager/model.js +4 -0
- package/vault/vendor/passmanager/vault/approval.js +36 -7
- package/vault/vendor/passmanager/vault/sealed-responder.js +5 -1
- package/vault/vendor/proxy-client/VERSION.txt +1 -1
- package/vault/vendor/proxy-client/client.js +23 -38
- package/vault/vendor/proxy-client/encpub.js +12 -22
- package/vault/vendor/proxy-client/errors.js +15 -0
- package/vault/vendor/proxy-client/sealing.js +7 -12
- package/vault/vendor/roadmap/VERSION.txt +1 -1
- package/vault/vendor/roadmap/manifests.js +60 -47
- package/vault/vendor/store/VERSION.txt +1 -1
- package/vault/vendor/vault/VERSION.txt +1 -1
- package/vault/vendor/vault/core/backup.js +244 -0
- package/vault/vendor/vault/core/libVersion.js +1 -1
- package/vault/vendor/vault/core/passwords.js +88 -12
- package/vault/vendor/vault/core/secretsStore.js +61 -2
- package/vault/vendor/vault/core/vault.js +169 -1
- package/vault/vendor/vault/sealed.js +7 -36
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { canonicalStringify } from './canonical.js'
|
|
30
30
|
import { verifyData, samePubkey } from './signature.js'
|
|
31
|
+
import { errorWithCode } from './errors.js'
|
|
31
32
|
|
|
32
33
|
export const ENCPUB_V = 1
|
|
33
34
|
|
|
@@ -54,17 +55,6 @@ export function isEncPub (v) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/**
|
|
58
|
-
* @param {string} mensaje
|
|
59
|
-
* @param {string} code
|
|
60
|
-
* @returns {Error & { code: string }}
|
|
61
|
-
*/
|
|
62
|
-
function errorCon (mensaje, code) {
|
|
63
|
-
const e = /** @type {Error & { code: string }} */ (new Error(mensaje))
|
|
64
|
-
e.code = code
|
|
65
|
-
return e
|
|
66
|
-
}
|
|
67
|
-
|
|
68
58
|
/**
|
|
69
59
|
* Firma el anuncio con lo que firma la identidad (`identity.signData`, o la llave local
|
|
70
60
|
* del cliente). No genera llaves ni cifra nada: solo dice de quién es la que ya tienes.
|
|
@@ -73,15 +63,15 @@ function errorCon (mensaje, code) {
|
|
|
73
63
|
* @returns {Promise<{data:any, signature:string}>}
|
|
74
64
|
*/
|
|
75
65
|
export async function buildEncPubStatement ({ publickey, encPub, sign } = /** @type {any} */ ({})) {
|
|
76
|
-
if (typeof publickey !== 'string' || !publickey) throw
|
|
77
|
-
if (!isEncPub(encPub)) throw
|
|
78
|
-
if (typeof sign !== 'function') throw
|
|
66
|
+
if (typeof publickey !== 'string' || !publickey) throw errorWithCode('buildEncPubStatement: missing publickey', 'encpub-shape')
|
|
67
|
+
if (!isEncPub(encPub)) throw errorWithCode('buildEncPubStatement: encPub is not a P-256 public JWK', 'encpub-shape')
|
|
68
|
+
if (typeof sign !== 'function') throw errorWithCode('buildEncPubStatement: missing sign(data)', 'encpub-shape')
|
|
79
69
|
const data = encPubBody({ publickey, encPub })
|
|
80
70
|
const firmado = await sign(data)
|
|
81
71
|
const signature = typeof firmado === 'string' ? firmado : firmado?.signature
|
|
82
72
|
// «No pude firmar» no es «se cayó la red», y aquí es donde se separan: quien llama
|
|
83
73
|
// decide una cosa u otra según el `code`, nunca según la frase.
|
|
84
|
-
if (typeof signature !== 'string') throw
|
|
74
|
+
if (typeof signature !== 'string') throw errorWithCode('buildEncPubStatement: sign() returned no signature', 'no-signature')
|
|
85
75
|
return { data, signature }
|
|
86
76
|
}
|
|
87
77
|
|
|
@@ -98,21 +88,21 @@ export async function buildEncPubStatement ({ publickey, encPub, sign } = /** @t
|
|
|
98
88
|
*/
|
|
99
89
|
export async function readEncPubStatement (statement, { publickey } = /** @type {any} */ ({})) {
|
|
100
90
|
if (typeof publickey !== 'string' || !publickey) {
|
|
101
|
-
throw
|
|
91
|
+
throw errorWithCode('readEncPubStatement: missing the publickey to check against', 'encpub-shape')
|
|
102
92
|
}
|
|
103
93
|
const data = statement?.data
|
|
104
94
|
const signature = statement?.signature
|
|
105
|
-
if (!data || typeof signature !== 'string') throw
|
|
106
|
-
if (data.v !== ENCPUB_V || data.op !== 'encpub') throw
|
|
107
|
-
if (data.aud !== ENCPUB_AUD) throw
|
|
95
|
+
if (!data || typeof signature !== 'string') throw errorWithCode('encpub statement: malformed', 'encpub-unverified')
|
|
96
|
+
if (data.v !== ENCPUB_V || data.op !== 'encpub') throw errorWithCode('encpub statement: not an encpub announcement', 'encpub-unverified')
|
|
97
|
+
if (data.aud !== ENCPUB_AUD) throw errorWithCode('encpub statement: wrong audience', 'encpub-unverified')
|
|
108
98
|
// `samePubkey` y no `===`: un JWK serializado no es canónico y la misma llave escrita
|
|
109
99
|
// por dos piezas distintas da dos strings distintos.
|
|
110
|
-
if (!samePubkey(data.publickey, publickey)) throw
|
|
111
|
-
if (!isEncPub(data.encpub)) throw
|
|
100
|
+
if (!samePubkey(data.publickey, publickey)) throw errorWithCode('encpub statement: announces another identity', 'encpub-unverified')
|
|
101
|
+
if (!isEncPub(data.encpub)) throw errorWithCode('encpub statement: not a P-256 public JWK', 'encpub-unverified')
|
|
112
102
|
// Se verifica con la llave que ANUNCIA el sobre, que ya se comprobó que es la misma que
|
|
113
103
|
// la pedida: así el texto firmado y la llave que lo comprueba salen del mismo sitio.
|
|
114
104
|
if (!(await verifyData(data.publickey, data, signature))) {
|
|
115
|
-
throw
|
|
105
|
+
throw errorWithCode('encpub statement: bad signature — the key is not bound to that identity', 'encpub-unverified')
|
|
116
106
|
}
|
|
117
107
|
return data.encpub
|
|
118
108
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error con un `code` estable.
|
|
3
|
+
*
|
|
4
|
+
* El contrato es el CODE, no la frase: comprobar por el texto cruza procesos y
|
|
5
|
+
* se rompe en silencio en cuanto alguien lo reescribe o lo traduce.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} message
|
|
8
|
+
* @param {string} code
|
|
9
|
+
* @returns {Error & { code: string }}
|
|
10
|
+
*/
|
|
11
|
+
export function errorWithCode (message, code) {
|
|
12
|
+
const e = /** @type {Error & { code: string }} */ (new Error(message))
|
|
13
|
+
e.code = code
|
|
14
|
+
return e
|
|
15
|
+
}
|
|
@@ -15,14 +15,9 @@
|
|
|
15
15
|
* second, older copy of a pillar inside every consumer.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
import { errorWithCode } from './errors.js'
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
function errorCon (mensaje, code) {
|
|
22
|
-
const e = /** @type {Error & { code: string }} */ (new Error(mensaje))
|
|
23
|
-
e.code = code
|
|
24
|
-
return e
|
|
25
|
-
}
|
|
20
|
+
const ECDH = { name: 'ECDH', namedCurve: 'P-256' }
|
|
26
21
|
|
|
27
22
|
const VERSION = 1
|
|
28
23
|
|
|
@@ -137,7 +132,7 @@ export function identitySealing (identity, { app = 'dotrino' } = {}) {
|
|
|
137
132
|
|
|
138
133
|
return {
|
|
139
134
|
async seal (msg, peerEncPub, { publickey } = {}) {
|
|
140
|
-
if (!peerEncPub) throw
|
|
135
|
+
if (!peerEncPub) throw errorWithCode('no encryption key for the other side', 'unsealed')
|
|
141
136
|
// Destinatarios como OBJETOS, y CON SU `publickey`: `encrypt` expande cada uno a
|
|
142
137
|
// todos los aparatos de esa persona por su tarjeta de perfil, y la tarjeta se busca
|
|
143
138
|
// por la pubkey. Sin ella el sobre solo lo abría el aparato que anunció la llave, y
|
|
@@ -149,7 +144,7 @@ export function identitySealing (identity, { app = 'dotrino' } = {}) {
|
|
|
149
144
|
// VACÍO: cifrado de verdad, y que no abre nadie. Eso no es un sobre, es un mensaje
|
|
150
145
|
// perdido con cara de enviado.
|
|
151
146
|
if (!sealed || !sealed.wrap || Object.keys(sealed.wrap).length === 0) {
|
|
152
|
-
throw
|
|
147
|
+
throw errorWithCode('identitySealing: the vault wrapped the message for nobody', 'unsealed')
|
|
153
148
|
}
|
|
154
149
|
return { app, sealed, from: await myEncPub() }
|
|
155
150
|
},
|
|
@@ -189,20 +184,20 @@ export function isBinSealed (env) {
|
|
|
189
184
|
/** Una CEK nueva para un mensaje con bytes. */
|
|
190
185
|
export async function makeMessageKey () {
|
|
191
186
|
const { makeContentKey } = await crypto_()
|
|
192
|
-
if (typeof makeContentKey !== 'function') throw
|
|
187
|
+
if (typeof makeContentKey !== 'function') throw errorWithCode('sealing bytes needs @dotrino/identity >= 0.117.0', 'no-bytes-sealing')
|
|
193
188
|
return makeContentKey()
|
|
194
189
|
}
|
|
195
190
|
|
|
196
191
|
/** Cifra `bytes` con la CEK y devuelve los campos del sobre (`iv`, `ct`). */
|
|
197
192
|
export async function encryptBytes (cek, bytes) {
|
|
198
193
|
const { encryptBytesWithCek } = await crypto_()
|
|
199
|
-
if (typeof encryptBytesWithCek !== 'function') throw
|
|
194
|
+
if (typeof encryptBytesWithCek !== 'function') throw errorWithCode('sealing bytes needs @dotrino/identity >= 0.117.0', 'no-bytes-sealing')
|
|
200
195
|
return encryptBytesWithCek({ cek, bytes })
|
|
201
196
|
}
|
|
202
197
|
|
|
203
198
|
/** Descifra el `ct` de un sobre binario con su CEK ya abierta. */
|
|
204
199
|
export async function decryptBytes (cek, env) {
|
|
205
200
|
const { decryptBytesWithCek } = await crypto_()
|
|
206
|
-
if (typeof decryptBytesWithCek !== 'function') throw
|
|
201
|
+
if (typeof decryptBytesWithCek !== 'function') throw errorWithCode('opening bytes needs @dotrino/identity >= 0.117.0', 'no-bytes-sealing')
|
|
207
202
|
return decryptBytesWithCek({ cek, iv: env.iv, ct: env.ct })
|
|
208
203
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/roadmap@0.4.
|
|
1
|
+
Copia vendorizada de @dotrino/roadmap@0.4.30 (dotrino-roadmap/src/{index,manifests}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
3
|
El registro de versiones rotas que viaja con cada versión. Importa @dotrino/compat/ranges.
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
export const MANIFESTS = Object.freeze({
|
|
9
9
|
"dotrino": {
|
|
10
10
|
"manifest": "dotrino",
|
|
11
|
-
"seq":
|
|
12
|
-
"issued": "2026-10-
|
|
11
|
+
"seq": 30,
|
|
12
|
+
"issued": "2026-10-16",
|
|
13
13
|
"note": "Registro comun de compatibilidad del ecosistema. Se edita a mano en este repo y se consulta con @dotrino/roadmap. Rangos: ver @dotrino/compat/ranges (x.y.z, >= > <= <, «a - b», «*», o una lista que es un O).",
|
|
14
14
|
"products": {
|
|
15
15
|
"compat": {
|
|
@@ -22,18 +22,20 @@ export const MANIFESTS = Object.freeze({
|
|
|
22
22
|
"env": {
|
|
23
23
|
"repo": "imdotrino/dotrino-vault",
|
|
24
24
|
"npm": "@dotrino/env",
|
|
25
|
-
"current": "0.
|
|
25
|
+
"current": "0.90.1",
|
|
26
26
|
"protocol": 1,
|
|
27
27
|
"requires": {
|
|
28
|
-
"vault": ">=0.
|
|
28
|
+
"vault": ">=0.90.1"
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"identity": {
|
|
32
32
|
"repo": "imdotrino/dotrino-identity",
|
|
33
33
|
"npm": "@dotrino/identity",
|
|
34
|
-
"current": "0.
|
|
34
|
+
"current": "0.119.0",
|
|
35
35
|
"protocol": 1,
|
|
36
|
-
"requires": {
|
|
36
|
+
"requires": {
|
|
37
|
+
"proxy-client": ">=0.13.1"
|
|
38
|
+
}
|
|
37
39
|
},
|
|
38
40
|
"lobby": {
|
|
39
41
|
"repo": "imdotrino/dotrino-lobby",
|
|
@@ -107,12 +109,12 @@ export const MANIFESTS = Object.freeze({
|
|
|
107
109
|
},
|
|
108
110
|
"proxy": {
|
|
109
111
|
"repo": "imdotrino/dotrino-proxy",
|
|
110
|
-
"current": "1.6.
|
|
112
|
+
"current": "1.6.4",
|
|
111
113
|
"protocol": 2,
|
|
112
114
|
"requires": {
|
|
113
|
-
"vault": ">=0.
|
|
114
|
-
"identity": ">=0.
|
|
115
|
-
"proxy-client": ">=0.
|
|
115
|
+
"vault": ">=0.87.0",
|
|
116
|
+
"identity": ">=0.117.2",
|
|
117
|
+
"proxy-client": ">=0.36.1",
|
|
116
118
|
"content": "0.5.3+"
|
|
117
119
|
}
|
|
118
120
|
},
|
|
@@ -169,7 +171,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
169
171
|
},
|
|
170
172
|
"terminal-desktop": {
|
|
171
173
|
"repo": "imdotrino/dotrino-terminal",
|
|
172
|
-
"current": "0.2.
|
|
174
|
+
"current": "0.2.77",
|
|
173
175
|
"protocol": 1,
|
|
174
176
|
"requires": {
|
|
175
177
|
"terminal-agent": ">=0.37.0"
|
|
@@ -179,42 +181,43 @@ export const MANIFESTS = Object.freeze({
|
|
|
179
181
|
"repo": "imdotrino/dotrino-ia",
|
|
180
182
|
"npm": "@dotrino/ia-agent",
|
|
181
183
|
"deprecated": "2026-10-07: retired, replaced by terminal-agent (dotrino-terminal)",
|
|
182
|
-
"current": "0.
|
|
184
|
+
"current": "0.9.0",
|
|
183
185
|
"protocol": 1,
|
|
184
186
|
"requires": {
|
|
185
|
-
"remote-agent": ">=0.
|
|
186
|
-
"identity": ">=0.
|
|
187
|
-
"vault": ">=0.
|
|
187
|
+
"remote-agent": ">=0.20.1",
|
|
188
|
+
"identity": ">=0.117.2",
|
|
189
|
+
"vault": ">=0.87.0",
|
|
190
|
+
"proxy-client": ">=0.36.1"
|
|
188
191
|
}
|
|
189
192
|
},
|
|
190
193
|
"content": {
|
|
191
194
|
"repo": "imdotrino/dotrino-content",
|
|
192
195
|
"npm": "@dotrino/content",
|
|
193
|
-
"current": "0.
|
|
196
|
+
"current": "0.6.0",
|
|
194
197
|
"protocol": 1,
|
|
195
198
|
"requires": {
|
|
196
|
-
"remote-agent": ">=0.
|
|
197
|
-
"identity": ">=0.
|
|
198
|
-
"vault": ">=0.
|
|
199
|
-
"proxy-client": ">=0.
|
|
199
|
+
"remote-agent": ">=0.20.1",
|
|
200
|
+
"identity": ">=0.117.2",
|
|
201
|
+
"vault": ">=0.87.0",
|
|
202
|
+
"proxy-client": ">=0.36.1"
|
|
200
203
|
}
|
|
201
204
|
},
|
|
202
205
|
"sealers": {
|
|
203
206
|
"repo": "imdotrino/dotrino-sealers",
|
|
204
207
|
"npm": "@dotrino/sealers",
|
|
205
|
-
"current": "0.
|
|
208
|
+
"current": "0.10.0",
|
|
206
209
|
"protocol": 1,
|
|
207
210
|
"requires": {
|
|
208
|
-
"remote-agent": ">=0.
|
|
209
|
-
"identity": ">=0.
|
|
210
|
-
"vault": ">=0.
|
|
211
|
-
"proxy-client": ">=0.
|
|
211
|
+
"remote-agent": ">=0.20.1",
|
|
212
|
+
"identity": ">=0.117.2",
|
|
213
|
+
"vault": ">=0.87.0",
|
|
214
|
+
"proxy-client": ">=0.36.1"
|
|
212
215
|
}
|
|
213
216
|
},
|
|
214
217
|
"roadmap": {
|
|
215
218
|
"repo": "imdotrino/dotrino-roadmap",
|
|
216
219
|
"npm": "@dotrino/roadmap",
|
|
217
|
-
"current": "0.4.
|
|
220
|
+
"current": "0.4.30",
|
|
218
221
|
"protocol": 1,
|
|
219
222
|
"requires": {
|
|
220
223
|
"compat": "0.4.0+"
|
|
@@ -250,32 +253,32 @@ export const MANIFESTS = Object.freeze({
|
|
|
250
253
|
"vault": {
|
|
251
254
|
"repo": "imdotrino/dotrino-vault",
|
|
252
255
|
"npm": "@dotrino/vault",
|
|
253
|
-
"current": "0.
|
|
256
|
+
"current": "0.90.1",
|
|
254
257
|
"protocol": 1,
|
|
255
258
|
"requires": {
|
|
256
259
|
"vaultd": ">=0.105.0",
|
|
257
260
|
"proxy-client": ">=0.36.1",
|
|
258
|
-
"identity": ">=0.
|
|
261
|
+
"identity": ">=0.118.0",
|
|
259
262
|
"opaque": ">=0.1.0",
|
|
260
263
|
"compat": ">=0.4.0",
|
|
261
|
-
"roadmap": ">=0.4.
|
|
262
|
-
"passmanager": ">=0.
|
|
263
|
-
"store": ">=0.
|
|
264
|
+
"roadmap": ">=0.4.26",
|
|
265
|
+
"passmanager": ">=0.18.4",
|
|
266
|
+
"store": ">=0.12.3"
|
|
264
267
|
}
|
|
265
268
|
},
|
|
266
269
|
"vaultd": {
|
|
267
270
|
"repo": "imdotrino/dotrino-vault",
|
|
268
271
|
"npm": "@dotrino/vaultd",
|
|
269
|
-
"current": "0.
|
|
272
|
+
"current": "0.162.1",
|
|
270
273
|
"protocol": 1,
|
|
271
274
|
"requires": {
|
|
272
|
-
"identity": ">=0.
|
|
275
|
+
"identity": ">=0.118.0",
|
|
273
276
|
"proxy-client": ">=0.36.1",
|
|
274
277
|
"store": ">=0.11.0",
|
|
275
|
-
"passmanager": ">=0.
|
|
278
|
+
"passmanager": ">=0.18.4",
|
|
276
279
|
"compat": ">=0.4.0",
|
|
277
280
|
"opaque": ">=0.1.0",
|
|
278
|
-
"roadmap": ">=0.4.
|
|
281
|
+
"roadmap": ">=0.4.26"
|
|
279
282
|
}
|
|
280
283
|
},
|
|
281
284
|
"verifier": {
|
|
@@ -290,7 +293,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
290
293
|
"passmanager": {
|
|
291
294
|
"repo": "imdotrino/dotrino-passmanager",
|
|
292
295
|
"npm": "@dotrino/passmanager",
|
|
293
|
-
"current": "0.18.
|
|
296
|
+
"current": "0.18.4",
|
|
294
297
|
"protocol": 1,
|
|
295
298
|
"requires": {
|
|
296
299
|
"identity": ">=0.117.2",
|
|
@@ -303,7 +306,7 @@ export const MANIFESTS = Object.freeze({
|
|
|
303
306
|
"protocol": 1,
|
|
304
307
|
"requires": {
|
|
305
308
|
"proxy": "1.1.0+",
|
|
306
|
-
"vaultd": ">=0.
|
|
309
|
+
"vaultd": ">=0.160.2"
|
|
307
310
|
}
|
|
308
311
|
},
|
|
309
312
|
"passmanager-ios": {
|
|
@@ -317,18 +320,19 @@ export const MANIFESTS = Object.freeze({
|
|
|
317
320
|
},
|
|
318
321
|
"passmanager-extension": {
|
|
319
322
|
"repo": "imdotrino/dotrino-passmanager",
|
|
320
|
-
"current": "0.
|
|
323
|
+
"current": "0.54.0",
|
|
321
324
|
"protocol": 1,
|
|
322
325
|
"requires": {
|
|
323
|
-
"identity": ">=0.
|
|
326
|
+
"identity": ">=0.118.0",
|
|
324
327
|
"proxy-client": ">=0.36.1",
|
|
325
|
-
"vault": ">=0.
|
|
326
|
-
"proxy": "1.1.0+"
|
|
328
|
+
"vault": ">=0.90.0",
|
|
329
|
+
"proxy": "1.1.0+",
|
|
330
|
+
"vaultd": ">=0.160.2"
|
|
327
331
|
}
|
|
328
332
|
},
|
|
329
333
|
"app": {
|
|
330
334
|
"repo": "imdotrino/dotrino-app",
|
|
331
|
-
"current": "0.4.
|
|
335
|
+
"current": "0.4.22",
|
|
332
336
|
"protocol": 1,
|
|
333
337
|
"requires": {
|
|
334
338
|
"proxy": "1.4.7+",
|
|
@@ -338,13 +342,13 @@ export const MANIFESTS = Object.freeze({
|
|
|
338
342
|
},
|
|
339
343
|
"social-bot": {
|
|
340
344
|
"repo": "imdotrino/dotrino-social-bot",
|
|
341
|
-
"current": "0.
|
|
345
|
+
"current": "0.3.0",
|
|
342
346
|
"protocol": 1,
|
|
343
347
|
"requires": {
|
|
344
|
-
"vault": ">=0.
|
|
345
|
-
"identity": ">=0.
|
|
346
|
-
"proxy-client": ">=0.
|
|
347
|
-
"remote-agent": ">=0.
|
|
348
|
+
"vault": ">=0.87.0",
|
|
349
|
+
"identity": ">=0.117.2",
|
|
350
|
+
"proxy-client": ">=0.36.1",
|
|
351
|
+
"remote-agent": ">=0.20.1"
|
|
348
352
|
}
|
|
349
353
|
}
|
|
350
354
|
},
|
|
@@ -832,6 +836,15 @@ export const MANIFESTS = Object.freeze({
|
|
|
832
836
|
"versions": "0.36.0",
|
|
833
837
|
"why": "por token frena los bytes por lo recordado de la identidad (que puede ser de antes de que el otro se actualizara), y no solo por lo que dijo esa conexión en su saludo",
|
|
834
838
|
"fix": "@dotrino/proxy-client 0.36.1"
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
"product": "vaultd",
|
|
842
|
+
"versions": [
|
|
843
|
+
"0.160.0",
|
|
844
|
+
"0.160.1"
|
|
845
|
+
],
|
|
846
|
+
"why": "la bóveda recordaba el sí de aprobación por aparato: después de aprobar un borrado, o de leer una contraseña, ese aparato podía borrar o pisar una passkey sin que se pidiera nada ni le llegara aviso a nadie",
|
|
847
|
+
"fix": "sube a @dotrino/vaultd 0.160.2"
|
|
835
848
|
}
|
|
836
849
|
]
|
|
837
850
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/store/core@0.12.
|
|
1
|
+
Copia vendorizada de @dotrino/store/core@0.12.4 (dotrino-store/store/{core}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
3
|
Las reglas del almacén (lápidas, páginas): las mismas en el store y en la bóveda.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.90.2 (dotrino-vault/lib/src/{admin,b64,browser,core/approvals,core/backup,core/blocked,core/browserDisk,core/commKey,core/disk,core/libVersion,core/machines,core/notices,core/notifiable,core/passwords,core/sealKey,core/sealer,core/sealers,core/secretsStore,core/sha256,core/store,core/subacta,core/threadStore,core/transport,core/vault,enroll,index,loginClient,passwordLogins,procContext,protocol,revocation,sealed}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
3
|
browser.js es la bóveda de esta pestaña: el MISMO núcleo que el binario (core/), con
|
|
4
4
|
el disco en IndexedDB. Importa @dotrino/{compat,roadmap,passmanager,store/core} y
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EL RESPALDO DE LA BÓVEDA: un archivo cifrado con una contraseña, con lo que hay en los
|
|
3
|
+
* cajones y en la bóveda de contraseñas. Diseño: `docs/respaldo.md`.
|
|
4
|
+
*
|
|
5
|
+
* QUÉ LLEVA Y QUÉ NO (dueño, 2026-10-16: «como las llaves son de dispositivo, solo se
|
|
6
|
+
* respalda el contenido de sobres y cajones»):
|
|
7
|
+
*
|
|
8
|
+
* · SÍ: los VALORES de cada cajón (`ns:<scope>`, `dev:<aparato>`, el perfil `@me`) y las
|
|
9
|
+
* entradas de la bóveda de contraseñas, abiertos y vueltos a cerrar con la contraseña
|
|
10
|
+
* del respaldo.
|
|
11
|
+
* · NO: ninguna llave. Ni la maestra, ni la de comunicación, ni las de los aparatos, ni
|
|
12
|
+
* el acta. Una llave es de su aparato; quien restaura en otra bóveda la reparte con sus
|
|
13
|
+
* propias llaves, a los aparatos de SU acta.
|
|
14
|
+
* · NO: el histórico. Un respaldo es lo vigente.
|
|
15
|
+
*
|
|
16
|
+
* Por qué los valores en claro (dentro del archivo cifrado) y no los sobres tal cual: los
|
|
17
|
+
* sobres van envueltos a los aparatos de ESTA cuenta y a su copia de recuperación. En otra
|
|
18
|
+
* bóveda no los abre nadie, y mover un cajón de una bóveda a otra es la mitad de lo que esto
|
|
19
|
+
* tiene que hacer.
|
|
20
|
+
*
|
|
21
|
+
* LAS DOS PUNTAS EXIGEN LA BÓVEDA ABIERTA. Exportar es leer —lo que guarda la frase—, e
|
|
22
|
+
* importar compara con lo que ya hay, que también es leer.
|
|
23
|
+
*
|
|
24
|
+
* CONFLICTOS. Importar NUNCA pisa en silencio: primero se hace un PLAN (`planImport`) que
|
|
25
|
+
* dice, valor por valor, qué es nuevo, qué es igual y qué choca. Si algo choca y quien
|
|
26
|
+
* importa no dijo qué hacer, no se escribe NADA y se devuelve la lista (`BACKUP_CONFLICTS`).
|
|
27
|
+
* Las dos respuestas posibles son `keep` (se queda lo de la bóveda) y `replace` (gana el
|
|
28
|
+
* respaldo), para todo de una vez o para cada choque.
|
|
29
|
+
*
|
|
30
|
+
* El cifrado del archivo es el de `@dotrino/passmanager` (PBKDF2-SHA256 600 000 + AES-GCM):
|
|
31
|
+
* corre igual en el navegador y en Node, así que lo que exporta una versión de la bóveda lo
|
|
32
|
+
* abre cualquiera de las otras (regla de las tres versiones).
|
|
33
|
+
*/
|
|
34
|
+
import { deriveKeyFromPassword, makeSalt, makeVerifier, checkVerifier, sealValue, openValue, toBase64, fromBase64 } from '@dotrino/passmanager'
|
|
35
|
+
|
|
36
|
+
export const BACKUP_FORMAT = 'dotrino-vault-backup'
|
|
37
|
+
export const BACKUP_VERSION = 1
|
|
38
|
+
/** El nombre del cajón de las contraseñas en un respaldo. No choca con un `ns:`/`dev:`. */
|
|
39
|
+
export const PASSWORDS_DRAWER = 'passwords'
|
|
40
|
+
/** Mínimo de la contraseña del respaldo: el archivo se ataca fuera de línea, sin freno. */
|
|
41
|
+
export const BACKUP_PWD_MIN = 12
|
|
42
|
+
const KDF_ITERATIONS = 600_000
|
|
43
|
+
const AAD = 'dotrino-vault-backup/v1'
|
|
44
|
+
|
|
45
|
+
const coded = (message, code, extra = {}) => Object.assign(new Error(message), { code, ...extra })
|
|
46
|
+
|
|
47
|
+
// --- el archivo ----------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* CIERRA un respaldo con su contraseña. Devuelve el objeto que se escribe al archivo.
|
|
51
|
+
* Lo único en claro es el formato y la fecha: ni los nombres de los cajones salen.
|
|
52
|
+
*/
|
|
53
|
+
export async function sealBackup (payload, password) {
|
|
54
|
+
if (typeof password !== 'string' || password.length < BACKUP_PWD_MIN) {
|
|
55
|
+
throw coded(`the backup password must be at least ${BACKUP_PWD_MIN} characters`, 'WEAK_PASSWORD')
|
|
56
|
+
}
|
|
57
|
+
const salt = makeSalt()
|
|
58
|
+
const key = await deriveKeyFromPassword(password, salt, KDF_ITERATIONS)
|
|
59
|
+
return {
|
|
60
|
+
format: BACKUP_FORMAT,
|
|
61
|
+
v: BACKUP_VERSION,
|
|
62
|
+
createdAt: payload.createdAt,
|
|
63
|
+
kdf: { name: 'PBKDF2-SHA256', iterations: KDF_ITERATIONS, salt: toBase64(salt) },
|
|
64
|
+
verifier: await makeVerifier(key),
|
|
65
|
+
box: await sealValue(key, JSON.stringify(payload), AAD)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* ABRE un respaldo. Distingue «no es un respaldo», «esa contraseña no es» y «está roto»,
|
|
71
|
+
* porque se arreglan de formas distintas.
|
|
72
|
+
*/
|
|
73
|
+
export async function openBackup (file, password) {
|
|
74
|
+
if (!file || file.format !== BACKUP_FORMAT) throw coded('this is not a vault backup file', 'NOT_A_BACKUP')
|
|
75
|
+
if (file.v !== BACKUP_VERSION) throw coded(`unknown backup version v${file.v}: update the vault`, 'BACKUP_VERSION')
|
|
76
|
+
if (file.kdf?.name !== 'PBKDF2-SHA256' || !file.kdf.salt || !file.kdf.iterations) {
|
|
77
|
+
throw coded('the backup file has no usable key derivation', 'BACKUP_CORRUPT')
|
|
78
|
+
}
|
|
79
|
+
const key = await deriveKeyFromPassword(String(password || ''), fromBase64(file.kdf.salt), file.kdf.iterations)
|
|
80
|
+
if (!(await checkVerifier(key, file.verifier))) throw coded('that password does not open this backup', 'WRONG_PASSWORD')
|
|
81
|
+
let payload
|
|
82
|
+
try { payload = JSON.parse(await openValue(key, file.box, AAD)) } catch (_) {
|
|
83
|
+
throw coded('the backup opens but its content cannot be read', 'BACKUP_CORRUPT')
|
|
84
|
+
}
|
|
85
|
+
if (!Array.isArray(payload?.drawers)) throw coded('the backup has no drawers', 'BACKUP_CORRUPT')
|
|
86
|
+
return payload
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- qué cajones -----------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* ¿Entra este cajón? `selected` es la lista que pidió quien exporta o importa (`null` =
|
|
93
|
+
* todos). Vale el dueño entero (`ns:proxy`, `dev:<pub>`, `passwords`) y `@me` como atajo.
|
|
94
|
+
*/
|
|
95
|
+
export function wants (selected, owner) {
|
|
96
|
+
if (!selected || !selected.length) return true
|
|
97
|
+
return selected.some((s) => s === owner || (s === '@me' && owner === 'ns:@me'))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Valida que todo lo pedido exista: pedir un cajón que no está no es «nada que hacer». */
|
|
101
|
+
export function assertSelected (selected, available) {
|
|
102
|
+
if (!selected || !selected.length) return
|
|
103
|
+
const missing = selected.filter((s) => !available.some((o) => wants([s], o)))
|
|
104
|
+
if (missing.length) throw coded(`no such drawer: ${missing.join(', ')}`, 'NO_SUCH_DRAWER', { missing })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- importar: el plan -----------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/** Lo que identifica un valor dentro de un plan: `<cajón destino> <clave>`. */
|
|
110
|
+
export const itemRef = (owner, key) => `${owner} ${key}`
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* EL PLAN DE IMPORTACIÓN. Puro: no lee ni escribe nada, recibe lo que hay.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} payload el respaldo ya abierto
|
|
116
|
+
* @param {object} o
|
|
117
|
+
* `selected` qué cajones del respaldo (`null` = todos)
|
|
118
|
+
* `map` `{ <cajón del respaldo>: <cajón destino> }` para moverlo de nombre o de
|
|
119
|
+
* aparato. Sin entrada, va al mismo nombre.
|
|
120
|
+
* `current` `{ <cajón destino>: { KEY: {value, public} } | null }` lo que ya hay
|
|
121
|
+
* `devices` pubs de los aparatos del acta de destino: un `dev:` solo entra si su
|
|
122
|
+
* aparato existe aquí
|
|
123
|
+
* `passwords` `{ entries: [...] }` lo que ya hay en la bóveda de contraseñas, abierto
|
|
124
|
+
*/
|
|
125
|
+
export function planImport (payload, { selected = null, map = {}, current = {}, devices = [], passwords = null } = {}) {
|
|
126
|
+
const available = payload.drawers.map((d) => d.owner)
|
|
127
|
+
assertSelected(selected, available)
|
|
128
|
+
const withDevice = new Set(devices)
|
|
129
|
+
const plan = { drawers: [], counts: { new: 0, same: 0, conflict: 0, skipped: 0 } }
|
|
130
|
+
for (const d of payload.drawers) {
|
|
131
|
+
if (!wants(selected, d.owner)) continue
|
|
132
|
+
const to = map[d.owner] || d.owner
|
|
133
|
+
if (d.owner === PASSWORDS_DRAWER) {
|
|
134
|
+
plan.drawers.push(planPasswords(d, passwords, plan.counts))
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
const out = { from: d.owner, to, label: d.label || null, status: 'ok', items: [] }
|
|
138
|
+
if (to.startsWith('dev:') && !withDevice.has(to.slice(4))) {
|
|
139
|
+
// EL APARATO NO ESTÁ EN ESTA CUENTA. No se guarda «por si acaso»: un cajón de aparato
|
|
140
|
+
// sin aparato no lo lee nadie, y reaparecería solo si un día entra esa llave. Se dice,
|
|
141
|
+
// y se puede mover a otro aparato con `map`.
|
|
142
|
+
out.status = 'no-device'
|
|
143
|
+
plan.counts.skipped += Object.keys(d.vars || {}).length
|
|
144
|
+
plan.drawers.push(out)
|
|
145
|
+
continue
|
|
146
|
+
}
|
|
147
|
+
const have = current[to] || {}
|
|
148
|
+
for (const [key, v] of Object.entries(d.vars || {})) {
|
|
149
|
+
const existing = have[key]
|
|
150
|
+
const status = !existing ? 'new' : (existing.value === v.value ? 'same' : 'conflict')
|
|
151
|
+
plan.counts[status]++
|
|
152
|
+
out.items.push({ key, status, public: !!v.public, ...(existing ? { currentPublic: !!existing.public } : {}) })
|
|
153
|
+
}
|
|
154
|
+
plan.drawers.push(out)
|
|
155
|
+
}
|
|
156
|
+
return plan
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Lo que dice que una entrada de contraseñas es «la misma» que otra: el mismo id, o los
|
|
161
|
+
* mismos sitios y el mismo usuario (una credencial por sitio y usuario).
|
|
162
|
+
*/
|
|
163
|
+
const sitesKey = (e) => [...(e.sites || [])].map((s) => String(s).toLowerCase()).sort().join(' ')
|
|
164
|
+
const whoKey = (e) => `${sitesKey(e)}\u0000${String(e.username || '').toLowerCase()}\u0000${e.sites?.length ? '' : String(e.title || '')}`
|
|
165
|
+
/** Lo que se compara para decir «igual»: el contenido, sin fechas. */
|
|
166
|
+
export const entryContent = (e) => JSON.stringify({
|
|
167
|
+
type: e.type || 'login', title: e.title || '', sites: [...(e.sites || [])].sort(),
|
|
168
|
+
name: e.name || '', username: e.username || '', secret: e.secret || '', totp: e.totp || '',
|
|
169
|
+
notes: e.notes || '', fields: e.fields || '[]', webauthn: e.webauthn || null
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
function planPasswords (d, passwords, counts) {
|
|
173
|
+
const out = { from: PASSWORDS_DRAWER, to: PASSWORDS_DRAWER, status: 'ok', items: [] }
|
|
174
|
+
if (!passwords) {
|
|
175
|
+
// Sin bóveda de contraseñas en el destino no hay con quién comparar ni dónde guardar:
|
|
176
|
+
// se dice. No se inventa una llave de perfil a medias.
|
|
177
|
+
out.status = 'no-passwords'
|
|
178
|
+
counts.skipped += (d.entries || []).length
|
|
179
|
+
return out
|
|
180
|
+
}
|
|
181
|
+
const byId = new Map(passwords.entries.map((e) => [e.id, e]))
|
|
182
|
+
const byWho = new Map(passwords.entries.map((e) => [whoKey(e), e]))
|
|
183
|
+
for (const e of d.entries || []) {
|
|
184
|
+
const existing = byId.get(e.id) || byWho.get(whoKey(e)) || null
|
|
185
|
+
const status = !existing ? 'new' : (entryContent(existing) === entryContent(e) ? 'same' : 'conflict')
|
|
186
|
+
counts[status]++
|
|
187
|
+
out.items.push({ key: e.id, title: e.title || (e.sites || [])[0] || '', username: e.username || '', status, ...(existing ? { currentId: existing.id } : {}) })
|
|
188
|
+
}
|
|
189
|
+
return out
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* LA DECISIÓN DE CADA CHOQUE. `onConflict` vale para todos; `decisions` (por `itemRef`) manda
|
|
194
|
+
* sobre él. Si queda alguno sin decidir, se para con la lista entera: importar a medias y
|
|
195
|
+
* descubrir el resto después es peor que no importar.
|
|
196
|
+
*/
|
|
197
|
+
export function resolvePlan (plan, { onConflict = null, decisions = {} } = {}) {
|
|
198
|
+
if (onConflict && onConflict !== 'keep' && onConflict !== 'replace') {
|
|
199
|
+
throw coded(`unknown conflict policy "${onConflict}" (use keep or replace)`, 'BAD_POLICY')
|
|
200
|
+
}
|
|
201
|
+
const pending = []
|
|
202
|
+
for (const d of plan.drawers) {
|
|
203
|
+
for (const it of d.items) {
|
|
204
|
+
if (it.status !== 'conflict') continue
|
|
205
|
+
const r = decisions[itemRef(d.to, it.key)] || onConflict
|
|
206
|
+
if (r !== 'keep' && r !== 'replace') { pending.push({ drawer: d.to, key: it.key, ...(it.title ? { title: it.title } : {}) }); continue }
|
|
207
|
+
it.resolution = r
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (pending.length) {
|
|
211
|
+
throw coded(`${pending.length} conflict(s) without a decision: nothing was imported`, 'BACKUP_CONFLICTS', { conflicts: pending, plan })
|
|
212
|
+
}
|
|
213
|
+
return plan
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* LO QUE HAY QUE ESCRIBIR, ya resuelto: `[{ to, key, value, public }]` para los cajones y
|
|
218
|
+
* `[{ entry, replaces }]` para las contraseñas. Lo `same` y lo `keep` no se tocan.
|
|
219
|
+
*
|
|
220
|
+
* La VISIBILIDAD: si lo que había era privado, se queda privado aunque el respaldo diga
|
|
221
|
+
* público. Hacer público un secreto no puede colarse en una importación (es la misma regla
|
|
222
|
+
* de `PrivateStaysPrivate`), y se anota para decirlo.
|
|
223
|
+
*/
|
|
224
|
+
export function writesOf (plan, payload) {
|
|
225
|
+
const vars = []
|
|
226
|
+
const entries = []
|
|
227
|
+
const byOwner = new Map(payload.drawers.map((d) => [d.owner, d]))
|
|
228
|
+
for (const d of plan.drawers) {
|
|
229
|
+
if (d.status !== 'ok') continue
|
|
230
|
+
const src = byOwner.get(d.from)
|
|
231
|
+
for (const it of d.items) {
|
|
232
|
+
if (it.status === 'same' || (it.status === 'conflict' && it.resolution !== 'replace')) continue
|
|
233
|
+
if (d.from === PASSWORDS_DRAWER) {
|
|
234
|
+
const entry = src.entries.find((e) => e.id === it.key)
|
|
235
|
+
entries.push({ entry, replaces: it.status === 'conflict' ? it.currentId : null })
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
const v = src.vars[it.key]
|
|
239
|
+
const keptPrivate = it.currentPublic === false && !!v.public
|
|
240
|
+
vars.push({ to: d.to, key: it.key, value: v.value, public: keptPrivate ? false : !!v.public, ...(keptPrivate ? { keptPrivate: true } : {}) })
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { vars, entries }
|
|
244
|
+
}
|