@dotrino/identity 0.11.0 → 0.13.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
package/src/index.js CHANGED
@@ -245,6 +245,15 @@ export class Identity {
245
245
  return this._call('listVaultDevices', {}, 20000)
246
246
  }
247
247
 
248
+ /**
249
+ * El cert de delegación de este dispositivo (o null si no está emparejado). El
250
+ * transporte lo presenta al proxy en `identify` → "una identidad": el proxy enruta
251
+ * los mensajes dirigidos a tu maestra M también a este dispositivo. No tiene secretos.
252
+ */
253
+ async getVaultCert () {
254
+ return this._call('getVaultCert')
255
+ }
256
+
248
257
  /** Suscribe a eventos de emparejamiento ('vault'): { phase:'challenge'|'paired'|'unpaired', ... }. */
249
258
  onVault (handler) {
250
259
  return this.on('vault', handler)
@@ -417,4 +426,4 @@ export class Identity {
417
426
 
418
427
  // Helpers de capacidad SIN clave maestra (lado dispositivo + verificación), reutilizables
419
428
  // por apps/bridges sin cargar el iframe del vault.
420
- export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
429
+ export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
package/src/node.js CHANGED
@@ -142,6 +142,7 @@ export class Identity {
142
142
  vaultSign (payload) { return this._h('vaultSign', { payload }) }
143
143
  vaultStore (method, args) { return this._h('vaultStore', { method, args }) }
144
144
  listVaultDevices () { return this._h('listVaultDevices') }
145
+ getVaultCert () { return this._h('getVaultCert') }
145
146
  onVault (handler) { return this.on('vault', handler) }
146
147
  mergeEndorsements (subject, endorsements, askerPubkey) {
147
148
  return this._h('mergeEndorsements', { subject, endorsements, askerPubkey })
@@ -185,4 +186,4 @@ export default Identity
185
186
 
186
187
  // Helpers de capacidad SIN clave maestra (lado dispositivo + verificación), para que
187
188
  // un bridge/bot Node pueda crear su clave, firmar acciones y verificar cadenas D←P.
188
- export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
189
+ export { makeDeviceKey, signWithDevice, verifyDelegation, verifyChain, pubkeyId, deriveSAS, verifyDeviceSig, makePairingCode, commitCode, MAX_DELEGATION_MS, DEFAULT_DELEGATION_MS } from '../vault/capabilities.js'
@@ -77,6 +77,28 @@ export async function deriveSAS (master, dpub, sn) {
77
77
  return String(n % 1000000).padStart(6, '0')
78
78
  }
79
79
 
80
+ /**
81
+ * Código de emparejamiento ALEATORIO de 6 dígitos. Lo genera el DISPOSITIVO y lo MUESTRA;
82
+ * el usuario lo tipea en el vault. El vault NO lo conoce: el dispositivo solo manda un
83
+ * COMPROMISO (`commitCode`), no el código → el vault lo aprende únicamente cuando vos se lo
84
+ * das, tipeándolo. Así, aprobar exige TENER el dispositivo (de ahí sale el código).
85
+ */
86
+ export function makePairingCode () {
87
+ const b = crypto.getRandomValues(new Uint8Array(4))
88
+ const n = (((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]) >>> 0)
89
+ return String(n % 1000000).padStart(6, '0')
90
+ }
91
+
92
+ /**
93
+ * Compromiso del código: `SHA-256(code ‖ dpub ‖ sn)` en hex. Va en el ENROLL (no el código).
94
+ * Liga el código a ESTE dispositivo y sesión (no reusable para otro). El vault lo guarda y,
95
+ * cuando tipeás el código, recomputa y compara → verifica posesión sin conocer el código antes.
96
+ */
97
+ export async function commitCode ({ code, dpub, sn }) {
98
+ const h = await crypto.subtle.digest('SHA-256', enc(canonicalStringify({ code: String(code), sub: dpub, sn })))
99
+ return [...new Uint8Array(h)].map((x) => x.toString(16).padStart(2, '0')).join('')
100
+ }
101
+
80
102
  /** Cuerpo canónico del certificado (lo que se firma): el cert SIN la firma. */
81
103
  export function delegationBody (cert) {
82
104
  return { v: cert.v, iss: cert.iss, sub: cert.sub, scope: cert.scope, iat: cert.iat, exp: cert.exp, nonce: cert.nonce }
package/vault/core.js CHANGED
@@ -515,7 +515,11 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
515
515
  // Genera D aquí dentro (su privada NUNCA sale de la identidad), hace el enroll
516
516
  // endurecido por el proxy y guarda el cert. NO cambia signData todavía (Fase 2).
517
517
  async vaultPair ({ qr }) {
518
- const res = await remoteEnroll({ qr, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, sas: c.sas }) })
518
+ // Usa la PROPIA llave de identidad de este navegador como dispositivo: el cert delega
519
+ // TU identidad (P) desde la maestra M → una sola identidad (signData/identify/cert = P).
520
+ let device
521
+ try { const k = JSON.parse(kv.getItem(KEY_STORAGE)); device = { publickey: JSON.stringify(k.publicJwk), privateJwk: k.privateJwk } } catch (_) { device = undefined }
522
+ const res = await remoteEnroll({ qr, device, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, code: c.code }) })
519
523
  kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify(res.device))
520
524
  kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
521
525
  emitVault({ phase: 'paired', deviceId: res.deviceId, master: res.master })
@@ -559,6 +563,13 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
559
563
  return remoteDevices({ master: v.master, proxy: v.proxy, device, cert: v.cert })
560
564
  },
561
565
 
566
+ // El cert de delegación de este dispositivo (para presentarlo al proxy en `identify`
567
+ // → "una identidad": el proxy bindea tu pubkey también bajo tu maestra M). Sin secretos.
568
+ async getVaultCert () {
569
+ const v = loadVaultCert()
570
+ return v?.cert ? { cert: v.cert, master: v.master } : null
571
+ },
572
+
562
573
  async listContacts () {
563
574
  return Object.values(loadPeers()).filter(p => p && p.isContact).sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0))
564
575
  },
package/vault/remote.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * No reimplementa cripto: usa `@dotrino/identity/capabilities`. Transporte:
13
13
  * `@dotrino/proxy-client` (importado perezosamente; solo se carga al emparejar).
14
14
  */
15
- import { makeDeviceKey, signWithDevice, verifyDelegation, deriveSAS, pubkeyId } from './capabilities.js'
15
+ import { makeDeviceKey, signWithDevice, verifyDelegation, makePairingCode, commitCode, pubkeyId } from './capabilities.js'
16
16
 
17
17
  const MSG = {
18
18
  ENROLL: 'vault.enroll',
@@ -24,27 +24,33 @@ const MSG = {
24
24
  /**
25
25
  * @param {Object} opts
26
26
  * @param {{v:number, iss:string, proxy:string, token:string, sn:string}} opts.qr QR v2 del vault.
27
- * @param {(c:{deviceId:string, sas:string})=>void} [opts.onChallenge] Para mostrar el SAS a comparar.
27
+ * @param {(c:{deviceId:string, code:string})=>void} [opts.onChallenge] Para mostrar el código a tipear en el PC.
28
28
  * @param {string} [opts.label]
29
29
  * @param {number} [opts.approveTimeoutMs] Espera de la aprobación humana (def 3 min).
30
30
  * @returns {Promise<{device, cert, master:string, proxy:string, deviceId:string}>}
31
31
  */
32
- export async function enrollDevice ({ qr, onChallenge, label = '', approveTimeoutMs = 180000 } = {}) {
32
+ export async function enrollDevice ({ qr, device, onChallenge, label = '', approveTimeoutMs = 180000 } = {}) {
33
33
  if (!qr?.iss || !qr?.proxy || !qr?.token || !qr?.sn) throw new Error('qr inválido (v2): faltan iss/proxy/token/sn')
34
34
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
35
35
  const client = new WebSocketProxyClient({ url: qr.proxy, enableWebRTC: false, autoReconnect: false })
36
36
  await client.connect()
37
37
  try {
38
- const device = await makeDeviceKey({ label })
39
- const deviceId = (await pubkeyId(device.publickey)).slice(0, 8).toUpperCase().replace(/(.{4})(.{4})/, '$1-$2')
40
- const sas = await deriveSAS(qr.iss, device.publickey, qr.sn)
41
- const data = { op: 'enroll', dpub: device.publickey, token: qr.token, sn: qr.sn, label, ts: Date.now() }
42
- const { signature } = await signWithDevice({ privateJwk: device.privateJwk, data })
38
+ // Por defecto genera una sub-clave nueva; pero el iframe pasa SU PROPIA llave de
39
+ // identidad (P) como `device` → el cert delega tu identidad y hay UNA sola (signData/
40
+ // identify/cert son la misma P).
41
+ const dev = device || await makeDeviceKey({ label })
42
+ const deviceId = (await pubkeyId(dev.publickey)).slice(0, 8).toUpperCase().replace(/(.{4})(.{4})/, '$1-$2')
43
+ // El DISPOSITIVO genera el código y manda solo su COMPROMISO (no el código). El vault
44
+ // aprende el código únicamente cuando vos lo tipeás en el PC → aprobar exige tener el dispositivo.
45
+ const code = makePairingCode()
46
+ const commit = await commitCode({ code, dpub: dev.publickey, sn: qr.sn })
47
+ const data = { op: 'enroll', dpub: dev.publickey, token: qr.token, sn: qr.sn, commit, label, ts: Date.now() }
48
+ const { signature } = await signWithDevice({ privateJwk: dev.privateJwk, data })
43
49
 
44
50
  const enrolled = new Promise((resolve, reject) => {
45
51
  const off = client.on('message', (_from, p) => {
46
52
  if (!p || typeof p !== 'object') return
47
- if (p.type === MSG.ENROLL_CHALLENGE) { try { onChallenge?.({ deviceId, sas }) } catch (_) {} }
53
+ if (p.type === MSG.ENROLL_CHALLENGE) { try { onChallenge?.({ deviceId, code }) } catch (_) {} }
48
54
  else if (p.type === MSG.ENROLLED) { cleanup(); resolve(p) }
49
55
  else if (p.type === MSG.ERROR) { cleanup(); reject(new Error(p.error)) }
50
56
  })
@@ -55,11 +61,11 @@ export async function enrollDevice ({ qr, onChallenge, label = '', approveTimeou
55
61
  const res = await enrolled
56
62
 
57
63
  // Validación estricta antes de guardar (cierra inyección de cert / sustitución de maestra).
58
- const v = await verifyDelegation({ cert: res.cert, expectedSub: device.publickey })
64
+ const v = await verifyDelegation({ cert: res.cert, expectedSub: dev.publickey })
59
65
  if (!v.ok) throw new Error('cert inválido: ' + v.reason)
60
66
  if (res.cert.iss !== qr.iss) throw new Error('cert firmado por una maestra distinta a la que viste')
61
- if (res.cert.sub !== device.publickey) throw new Error('cert emitido para otro dispositivo')
62
- return { device, cert: res.cert, master: qr.iss, proxy: qr.proxy, deviceId }
67
+ if (res.cert.sub !== dev.publickey) throw new Error('cert emitido para otro dispositivo')
68
+ return { device: dev, cert: res.cert, master: qr.iss, proxy: qr.proxy, deviceId }
63
69
  } finally { try { client.close() } catch (_) {} }
64
70
  }
65
71
 
@@ -1,4 +1,2 @@
1
- Copia vendorizada de @dotrino/proxy-client@0.6.3 (src/, sin dependencias).
2
- El iframe id.dotrino.com se sirve estático (sin bundler), así que el transporte
3
- va self-hosted aquí (no por CDN) para no depender de terceros en runtime.
4
- Actualizar: re-copiar node_modules/@dotrino/proxy-client/src/*.js y bumpear esta nota.
1
+ Copia vendorizada de @dotrino/proxy-client@0.6.4 (src/, sin dependencias).
2
+ Ver dotrino-identity: el iframe se sirve estatico, transporte self-hosted (no CDN).
@@ -233,9 +233,11 @@ export class WebSocketProxyClient {
233
233
  * (típicamente por el identity vault). Devuelve la respuesta del proxy con
234
234
  * `queued_delivered` (mensajes offline despachados al instante).
235
235
  */
236
- identify ({ data, signature }) {
236
+ identify ({ data, signature, cert }) {
237
237
  if (!data || !signature) throw new Error('identify requires {data, signature}')
238
- return this._request({ type: 'identify', data, signature }, 'identified')
238
+ const msg = { type: 'identify', data, signature }
239
+ if (cert) msg.cert = cert // "una identidad": el proxy bindea este token también bajo tu maestra M
240
+ return this._request(msg, 'identified')
239
241
  }
240
242
 
241
243
  /**