@dotrino/identity 0.35.0 → 0.37.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 +2 -2
  2. package/vault/remote.js +52 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/identity",
3
- "version": "0.35.0",
3
+ "version": "0.37.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",
@@ -55,7 +55,7 @@
55
55
  "url": "git+https://github.com/imdotrino/dotrino-identity.git"
56
56
  },
57
57
  "dependencies": {
58
- "@dotrino/proxy-client": "0.8.0"
58
+ "@dotrino/proxy-client": "0.9.1"
59
59
  },
60
60
  "devDependencies": {
61
61
  "fake-indexeddb": "^6.2.5"
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,48 @@ 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
+
79
+ /**
80
+ * Canjea la CITA del QR: devuelve la dirección real de la conexión de la bóveda. El
81
+ * código se quema al usarse, así que esto va una sola vez por emparejamiento.
82
+ */
83
+ async function canjearCita (client, code) {
84
+ const r = await client.redeemPairingCode(code)
85
+ if (!r?.ok || !r.instance) throw new Error(r?.error || 'ese código de emparejamiento ya no vale')
86
+ return r.instance
87
+ }
88
+
89
+ /** «¿Quién eres?» del QR corto: devuelve `{ iss, proxy, acct, m }` de la bóveda. */
90
+ async function askVault (client, qr) {
91
+ const destino = await canjearCita(client, qr.conn)
92
+ return new Promise((resolve, reject) => {
93
+ const off = client.on('message', (_from, p) => {
94
+ 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) }
95
+ else if (p?.type === MSG.ERROR) { fin(); reject(new Error(p.error)) }
96
+ })
97
+ const t = setTimeout(() => { fin(); reject(new Error('la bóveda no contestó: ese código pudo caducar')) }, 15000)
98
+ const fin = () => { off(); clearTimeout(t) }
99
+ try { client.send(destino, { type: MSG.HELLO, sn: qr.sn }) } catch (e) { fin(); reject(e) }
100
+ })
101
+ }
102
+
59
103
  /**
60
104
  * @param {Object} opts
61
105
  * @param {{v:number, iss:string, proxy:string, token:string, sn:string}} opts.qr QR v2 del vault.
@@ -65,11 +109,16 @@ async function identifyAsDevice (client, device, { cert = null, acta = null } =
65
109
  * @returns {Promise<{device, cert, master:string, proxy:string, deviceId:string}>}
66
110
  */
67
111
  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')
112
+ if (!qr?.sn || !(qr.iss || qr.conn)) throw new Error('qr inválido: falta la bóveda o el nonce')
69
113
  const { WebSocketProxyClient } = await import('@dotrino/proxy-client')
70
- const client = new WebSocketProxyClient({ url: qr.proxy, enableWebRTC: false, autoReconnect: false })
114
+ const client = new WebSocketProxyClient({ url: qr.proxy || 'wss://proxy.dotrino.com', enableWebRTC: false, autoReconnect: false })
71
115
  await client.connect()
72
116
  try {
117
+ // QR CORTO: no trae la llave, solo la dirección de la bóveda en el proxy. Se le
118
+ // pregunta quién es, punto a punto, presentando el `sn`; solo contesta si esa sesión
119
+ // de emparejamiento sigue abierta. Lo que acredita la llave no es el QR: es la firma
120
+ // del certificado y el código de 6 dígitos, que solo aprende la bóveda donde lo tecleas.
121
+ if (!qr.iss) qr = { ...qr, ...(await askVault(client, qr)) }
73
122
  // Por defecto genera una sub-clave nueva; pero el iframe pasa SU PROPIA llave de
74
123
  // identidad (P) como `device` → el cert delega tu identidad y hay UNA sola (signData/
75
124
  // identify/cert son la misma P).
@@ -94,7 +143,7 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', conti
94
143
  const adoptar = intent === 'adopt'
95
144
  if (adoptar && typeof onAdopt !== 'function') throw new Error('enrollDevice(adopt): falta onAdopt')
96
145
  const data = {
97
- op: 'enroll', dpub: dev.publickey, token: qr.token, sn: qr.sn, commit, label, ts: Date.now(), intent,
146
+ op: 'enroll', dpub: dev.publickey, token: qr.token || qr.sn, sn: qr.sn, commit, label, ts: Date.now(), intent,
98
147
  ...(adoptar && profileId ? { profileId } : {}),
99
148
  ...(continuity ? { continuity } : {}), ...(encPub ? { encPub } : {})
100
149
  }