@dotrino/identity 0.32.0 → 0.34.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 +5 -2
- package/src/index.js +23 -4
- package/src/node.js +4 -2
- package/vault/capabilities.js +4 -6
- package/vault/core.js +101 -18
- package/vault/keyid.js +41 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/identity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Identidad y rating de usuarios compartidos entre apps de Dotrino (vault iframe + postMessage)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
},
|
|
28
28
|
"./content": {
|
|
29
29
|
"import": "./vault/content.js"
|
|
30
|
+
},
|
|
31
|
+
"./keyid": {
|
|
32
|
+
"import": "./vault/keyid.js"
|
|
30
33
|
}
|
|
31
34
|
},
|
|
32
35
|
"files": [
|
|
@@ -52,7 +55,7 @@
|
|
|
52
55
|
"url": "git+https://github.com/imdotrino/dotrino-identity.git"
|
|
53
56
|
},
|
|
54
57
|
"dependencies": {
|
|
55
|
-
"@dotrino/proxy-client": "0.
|
|
58
|
+
"@dotrino/proxy-client": "0.8.0"
|
|
56
59
|
},
|
|
57
60
|
"devDependencies": {
|
|
58
61
|
"fake-indexeddb": "^6.2.5"
|
package/src/index.js
CHANGED
|
@@ -299,6 +299,13 @@ export class Identity {
|
|
|
299
299
|
|
|
300
300
|
/** Adopta un acta recibida de otro miembro (gana el seq mayor; a igual seq, el traspaso). */
|
|
301
301
|
async adoptActa (acta) { return this._call('adoptActa', { acta }) }
|
|
302
|
+
/**
|
|
303
|
+
* Une ESTE perfil a la cuenta de otro (la de una bóveda). No es adoptar una versión nueva
|
|
304
|
+
* de la tuya: esta llave pasa a ser de OTRA cuenta y deja de tener la suya, así que solo
|
|
305
|
+
* procede sobre un perfil creado con `{ forVault: true }`. Si no, devuelve
|
|
306
|
+
* `{ joined: false, reason: 'perfil-con-datos' }` y no escribe nada.
|
|
307
|
+
*/
|
|
308
|
+
async joinProfile (acta) { return this._call('joinProfile', { acta }) }
|
|
302
309
|
/** MI tarjeta de perfil: lo mínimo que un contacto necesita para cifrarme a todos mis
|
|
303
310
|
* dispositivos (perfil, versión y llaves). Sin etiquetas ni permisos. */
|
|
304
311
|
async profileCard () { return this._call('profileCard') }
|
|
@@ -321,10 +328,16 @@ export class Identity {
|
|
|
321
328
|
* nunca sale), hace el emparejamiento endurecido por el proxy y guarda el cert.
|
|
322
329
|
* Emite un evento 'vault' { phase:'challenge', deviceId, sas } para que muestres el
|
|
323
330
|
* código a comparar; resuelve cuando el dueño aprueba en su PC (espera hasta 3 min).
|
|
331
|
+
*
|
|
332
|
+
* `join` dice **de qué cuenta se está hablando** (`vinculacion-de-cuentas.md` §3):
|
|
333
|
+
* · `'new'` → crea aquí una cuenta MÁS, con llave nueva, y es esa la que entra en la
|
|
334
|
+
* bóveda. La que estabas usando no se toca. **Es lo normal.**
|
|
335
|
+
* · `'current'` → sigue con la cuenta abierta; solo vale si nació para adoptar o si ya
|
|
336
|
+
* está emparejada con esa misma bóveda. Si no, falla antes de tocar la red.
|
|
324
337
|
* @returns {Promise<{ ok:boolean, deviceId:string, master:string, exp:number, scope:string[] }>}
|
|
325
338
|
*/
|
|
326
|
-
async enrollDevice (qr, { label = '' } = {}) {
|
|
327
|
-
return this._call('vaultPair', { qr, label }, 200000)
|
|
339
|
+
async enrollDevice (qr, { label = '', join = 'current' } = {}) {
|
|
340
|
+
return this._call('vaultPair', { qr, label, join }, 200000)
|
|
328
341
|
}
|
|
329
342
|
|
|
330
343
|
/** Estado de emparejamiento: { paired, deviceId?, master?, scope?, exp?, pairedAt? }. */
|
|
@@ -410,8 +423,14 @@ export class Identity {
|
|
|
410
423
|
async listProfiles () { return this._call('listProfiles') }
|
|
411
424
|
/** El perfil activo: { id, name, pubkey }. */
|
|
412
425
|
async currentProfile () { return this._call('currentProfile') }
|
|
413
|
-
/**
|
|
414
|
-
|
|
426
|
+
/**
|
|
427
|
+
* Crea un perfil nuevo (identidad fresca) y lo deja activo. La app debe recargar.
|
|
428
|
+
*
|
|
429
|
+
* `forVault: true` lo marca como **nacido para adoptar** la cuenta de una bóveda (camino B
|
|
430
|
+
* de `vinculacion-de-cuentas.md`): es el único permiso que acepta `joinProfile`, y se
|
|
431
|
+
* consume al unirse. Sin esa marca, la cuenta es de este dispositivo y nadie se la lleva.
|
|
432
|
+
*/
|
|
433
|
+
async createProfile (name, { forVault = false } = {}) { return this._call('createProfile', { name, forVault }) }
|
|
415
434
|
/** Cambia el perfil activo. La app debe recargar la página. */
|
|
416
435
|
async switchProfile (id) { return this._call('switchProfile', { id }) }
|
|
417
436
|
/** Renombra un perfil (o el activo si no se pasa id). */
|
package/src/node.js
CHANGED
|
@@ -173,6 +173,8 @@ export class Identity {
|
|
|
173
173
|
renounceCaps (caps) { return this._h('renounceCaps', { caps }) }
|
|
174
174
|
absorbRenounce (record) { return this._h('absorbRenounce', { record }) }
|
|
175
175
|
adoptActa (acta) { return this._h('adoptActa', { acta }) }
|
|
176
|
+
/** Une ESTE perfil a la cuenta de otro (solo si nació para eso: `createProfile(n, { forVault: true })`). */
|
|
177
|
+
joinProfile (acta) { return this._h('joinProfile', { acta }) }
|
|
176
178
|
/** MI tarjeta de perfil: lo mínimo que un contacto necesita para cifrarme a todos mis
|
|
177
179
|
* dispositivos (perfil, versión y llaves). Sin etiquetas ni permisos. */
|
|
178
180
|
profileCard () { return this._h('profileCard') }
|
|
@@ -187,7 +189,7 @@ export class Identity {
|
|
|
187
189
|
/** Rota la clave de contenido (corta el acceso al contenido FUTURO de quien ya no está). */
|
|
188
190
|
rotateContentKey () { return this._h('rotateContentKey') }
|
|
189
191
|
// Emparejar ESTE dispositivo con el vault del usuario (Fase 1)
|
|
190
|
-
enrollDevice (qr, { label = '' } = {}) { return this._h('vaultPair', { qr, label }) }
|
|
192
|
+
enrollDevice (qr, { label = '', join = 'current' } = {}) { return this._h('vaultPair', { qr, label, join }) }
|
|
191
193
|
vaultStatus () { return this._h('vaultStatus') }
|
|
192
194
|
unpairDevice () { return this._h('vaultUnpair') }
|
|
193
195
|
vaultSign (payload) { return this._h('vaultSign', { payload }) }
|
|
@@ -198,7 +200,7 @@ export class Identity {
|
|
|
198
200
|
// Multi-perfil por dispositivo (crear/cambiar reinicializa con el nuevo perfil activo).
|
|
199
201
|
listProfiles () { return this._h('listProfiles') }
|
|
200
202
|
currentProfile () { return this._h('currentProfile') }
|
|
201
|
-
createProfile (name) { return this._h('createProfile', { name }) }
|
|
203
|
+
createProfile (name, { forVault = false } = {}) { return this._h('createProfile', { name, forVault }) }
|
|
202
204
|
switchProfile (id) { return this._h('switchProfile', { id }) }
|
|
203
205
|
renameProfile (id, name) { return this._h('renameProfile', { id, name }) }
|
|
204
206
|
deleteProfile (id) { return this._h('deleteProfile', { id }) }
|
package/vault/capabilities.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* servidor de geo sin cargar el iframe.
|
|
22
22
|
*/
|
|
23
23
|
import { canonicalStringify, bufToBase64, base64ToBuf } from './core.js'
|
|
24
|
+
import { pubkeyId, keyLabel } from './keyid.js'
|
|
24
25
|
|
|
25
26
|
const ECDSA = { name: 'ECDSA', namedCurve: 'P-256' }
|
|
26
27
|
const SIGN = { name: 'ECDSA', hash: { name: 'SHA-256' } }
|
|
@@ -46,12 +47,9 @@ async function rawVerify (publicJwkStr, bytes, sigB64) {
|
|
|
46
47
|
const publicOf = (privateJwk) => ({ kty: privateJwk.kty, crv: privateJwk.crv, x: privateJwk.x, y: privateJwk.y })
|
|
47
48
|
const scopeAllows = (scope, expected) => Array.isArray(scope) ? scope.includes(expected) : scope === expected
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const h = await crypto.subtle.digest('SHA-256', enc(canonicalStringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y })))
|
|
53
|
-
return [...new Uint8Array(h)].map(b => b.toString(16).padStart(2, '0')).join('')
|
|
54
|
-
}
|
|
50
|
+
// El id de una llave y su huella legible viven en `./keyid.js`, que no tiene dependencias:
|
|
51
|
+
// así una interfaz puede mostrar `AB12-CD34` sin arrastrar todo esto. Una sola implementación.
|
|
52
|
+
export { pubkeyId, keyLabel }
|
|
55
53
|
|
|
56
54
|
/**
|
|
57
55
|
* Verifica que `signature` (base64) sobre `data` fue hecha por la privada de
|
package/vault/core.js
CHANGED
|
@@ -178,6 +178,19 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
178
178
|
const loadProfiles = () => { try { return JSON.parse(rawKv.getItem(PROFILES_STORAGE) || '[]') || [] } catch { return [] } }
|
|
179
179
|
const saveProfiles = (list) => rawKv.setItem(PROFILES_STORAGE, JSON.stringify(list))
|
|
180
180
|
|
|
181
|
+
// ----- la MARCA de «este perfil nació para adoptar la cuenta de una bóveda» -----
|
|
182
|
+
// Unirse a otra cuenta borra la que este perfil tenía, así que no puede pasar por
|
|
183
|
+
// accidente ni deducirse de heurísticas tipo «parece vacío»: se pide a propósito con
|
|
184
|
+
// `createProfile({ forVault: true })` y esa marca es el único permiso que vale.
|
|
185
|
+
// Además identity NO PUEDE ver el contenido del store (vive en otro origen), así que
|
|
186
|
+
// «vacío» no es algo que pueda comprobar por su cuenta.
|
|
187
|
+
// Ver `dotrino-vault/docs/vinculacion-de-cuentas.md` §5.1.
|
|
188
|
+
const isPendingJoin = (pid = currentPid) => !!loadProfiles().find((p) => p.id === pid)?.pendingJoin
|
|
189
|
+
const clearPendingJoin = (pid = currentPid) => {
|
|
190
|
+
const list = loadProfiles(); const e = list.find((p) => p.id === pid)
|
|
191
|
+
if (e?.pendingJoin) { delete e.pendingJoin; saveProfiles(list) }
|
|
192
|
+
}
|
|
193
|
+
|
|
181
194
|
// ----- keypair loaders -----
|
|
182
195
|
// Con `keyStore` (IndexedDB del navegador): la privada vive como CryptoKey
|
|
183
196
|
// NO EXTRACTABLE — puede FIRMAR/DERIVAR pero nadie (ni este código, ni un XSS)
|
|
@@ -440,13 +453,18 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
440
453
|
}
|
|
441
454
|
|
|
442
455
|
/**
|
|
443
|
-
* UNIRSE a
|
|
444
|
-
*
|
|
445
|
-
*
|
|
456
|
+
* UNIRSE a la cuenta de la bóveda con la que te acabas de emparejar (camino B de
|
|
457
|
+
* `dotrino-vault/docs/vinculacion-de-cuentas.md`). NO es adoptar una versión nueva de TU
|
|
458
|
+
* acta: es que ESTA llave pase a ser de OTRA cuenta, y por lo tanto **deja de tener la
|
|
459
|
+
* suya**.
|
|
460
|
+
*
|
|
461
|
+
* Por eso solo procede sobre un perfil que **nació para esto** (`createProfile({ forVault:
|
|
462
|
+
* true })`). Sin la marca no se une: se devuelve el conflicto para que la consola ofrezca
|
|
463
|
+
* crear una cuenta nueva, y **no se escribe nada**.
|
|
446
464
|
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
465
|
+
* Antes bastaba con ser el único miembro del acta propia, y entonces se sobrescribía el
|
|
466
|
+
* acta sin preguntar: una cuenta con su contenido pasaba a colgar de otra en silencio —
|
|
467
|
+
* justo la fusión de cuentas que el modelo prohíbe (§4). Eso se acabó.
|
|
450
468
|
*/
|
|
451
469
|
async function joinProfile (candidate) {
|
|
452
470
|
const v = await Acta.verifyActa({ acta: candidate })
|
|
@@ -455,10 +473,29 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
455
473
|
return { joined: false, reason: 'no-soy-miembro' }
|
|
456
474
|
}
|
|
457
475
|
const current = loadActa()
|
|
458
|
-
|
|
459
|
-
|
|
476
|
+
|
|
477
|
+
// Misma cuenta: no es unirse, es ponerse al día. Va por las reglas de adopción (§2.4.1).
|
|
478
|
+
if (current && current.profileId === candidate.profileId) {
|
|
479
|
+
const r = await adoptActa(candidate)
|
|
480
|
+
return { joined: r.adopted, reason: r.reason, profileId: candidate.profileId, seq: r.seq }
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (current && !isPendingJoin()) {
|
|
484
|
+
return {
|
|
485
|
+
joined: false,
|
|
486
|
+
reason: 'perfil-con-datos',
|
|
487
|
+
profileId: current.profileId,
|
|
488
|
+
seq: current.seq,
|
|
489
|
+
members: current.members.length
|
|
490
|
+
}
|
|
460
491
|
}
|
|
492
|
+
|
|
461
493
|
saveActa(candidate)
|
|
494
|
+
// El historial era de la cuenta anterior y no encadena con esta. La que se abandona es
|
|
495
|
+
// siempre una génesis recién nacida (lo garantiza la marca), así que no hay nada que
|
|
496
|
+
// retener; guardarla aquí solo mezclaría dos cuentas en la misma ventana (§1.3).
|
|
497
|
+
kv.setItem(ACTA_HISTORY_STORAGE, '[]')
|
|
498
|
+
clearPendingJoin()
|
|
462
499
|
emitVault({ phase: 'acta', seq: candidate.seq, sealer: candidate.sealer, joined: true })
|
|
463
500
|
return { joined: true, profileId: candidate.profileId, seq: candidate.seq }
|
|
464
501
|
}
|
|
@@ -1005,14 +1042,36 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1005
1042
|
// ----- perfiles (multi-perfil por dispositivo) -----
|
|
1006
1043
|
// Cambiar/crear setea el perfil activo; la app RECARGA la página y re-inicializa con él
|
|
1007
1044
|
// (no reactivo, por diseño). Las apps abiertas conservan el perfil con el que cargaron.
|
|
1045
|
+
/**
|
|
1046
|
+
* Los perfiles de este dispositivo, para el conmutador. Incluye el AVATAR de cada uno:
|
|
1047
|
+
* sin él, la lista caía siempre al identicon automático y tu foto no aparecía —aunque la
|
|
1048
|
+
* hubieras subido— porque el avatar vive en el `me` de cada perfil, no en el registro.
|
|
1049
|
+
*/
|
|
1008
1050
|
async listProfiles () {
|
|
1009
|
-
return loadProfiles().map((p) =>
|
|
1051
|
+
return loadProfiles().map((p) => {
|
|
1052
|
+
let avatar = null
|
|
1053
|
+
try {
|
|
1054
|
+
const raw = p.id === currentPid ? JSON.stringify(me || null) : rawKv.getItem(`dotrino.identity.p.${p.id}.me`)
|
|
1055
|
+
const m = raw ? JSON.parse(raw) : null
|
|
1056
|
+
if (m && typeof m.avatar === 'string') avatar = m.avatar
|
|
1057
|
+
} catch (_) {}
|
|
1058
|
+
// `pendingJoin`: nació para adoptar la cuenta de una bóveda y todavía no se unió.
|
|
1059
|
+
// La consola lo usa para no ofrecerlo como una cuenta normal a medio hacer.
|
|
1060
|
+
return { id: p.id, name: p.name || '', pubkey: p.pubkey || null, avatar, current: p.id === currentPid, pendingJoin: !!p.pendingJoin }
|
|
1061
|
+
})
|
|
1010
1062
|
},
|
|
1011
1063
|
async currentProfile () {
|
|
1012
1064
|
const e = loadProfiles().find((p) => p.id === currentPid) || {}
|
|
1013
1065
|
return { id: currentPid, name: e.name || me?.nickname || '', pubkey: publickeyJwkStr }
|
|
1014
1066
|
},
|
|
1015
|
-
|
|
1067
|
+
/**
|
|
1068
|
+
* Crea una cuenta más en este dispositivo, con su propia llave.
|
|
1069
|
+
*
|
|
1070
|
+
* `forVault: true` la marca como **nacida para adoptar** la cuenta de una bóveda
|
|
1071
|
+
* (camino B): es el único permiso que acepta `joinProfile`, y se consume al unirse.
|
|
1072
|
+
* Sin la marca, la cuenta es de este dispositivo y nadie se la puede llevar.
|
|
1073
|
+
*/
|
|
1074
|
+
async createProfile ({ name, forVault = false } = {}) {
|
|
1016
1075
|
const pid = 'p' + crypto.randomUUID().slice(0, 8)
|
|
1017
1076
|
currentPid = pid
|
|
1018
1077
|
rawKv.setItem(CURRENT_STORAGE, pid)
|
|
@@ -1022,9 +1081,11 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1022
1081
|
encKeypair = await loadOrCreateEncKeypair(); encPublickeyJwkStr = JSON.stringify(encKeypair.publicJwk)
|
|
1023
1082
|
me = { publickey: publickeyJwkStr, encryptionPubkey: encPublickeyJwkStr, nickname: String(name || '').slice(0, 40) }
|
|
1024
1083
|
saveMe(me)
|
|
1025
|
-
const list = loadProfiles()
|
|
1084
|
+
const list = loadProfiles()
|
|
1085
|
+
list.push({ id: pid, name: me.nickname, pubkey: publickeyJwkStr, ...(forVault ? { pendingJoin: true } : {}) })
|
|
1086
|
+
saveProfiles(list)
|
|
1026
1087
|
await ensureActa(me.nickname) // el perfil nuevo nace con su acta (él mismo es el master)
|
|
1027
|
-
return { id: pid, name: me.nickname, pubkey: publickeyJwkStr }
|
|
1088
|
+
return { id: pid, name: me.nickname, pubkey: publickeyJwkStr, pendingJoin: !!forVault }
|
|
1028
1089
|
},
|
|
1029
1090
|
async switchProfile ({ id } = {}) {
|
|
1030
1091
|
if (!loadProfiles().find((p) => p.id === id)) throw new Error('perfil no existe')
|
|
@@ -1246,7 +1307,27 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1246
1307
|
// ----- emparejar ESTE dispositivo con el vault del usuario (Fase 1) -----
|
|
1247
1308
|
// Genera D aquí dentro (su privada NUNCA sale de la identidad), hace el enroll
|
|
1248
1309
|
// endurecido por el proxy y guarda el cert. NO cambia signData todavía (Fase 2).
|
|
1249
|
-
|
|
1310
|
+
/**
|
|
1311
|
+
* Empareja este dispositivo con una bóveda.
|
|
1312
|
+
*
|
|
1313
|
+
* `join` dice **de qué cuenta estamos hablando** (V7: la intención es explícita, nunca
|
|
1314
|
+
* se adivina):
|
|
1315
|
+
* · `'new'` → camino B: crea aquí una cuenta más, con llave nueva, y ES ESA la que
|
|
1316
|
+
* entra al acta de la bóveda. La que estabas usando **no se toca**.
|
|
1317
|
+
* · `'current'` → sigue con la cuenta abierta. Solo vale si nació para adoptar
|
|
1318
|
+
* (`forVault`) o si ya está emparejada con ESA misma bóveda
|
|
1319
|
+
* (re-emparejar). En cualquier otro caso falla **antes de tocar la
|
|
1320
|
+
* red**, en vez de traerse un acta ajena y pisar la tuya.
|
|
1321
|
+
*/
|
|
1322
|
+
async vaultPair ({ qr, label = '', join = 'current' }) {
|
|
1323
|
+
if (join === 'new') {
|
|
1324
|
+
await handlers.createProfile({ name: label || me?.nickname || '', forVault: true })
|
|
1325
|
+
} else {
|
|
1326
|
+
const yaConEsta = loadVaultCert()?.master === qr?.iss
|
|
1327
|
+
if (loadActa() && !isPendingJoin() && !yaConEsta) {
|
|
1328
|
+
throw new Error('este aparato ya está usando una cuenta: para usar también la de tu bóveda, crea una cuenta nueva aquí (la que tienes abierta no se toca)')
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1250
1331
|
// Usa la PROPIA llave de identidad de este navegador como dispositivo: el cert delega
|
|
1251
1332
|
// TU identidad (P) desde la maestra M → una sola identidad (signData/identify/cert = P).
|
|
1252
1333
|
// La privada es la CryptoKey del perfil (no extractable): se pasa como `privateKey`
|
|
@@ -1254,18 +1335,20 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1254
1335
|
const device = { publickey: publickeyJwkStr, privateKey: keypair.privateKey }
|
|
1255
1336
|
// Si esta identidad ya existía por su cuenta, se lleva un certificado de continuidad
|
|
1256
1337
|
// firmado por ella misma: es el puente para que su reputación previa siga contando.
|
|
1338
|
+
// Solo si esta llave tenía vida propia. Una recién creada para adoptar (camino B) no
|
|
1339
|
+
// tiene pasado que salvar: mandarle un puente de continuidad sería puro ruido.
|
|
1257
1340
|
const mio = loadActa()
|
|
1258
|
-
const continuity = (mio && mio.members.length === 1)
|
|
1341
|
+
const continuity = (mio && mio.members.length === 1 && !isPendingJoin())
|
|
1259
1342
|
? await Acta.makeContinuity({ member: publickeyJwkStr, from: mio.profileId, privateKey: keypair.privateKey })
|
|
1260
1343
|
: null
|
|
1261
1344
|
const res = await remoteEnroll({ qr, device, continuity, encPub: encPublickeyJwkStr, label: label || me?.nickname || '', onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, code: c.code }) })
|
|
1262
1345
|
kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify({ useIdentityKey: true, publickey: publickeyJwkStr }))
|
|
1263
1346
|
kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
|
|
1264
|
-
// Conectarse a una bóveda es ENTRAR A SU
|
|
1265
|
-
const
|
|
1266
|
-
emitVault({ phase: 'paired', deviceId: res.deviceId, master: res.master, join })
|
|
1347
|
+
// Conectarse a una bóveda es ENTRAR A SU CUENTA: el acta viene con el cert.
|
|
1348
|
+
const unido = res.acta ? await joinProfile(res.acta) : { joined: false, reason: 'sin-acta' }
|
|
1349
|
+
emitVault({ phase: 'paired', deviceId: res.deviceId, master: res.master, join: unido })
|
|
1267
1350
|
pullProfileFromVault() // adoptar el perfil que ya viva en el vault (si hay)
|
|
1268
|
-
return { ok: true, deviceId: res.deviceId, master: res.master, exp: res.cert.exp, scope: res.cert.scope, join }
|
|
1351
|
+
return { ok: true, deviceId: res.deviceId, master: res.master, exp: res.cert.exp, scope: res.cert.scope, join: unido }
|
|
1269
1352
|
},
|
|
1270
1353
|
|
|
1271
1354
|
async vaultStatus () {
|
package/vault/keyid.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keyid.js — el identificador LEGIBLE de una llave.
|
|
3
|
+
*
|
|
4
|
+
* Una pubkey es un JWK: `{"crv":"P-256","kty":"EC","x":"…","y":"…"}`. Eso no se le enseña a
|
|
5
|
+
* nadie — ni entero ni recortado, que es peor: parece un error. Para que una persona pueda
|
|
6
|
+
* reconocer y comparar una llave se usa su huella corta, `AB12-CD34`, que es la misma que
|
|
7
|
+
* ya se muestra al emparejar («aprueba el dispositivo AB12-CD34») y en el acta del perfil.
|
|
8
|
+
*
|
|
9
|
+
* Módulo aparte y sin dependencias a propósito: cualquier interfaz que necesite mostrar una
|
|
10
|
+
* llave puede importarlo sin arrastrar el resto de la identidad. `capabilities.js` reexporta
|
|
11
|
+
* `pubkeyId` desde aquí para que haya UNA sola implementación.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const enc = (s) => new TextEncoder().encode(s)
|
|
15
|
+
|
|
16
|
+
/** Serialización canónica de los campos que identifican la llave (mismo orden siempre). */
|
|
17
|
+
function canonicalJwk (jwk) {
|
|
18
|
+
return JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y })
|
|
19
|
+
.replace(/^{/, '{').replace(/}$/, '}')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** id corto y estable de un pubkey (sha-256 hex de los campos canónicos del JWK). */
|
|
23
|
+
export async function pubkeyId (publicJwkStr) {
|
|
24
|
+
const jwk = typeof publicJwkStr === 'string' ? JSON.parse(publicJwkStr) : publicJwkStr
|
|
25
|
+
const h = await crypto.subtle.digest('SHA-256', enc(canonicalJwk(jwk)))
|
|
26
|
+
return [...new Uint8Array(h)].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* La huella que SÍ se le enseña a una persona: `AB12-CD34`. Corta, comparable de un vistazo
|
|
31
|
+
* y la misma en todo el ecosistema (emparejamiento, acta, lista de dispositivos).
|
|
32
|
+
*/
|
|
33
|
+
export async function keyLabel (publicJwkStr) {
|
|
34
|
+
if (!publicJwkStr) return ''
|
|
35
|
+
try {
|
|
36
|
+
const id = (await pubkeyId(publicJwkStr)).slice(0, 8).toUpperCase()
|
|
37
|
+
return id.slice(0, 4) + '-' + id.slice(4, 8)
|
|
38
|
+
} catch (_) { return '' }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default { pubkeyId, keyLabel }
|