@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 +1 -1
- package/src/index.js +1 -1
- package/src/node.js +1 -1
- package/vault/capabilities.js +22 -0
- package/vault/core.js +1 -1
- package/vault/remote.js +11 -5
package/package.json
CHANGED
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'
|
package/vault/capabilities.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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,
|
|
51
|
-
|
|
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)
|