@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 +1 -1
- package/src/index.js +10 -1
- package/src/node.js +2 -1
- package/vault/capabilities.js +22 -0
- package/vault/core.js +12 -1
- package/vault/remote.js +18 -12
- package/vault/vendor/proxy-client/VERSION.txt +2 -4
- package/vault/vendor/proxy-client/client.js +4 -2
package/package.json
CHANGED
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'
|
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
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
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,
|
|
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:
|
|
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 !==
|
|
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.
|
|
2
|
-
|
|
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
|
-
|
|
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
|
/**
|