@dotrino/identity 0.107.0 → 0.107.1
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/vault/content.js +88 -45
- package/vault/core.js +4 -3
package/package.json
CHANGED
package/vault/content.js
CHANGED
|
@@ -118,56 +118,25 @@ export const CEK_ENVELOPE = 'dotrino-cek'
|
|
|
118
118
|
export const isCekEnvelope = (v) => !!v && typeof v === 'object' && v.t === CEK_ENVELOPE &&
|
|
119
119
|
Number.isInteger(v.gen) && typeof v.iv === 'string' && typeof v.ct === 'string'
|
|
120
120
|
|
|
121
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Cifra con una clave simétrica. Devuelve `{ gen, iv, ct }` (el `gen` dice con cuál).
|
|
123
|
+
*
|
|
124
|
+
* Es GENÉRICA: la usan la llave de la cuenta y también llaves que no son de la cuenta (los
|
|
125
|
+
* cajones de secretos, cada entrada del gestor de contraseñas, el transporte). Por eso NO
|
|
126
|
+
* lleva la marca: la marca dice «esto es de la cuenta» y solo la pone `sealAccount`. En
|
|
127
|
+
* 0.107.0 la ponía aquí, y marcaba como de la cuenta sobres que la cuenta no abre.
|
|
128
|
+
*/
|
|
122
129
|
export async function encryptWithCek ({ cek, gen, plaintext }) {
|
|
123
130
|
const k = await subtle.importKey('raw', fromB64(cek), { name: 'AES-GCM' }, false, ['encrypt'])
|
|
124
131
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))
|
|
125
132
|
const ct = await subtle.encrypt({ name: 'AES-GCM', iv }, k, new TextEncoder().encode(plaintext))
|
|
126
|
-
return {
|
|
133
|
+
return { gen, iv: b64(iv), ct: b64(ct) }
|
|
127
134
|
}
|
|
128
135
|
|
|
129
|
-
/**
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
*
|
|
134
|
-
* Es lo que hace la bóveda al abrirse después de una rotación (dueño, 2026-09-30): lo que
|
|
135
|
-
* sirve va SIEMPRE con la llave vigente, así que un aparato que entra después —que solo
|
|
136
|
-
* recibe esa generación— abre todo, y la llave vieja deja de abrir nada porque ya no se
|
|
137
|
-
* le sirve nada cerrado con ella.
|
|
138
|
-
*
|
|
139
|
-
* `reseal(envelope)` abre y vuelve a cerrar un sobre; lo pone quien tiene las llaves. Si
|
|
140
|
-
* un sobre no se puede abrir, se PARA: no se deja a medias ni se salta en silencio.
|
|
141
|
-
*
|
|
142
|
-
* @param {any} value
|
|
143
|
-
* @param {{ gen: number, reseal: (env: object) => Promise<object> }} o
|
|
144
|
-
* @returns {Promise<{ value: any, changed: number }>}
|
|
145
|
-
*/
|
|
146
|
-
export async function resealStale (value, { gen, reseal }) {
|
|
147
|
-
if (!Number.isInteger(gen)) throw new Error('resealStale: the current generation is required')
|
|
148
|
-
let changed = 0
|
|
149
|
-
const walk = async (v) => {
|
|
150
|
-
if (isCekEnvelope(v)) {
|
|
151
|
-
if (v.gen === gen) return v
|
|
152
|
-
const nuevo = await reseal(v)
|
|
153
|
-
if (!isCekEnvelope(nuevo) || nuevo.gen !== gen) throw new Error(`resealStale: reseal did not return an envelope of generation ${gen}`)
|
|
154
|
-
changed++
|
|
155
|
-
return nuevo
|
|
156
|
-
}
|
|
157
|
-
if (Array.isArray(v)) {
|
|
158
|
-
const out = []
|
|
159
|
-
for (const x of v) out.push(await walk(x))
|
|
160
|
-
return out
|
|
161
|
-
}
|
|
162
|
-
if (v && typeof v === 'object') {
|
|
163
|
-
const out = {}
|
|
164
|
-
for (const [k, x] of Object.entries(v)) out[k] = await walk(x)
|
|
165
|
-
return out
|
|
166
|
-
}
|
|
167
|
-
return v
|
|
168
|
-
}
|
|
169
|
-
const out = await walk(value)
|
|
170
|
-
return { value: out, changed }
|
|
136
|
+
/** Cierra con la LLAVE DE LA CUENTA (generación `gen`): el sobre lleva la marca. */
|
|
137
|
+
export async function sealAccount ({ cek, gen, plaintext }) {
|
|
138
|
+
if (!Number.isInteger(gen) || gen < 1) throw new Error('sealAccount: the account key generation starts at 1')
|
|
139
|
+
return { t: CEK_ENVELOPE, ...(await encryptWithCek({ cek, gen, plaintext })) }
|
|
171
140
|
}
|
|
172
141
|
|
|
173
142
|
/**
|
|
@@ -198,7 +167,81 @@ export async function decryptWithKeyring ({ envelope, keyring, myPub, myEncPriva
|
|
|
198
167
|
return new TextDecoder().decode(pt)
|
|
199
168
|
}
|
|
200
169
|
|
|
170
|
+
/**
|
|
171
|
+
* MIGRACIÓN, con fecha de caducidad: hasta 0.107.0 los sobres de la cuenta salían SIN marca,
|
|
172
|
+
* como `{ gen, iv, ct }` a secas. Hasta esta fecha, `resealStale` también prueba esos: si
|
|
173
|
+
* abren con la llave de la cuenta, los vuelve a cerrar ya marcados; si no abren, no eran de
|
|
174
|
+
* la cuenta y se dejan como están. Pasada la fecha, un sobre sin marca ya no se toca. Se
|
|
175
|
+
* quita el código cuando pase (y su prueba con él).
|
|
176
|
+
*/
|
|
177
|
+
export const LEGACY_UNMARKED_UNTIL = Date.parse('2027-06-30T00:00:00Z')
|
|
178
|
+
|
|
179
|
+
/** ¿Tiene la forma EXACTA de un sobre de la cuenta anterior a la marca? */
|
|
180
|
+
export const isLegacyCekEnvelope = (v) => !!v && typeof v === 'object' && !Array.isArray(v) &&
|
|
181
|
+
Object.keys(v).length === 3 && Number.isInteger(v.gen) && v.gen >= 1 &&
|
|
182
|
+
typeof v.iv === 'string' && typeof v.ct === 'string'
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* VUELVE A CERRAR con la generación vigente todo sobre de la cuenta de una generación
|
|
186
|
+
* anterior que haya dentro de `value`, a cualquier profundidad. Devuelve una copia (no toca
|
|
187
|
+
* el original) y cuántos sobres cambió.
|
|
188
|
+
*
|
|
189
|
+
* Es lo que hace la bóveda al abrirse y después de una rotación (dueño, 2026-09-30): lo que
|
|
190
|
+
* sirve va SIEMPRE con la llave vigente, así que un aparato que entra después —que solo
|
|
191
|
+
* recibe esa generación— abre todo, y la llave vieja deja de abrir nada porque ya no se le
|
|
192
|
+
* sirve nada cerrado con ella.
|
|
193
|
+
*
|
|
194
|
+
* `reseal(envelope)` abre y vuelve a cerrar un sobre con la llave de la cuenta; lo pone quien
|
|
195
|
+
* la tiene, y debe devolver un sobre MARCADO de la generación `gen`.
|
|
196
|
+
*
|
|
197
|
+
* · Un sobre MARCADO que no abre PARA todo: es de la cuenta y algo va mal; no se deja a
|
|
198
|
+
* medias ni se salta en silencio.
|
|
199
|
+
* · Un sobre SIN MARCA (migración, hasta `LEGACY_UNMARKED_UNTIL`) se prueba aunque ya
|
|
200
|
+
* tenga la generación vigente —hay que ponerle la marca—; si no abre, se deja y se
|
|
201
|
+
* cuenta en `skipped`.
|
|
202
|
+
*
|
|
203
|
+
* @param {any} value
|
|
204
|
+
* @param {{ gen: number, reseal: (env: object) => Promise<object>, now?: number }} o
|
|
205
|
+
* @returns {Promise<{ value: any, changed: number, skipped: number }>}
|
|
206
|
+
*/
|
|
207
|
+
export async function resealStale (value, { gen, reseal, now = Date.now() }) {
|
|
208
|
+
if (!Number.isInteger(gen)) throw new Error('resealStale: the current generation is required')
|
|
209
|
+
const migrar = now < LEGACY_UNMARKED_UNTIL
|
|
210
|
+
let changed = 0; let skipped = 0
|
|
211
|
+
const comprobar = (nuevo) => {
|
|
212
|
+
if (!isCekEnvelope(nuevo) || nuevo.gen !== gen) throw new Error(`resealStale: reseal did not return a marked envelope of generation ${gen}`)
|
|
213
|
+
return nuevo
|
|
214
|
+
}
|
|
215
|
+
const walk = async (v) => {
|
|
216
|
+
if (isCekEnvelope(v)) {
|
|
217
|
+
if (v.gen === gen) return v
|
|
218
|
+
const nuevo = comprobar(await reseal(v))
|
|
219
|
+
changed++
|
|
220
|
+
return nuevo
|
|
221
|
+
}
|
|
222
|
+
if (migrar && isLegacyCekEnvelope(v)) {
|
|
223
|
+
let nuevo
|
|
224
|
+
try { nuevo = await reseal(v) } catch (_) { skipped++; return v }
|
|
225
|
+
changed++
|
|
226
|
+
return comprobar(nuevo)
|
|
227
|
+
}
|
|
228
|
+
if (Array.isArray(v)) {
|
|
229
|
+
const out = []
|
|
230
|
+
for (const x of v) out.push(await walk(x))
|
|
231
|
+
return out
|
|
232
|
+
}
|
|
233
|
+
if (v && typeof v === 'object') {
|
|
234
|
+
const out = {}
|
|
235
|
+
for (const [k, x] of Object.entries(v)) out[k] = await walk(x)
|
|
236
|
+
return out
|
|
237
|
+
}
|
|
238
|
+
return v
|
|
239
|
+
}
|
|
240
|
+
const out = await walk(value)
|
|
241
|
+
return { value: out, changed, skipped }
|
|
242
|
+
}
|
|
243
|
+
|
|
201
244
|
export default {
|
|
202
245
|
makeContentKey, wrapForMember, openWrap, makeGeneration, myContentKey,
|
|
203
|
-
encryptWithCek, decryptWithCek, decryptWithKeyring, CEK_ENVELOPE, isCekEnvelope, resealStale
|
|
246
|
+
encryptWithCek, sealAccount, decryptWithCek, decryptWithKeyring, CEK_ENVELOPE, isCekEnvelope, isLegacyCekEnvelope, LEGACY_UNMARKED_UNTIL, resealStale
|
|
204
247
|
}
|
package/vault/core.js
CHANGED
|
@@ -2695,13 +2695,14 @@ export async function createIdentityCore ({ kv: hostKv, peers, makeSync = null,
|
|
|
2695
2695
|
async contentKey () { return myCek() },
|
|
2696
2696
|
|
|
2697
2697
|
/**
|
|
2698
|
-
* Cifra algo con la clave de contenido del perfil. Devuelve el sobre
|
|
2699
|
-
*
|
|
2698
|
+
* Cifra algo con la clave de contenido del perfil. Devuelve el sobre MARCADO
|
|
2699
|
+
* `{ t: 'dotrino-cek', gen, iv, ct }` (la marca deja a la bóveda reconocerlo y volver a
|
|
2700
|
+
* cerrarlo al rotar). La llave privada de cifrado NUNCA sale de aquí.
|
|
2700
2701
|
*/
|
|
2701
2702
|
async sealContent ({ plaintext } = {}) {
|
|
2702
2703
|
const mine = await myCek()
|
|
2703
2704
|
if (!mine) throw new Error('this device does not hold the profile content key yet')
|
|
2704
|
-
return Content.
|
|
2705
|
+
return Content.sealAccount({ cek: mine.cek, gen: mine.gen, plaintext: String(plaintext) })
|
|
2705
2706
|
},
|
|
2706
2707
|
|
|
2707
2708
|
/**
|