@dotrino/identity 0.12.0 → 0.14.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.12.0",
3
+ "version": "0.14.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
@@ -426,4 +426,4 @@ export class Identity {
426
426
 
427
427
  // Helpers de capacidad SIN clave maestra (lado dispositivo + verificación), reutilizables
428
428
  // por apps/bridges sin cargar el iframe del vault.
429
- 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
@@ -186,4 +186,4 @@ export default Identity
186
186
 
187
187
  // Helpers de capacidad SIN clave maestra (lado dispositivo + verificación), para que
188
188
  // un bridge/bot Node pueda crear su clave, firmar acciones y verificar cadenas D←P.
189
- 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
@@ -519,7 +519,7 @@ export async function createIdentityCore ({ kv, peers, makeSync = null }) {
519
519
  // TU identidad (P) desde la maestra M → una sola identidad (signData/identify/cert = P).
520
520
  let device
521
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, sas: c.sas }) })
522
+ const res = await remoteEnroll({ qr, device, onChallenge: (c) => emitVault({ phase: 'challenge', deviceId: c.deviceId, code: c.code }) })
523
523
  kv.setItem(VAULT_DEVICE_STORAGE, JSON.stringify(res.device))
524
524
  kv.setItem(VAULT_CERT_STORAGE, JSON.stringify({ cert: res.cert, master: res.master, proxy: res.proxy, deviceId: res.deviceId, pairedAt: Date.now() }))
525
525
  emitVault({ phase: 'paired', deviceId: res.deviceId, master: res.master })
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, pubkeyId } from './capabilities.js'
16
16
 
17
17
  const MSG = {
18
18
  ENROLL: 'vault.enroll',
@@ -24,7 +24,7 @@ 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}>}
@@ -40,15 +40,21 @@ export async function enrollDevice ({ qr, device, onChallenge, label = '', appro
40
40
  // identify/cert son la misma P).
41
41
  const dev = device || await makeDeviceKey({ label })
42
42
  const deviceId = (await pubkeyId(dev.publickey)).slice(0, 8).toUpperCase().replace(/(.{4})(.{4})/, '$1-$2')
43
- const sas = await deriveSAS(qr.iss, dev.publickey, qr.sn)
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
+ // NO se manda el código ni un compromiso: el vault lo aprende SOLO cuando lo tipeás, y al
47
+ // ECHARLO de vuelta el dispositivo confía. Un vault falso no conoce el código → no empareja.
44
48
  const data = { op: 'enroll', dpub: dev.publickey, token: qr.token, sn: qr.sn, label, ts: Date.now() }
45
49
  const { signature } = await signWithDevice({ privateJwk: dev.privateJwk, data })
46
50
 
47
51
  const enrolled = new Promise((resolve, reject) => {
48
52
  const off = client.on('message', (_from, p) => {
49
53
  if (!p || typeof p !== 'object') return
50
- if (p.type === MSG.ENROLL_CHALLENGE) { try { onChallenge?.({ deviceId, sas }) } catch (_) {} }
51
- else if (p.type === MSG.ENROLLED) { cleanup(); resolve(p) }
54
+ if (p.type === MSG.ENROLL_CHALLENGE) { try { onChallenge?.({ deviceId, code }) } catch (_) {} }
55
+ // El vault ECHA el código que tipeaste; aceptamos SOLO si coincide con el que generamos.
56
+ // (Un código distinto = un vault que no lo conoce → lo ignoramos y seguimos esperando.)
57
+ else if (p.type === MSG.ENROLLED) { if (p.code === code) { cleanup(); resolve(p) } }
52
58
  else if (p.type === MSG.ERROR) { cleanup(); reject(new Error(p.error)) }
53
59
  })
54
60
  const t = setTimeout(() => { cleanup(); reject(new Error('timeout esperando la aprobación en el vault')) }, approveTimeoutMs)