@dotrino/identity 0.31.0 → 0.33.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 +4 -1
- package/src/index.js +2 -2
- package/src/node.js +1 -1
- package/vault/capabilities.js +4 -6
- package/vault/core.js +16 -3
- 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.33.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": [
|
package/src/index.js
CHANGED
|
@@ -323,8 +323,8 @@ export class Identity {
|
|
|
323
323
|
* código a comparar; resuelve cuando el dueño aprueba en su PC (espera hasta 3 min).
|
|
324
324
|
* @returns {Promise<{ ok:boolean, deviceId:string, master:string, exp:number, scope:string[] }>}
|
|
325
325
|
*/
|
|
326
|
-
async enrollDevice (qr) {
|
|
327
|
-
return this._call('vaultPair', { qr }, 200000)
|
|
326
|
+
async enrollDevice (qr, { label = '' } = {}) {
|
|
327
|
+
return this._call('vaultPair', { qr, label }, 200000)
|
|
328
328
|
}
|
|
329
329
|
|
|
330
330
|
/** Estado de emparejamiento: { paired, deviceId?, master?, scope?, exp?, pairedAt? }. */
|
package/src/node.js
CHANGED
|
@@ -187,7 +187,7 @@ export class Identity {
|
|
|
187
187
|
/** Rota la clave de contenido (corta el acceso al contenido FUTURO de quien ya no está). */
|
|
188
188
|
rotateContentKey () { return this._h('rotateContentKey') }
|
|
189
189
|
// Emparejar ESTE dispositivo con el vault del usuario (Fase 1)
|
|
190
|
-
enrollDevice (qr) { return this._h('vaultPair', { qr }) }
|
|
190
|
+
enrollDevice (qr, { label = '' } = {}) { return this._h('vaultPair', { qr, label }) }
|
|
191
191
|
vaultStatus () { return this._h('vaultStatus') }
|
|
192
192
|
unpairDevice () { return this._h('vaultUnpair') }
|
|
193
193
|
vaultSign (payload) { return this._h('vaultSign', { payload }) }
|
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
|
@@ -1005,8 +1005,21 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1005
1005
|
// ----- perfiles (multi-perfil por dispositivo) -----
|
|
1006
1006
|
// Cambiar/crear setea el perfil activo; la app RECARGA la página y re-inicializa con él
|
|
1007
1007
|
// (no reactivo, por diseño). Las apps abiertas conservan el perfil con el que cargaron.
|
|
1008
|
+
/**
|
|
1009
|
+
* Los perfiles de este dispositivo, para el conmutador. Incluye el AVATAR de cada uno:
|
|
1010
|
+
* sin él, la lista caía siempre al identicon automático y tu foto no aparecía —aunque la
|
|
1011
|
+
* hubieras subido— porque el avatar vive en el `me` de cada perfil, no en el registro.
|
|
1012
|
+
*/
|
|
1008
1013
|
async listProfiles () {
|
|
1009
|
-
return loadProfiles().map((p) =>
|
|
1014
|
+
return loadProfiles().map((p) => {
|
|
1015
|
+
let avatar = null
|
|
1016
|
+
try {
|
|
1017
|
+
const raw = p.id === currentPid ? JSON.stringify(me || null) : rawKv.getItem(`dotrino.identity.p.${p.id}.me`)
|
|
1018
|
+
const m = raw ? JSON.parse(raw) : null
|
|
1019
|
+
if (m && typeof m.avatar === 'string') avatar = m.avatar
|
|
1020
|
+
} catch (_) {}
|
|
1021
|
+
return { id: p.id, name: p.name || '', pubkey: p.pubkey || null, avatar, current: p.id === currentPid }
|
|
1022
|
+
})
|
|
1010
1023
|
},
|
|
1011
1024
|
async currentProfile () {
|
|
1012
1025
|
const e = loadProfiles().find((p) => p.id === currentPid) || {}
|
|
@@ -1246,7 +1259,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1246
1259
|
// ----- emparejar ESTE dispositivo con el vault del usuario (Fase 1) -----
|
|
1247
1260
|
// Genera D aquí dentro (su privada NUNCA sale de la identidad), hace el enroll
|
|
1248
1261
|
// endurecido por el proxy y guarda el cert. NO cambia signData todavía (Fase 2).
|
|
1249
|
-
async vaultPair ({ qr }) {
|
|
1262
|
+
async vaultPair ({ qr, label = '' }) {
|
|
1250
1263
|
// Usa la PROPIA llave de identidad de este navegador como dispositivo: el cert delega
|
|
1251
1264
|
// TU identidad (P) desde la maestra M → una sola identidad (signData/identify/cert = P).
|
|
1252
1265
|
// La privada es la CryptoKey del perfil (no extractable): se pasa como `privateKey`
|
|
@@ -1258,7 +1271,7 @@ export async function createIdentityCore ({ kv: rawKv, peers, makeSync = null, k
|
|
|
1258
1271
|
const continuity = (mio && mio.members.length === 1)
|
|
1259
1272
|
? await Acta.makeContinuity({ member: publickeyJwkStr, from: mio.profileId, privateKey: keypair.privateKey })
|
|
1260
1273
|
: null
|
|
1261
|
-
const res = await remoteEnroll({ qr, device, continuity, encPub: encPublickeyJwkStr, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, code: c.code }) })
|
|
1274
|
+
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
1275
|
kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify({ useIdentityKey: true, publickey: publickeyJwkStr }))
|
|
1263
1276
|
kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
|
|
1264
1277
|
// Conectarse a una bóveda es ENTRAR A SU PERFIL: el acta viene con el cert.
|
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 }
|