@dotrino/identity 0.35.0 → 0.36.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/vault/remote.js +40 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.35.0",
3
+ "version": "0.36.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/vault/remote.js CHANGED
@@ -16,6 +16,8 @@
16
16
  import { makeDeviceKey, signWithDevice, verifyDelegation, verifyDeviceSig, makePairingCode, commitCode, pubkeyId } from './capabilities.js'
17
17
 
18
18
  const MSG = {
19
+ HELLO: 'vault.hello',
20
+ HELLO_OK: 'vault.hello.ok',
19
21
  ENROLL: 'vault.enroll',
20
22
  ENROLL_CHALLENGE: 'vault.enroll.challenge',
21
23
  ENROLLED: 'vault.enrolled',
@@ -56,6 +58,36 @@ async function identifyAsDevice (client, device, { cert = null, acta = null } =
56
58
  await client.identify({ data, signature, cert, acta })
57
59
  }
58
60
 
61
+
62
+ /**
63
+ * La respuesta al `hello` va firmada y con el `sn` DENTRO de lo firmado. Comprobarlo
64
+ * ata la respuesta a ESTA sesión: no vale la de otro emparejamiento ni la de otra
65
+ * bóveda. Ojo con lo que NO prueba: cualquiera puede firmar con una llave suya, así
66
+ * que esto no dice que sea TU bóveda — eso lo dice el código de 6 dígitos, que solo
67
+ * aprende la bóveda donde tú lo tecleas.
68
+ */
69
+ async function verificarHola (p, sn) {
70
+ const b = p?.body
71
+ if (!b?.iss || b.sn !== sn) throw new Error('la bóveda contestó a otro emparejamiento')
72
+ if (!(await verifyDeviceSig({ publickey: b.iss, data: b, signature: p.signature }))) {
73
+ throw new Error('la respuesta de la bóveda no está bien firmada')
74
+ }
75
+ return b
76
+ }
77
+
78
+ /** «¿Quién eres?» del QR corto: devuelve `{ iss, proxy, acct, m }` de la bóveda. */
79
+ async function askVault (client, qr) {
80
+ return new Promise((resolve, reject) => {
81
+ const off = client.on('message', (_from, p) => {
82
+ if (p?.type === MSG.HELLO_OK) { fin(); verificarHola(p, qr.sn).then((b) => resolve({ iss: b.iss, proxy: b.proxy || qr.proxy, acct: b.acct || '', m: b.m || qr.m }), reject) }
83
+ else if (p?.type === MSG.ERROR) { fin(); reject(new Error(p.error)) }
84
+ })
85
+ const t = setTimeout(() => { fin(); reject(new Error('la bóveda no contestó: ese código pudo caducar')) }, 15000)
86
+ const fin = () => { off(); clearTimeout(t) }
87
+ try { client.send(qr.conn, { type: MSG.HELLO, sn: qr.sn }) } catch (e) { fin(); reject(e) }
88
+ })
89
+ }
90
+
59
91
  /**
60
92
  * @param {Object} opts
61
93
  * @param {{v:number, iss:string, proxy:string, token:string, sn:string}} opts.qr QR v2 del vault.
@@ -65,11 +97,16 @@ async function identifyAsDevice (client, device, { cert = null, acta = null } =
65
97
  * @returns {Promise<{device, cert, master:string, proxy:string, deviceId:string}>}
66
98
  */
67
99
  export async function enrollDevice ({ qr, device, onChallenge, label = '', continuity = null, encPub = null, approveTimeoutMs = 180000, intent = 'join', profileId = null, onAdopt = null } = {}) {
68
- if (!qr?.iss || !qr?.proxy || !qr?.token || !qr?.sn) throw new Error('qr inválido (v2): faltan iss/proxy/token/sn')
100
+ if (!qr?.sn || !(qr.iss || qr.conn)) throw new Error('qr inválido: falta la bóveda o el nonce')
69
101
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
70
- const client = new WebSocketProxyClient({ url: qr.proxy, enableWebRTC: false, autoReconnect: false })
102
+ const client = new WebSocketProxyClient({ url: qr.proxy || 'wss://proxy.dotrino.com', enableWebRTC: false, autoReconnect: false })
71
103
  await client.connect()
72
104
  try {
105
+ // QR CORTO: no trae la llave, solo la dirección de la bóveda en el proxy. Se le
106
+ // pregunta quién es, punto a punto, presentando el `sn`; solo contesta si esa sesión
107
+ // de emparejamiento sigue abierta. Lo que acredita la llave no es el QR: es la firma
108
+ // del certificado y el código de 6 dígitos, que solo aprende la bóveda donde lo tecleas.
109
+ if (!qr.iss) qr = { ...qr, ...(await askVault(client, qr)) }
73
110
  // Por defecto genera una sub-clave nueva; pero el iframe pasa SU PROPIA llave de
74
111
  // identidad (P) como `device` → el cert delega tu identidad y hay UNA sola (signData/
75
112
  // identify/cert son la misma P).
@@ -94,7 +131,7 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', conti
94
131
  const adoptar = intent === 'adopt'
95
132
  if (adoptar && typeof onAdopt !== 'function') throw new Error('enrollDevice(adopt): falta onAdopt')
96
133
  const data = {
97
- op: 'enroll', dpub: dev.publickey, token: qr.token, sn: qr.sn, commit, label, ts: Date.now(), intent,
134
+ op: 'enroll', dpub: dev.publickey, token: qr.token || qr.sn, sn: qr.sn, commit, label, ts: Date.now(), intent,
98
135
  ...(adoptar && profileId ? { profileId } : {}),
99
136
  ...(continuity ? { continuity } : {}), ...(encPub ? { encPub } : {})
100
137
  }