@dotrino/identity 0.99.0 → 0.100.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
package/vault/vault.js
CHANGED
|
@@ -405,7 +405,7 @@ import { pubkeyId } from './capabilities.js'
|
|
|
405
405
|
* la bóveda guarda un paquete que no puede abrir. Es el mismo camino que `logins add`
|
|
406
406
|
* del binario, con la misma pieza compartida.
|
|
407
407
|
*/
|
|
408
|
-
selfVaultLoginAdd: async ({ user, password, label = '',
|
|
408
|
+
selfVaultLoginAdd: async ({ user, password, label = '', caps } = {}) => {
|
|
409
409
|
const d = pidaDaemon()
|
|
410
410
|
const { client: opaque } = await import('@dotrino/opaque')
|
|
411
411
|
const { makeDeviceKey, makeDeviceEncKey } = await import('@dotrino/identity/capabilities')
|
|
@@ -422,7 +422,8 @@ import { pubkeyId } from './capabilities.js'
|
|
|
422
422
|
const blob = await sealDeviceKeys(fin.exportKey, { sign: device.privateJwk, enc: enc.encPrivateJwk })
|
|
423
423
|
const r = await d.loginRegisterFinish({
|
|
424
424
|
user, upload: fin.upload, pub: device.publickey, encPub: enc.encPublickey,
|
|
425
|
-
|
|
425
|
+
// Los PERMISOS del acta, cualquiera de ellos: los traduce a scopes el pilar del vault.
|
|
426
|
+
label: nombre, blob, ...(caps ? { caps } : {})
|
|
426
427
|
})
|
|
427
428
|
return { ...r, address: loginAddress(user, await accountFingerprint(selfIdentity)) }
|
|
428
429
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.71.0 (dotrino-vault/lib/src/{index,enroll,protocol,passwordLogins,loginClient,b64}.js).
|
|
2
2
|
NO se edita a mano: la escribe `node vendor.mjs` y la vigila test/vendor-up-to-date.test.mjs.
|
|
3
3
|
index.js importa ./enroll.js y ./protocol.js (relativos, van en esta misma copia),
|
|
4
4
|
@dotrino/identity/{capabilities,acta} (= ../../{capabilities,acta}.js) y
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* Si algo de eso falla, se PARA con un `code` propio. No hay repliegue: entrar «a medias»
|
|
35
35
|
* en una cuenta es peor que no entrar.
|
|
36
36
|
*/
|
|
37
|
-
import { client as
|
|
37
|
+
import { client as opaquePorDefecto } from '@dotrino/opaque'
|
|
38
38
|
import { pubkeyId, verifyDeviceSig, signWithDevice } from '@dotrino/identity/capabilities'
|
|
39
39
|
import { checkVaultReply } from '@dotrino/identity/acta'
|
|
40
40
|
import { MSG } from './protocol.js'
|
|
@@ -82,11 +82,16 @@ const parseJson = (s) => { try { return JSON.parse(s) } catch (_) { return null
|
|
|
82
82
|
* @param {string} opts.password
|
|
83
83
|
* @param {string} [opts.label] de dónde se entra («el cyber de la esquina»): es lo que
|
|
84
84
|
* el dueño va a leer en su consola para decidir si cerrarlo.
|
|
85
|
+
* @param {object} [opts.opaque] el OPAQUE a usar. Por defecto el de casa, que va en este
|
|
86
|
+
* mismo hilo. Se inyecta porque en una EXTENSIÓN el WASM vive en una página sandbox, al
|
|
87
|
+
* otro lado de un `postMessage`, y entonces sus métodos devuelven promesas — igual que en
|
|
88
|
+
* `createLoginDesk`, y por el mismo motivo. Aquí se espera siempre: esperar a algo que no
|
|
89
|
+
* es una promesa no cuesta nada, y dos firmas serían dos clientes.
|
|
85
90
|
* @returns {Promise<{user:string,address:string,code:string,sid:string,cert:object,
|
|
86
91
|
* iss:string,acta:object,vaultToken:string,publickey:string,encPublickey:(string|null),
|
|
87
92
|
* keys:{sign:object,enc:(object|null)}}>}
|
|
88
93
|
*/
|
|
89
|
-
export async function loginWithPassword ({ transport, address, password, label = '', timeoutMs = REPLY_TIMEOUT_MS } = {}) {
|
|
94
|
+
export async function loginWithPassword ({ transport, address, password, label = '', timeoutMs = REPLY_TIMEOUT_MS, opaque = opaquePorDefecto } = {}) {
|
|
90
95
|
if (!transport || typeof transport.on !== 'function' || typeof transport.send !== 'function') {
|
|
91
96
|
throw err('no-transport', 'loginWithPassword: a connected transport is required')
|
|
92
97
|
}
|
|
@@ -104,7 +109,7 @@ export async function loginWithPassword ({ transport, address, password, label =
|
|
|
104
109
|
let ultimo = null
|
|
105
110
|
for (const token of tokens) {
|
|
106
111
|
try {
|
|
107
|
-
return await unIntento({ transport, token, user, code, dir, password, label, timeoutMs })
|
|
112
|
+
return await unIntento({ transport, token, user, code, dir, password, label, timeoutMs, opaque })
|
|
108
113
|
} catch (e) {
|
|
109
114
|
// Se para: son cosas del que entra, y probar con otra bóveda no las arregla.
|
|
110
115
|
if (e?.code === 'login-failed' || e?.code === 'too-many-tries' || e?.code === 'wrong-account' ||
|
|
@@ -115,15 +120,15 @@ export async function loginWithPassword ({ transport, address, password, label =
|
|
|
115
120
|
throw ultimo || err('no-vault', 'none of the vaults on that address could let you in')
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
async function unIntento ({ transport, token, user, code, dir, password, label, timeoutMs }) {
|
|
119
|
-
const start = opaque.loginStart({ password })
|
|
123
|
+
async function unIntento ({ transport, token, user, code, dir, password, label, timeoutMs, opaque }) {
|
|
124
|
+
const start = await opaque.loginStart({ password })
|
|
120
125
|
transport.send(token, { type: MSG.LOGIN_START, user, request: start.request })
|
|
121
126
|
const r1 = await waitFor(transport, token, (p) => p.type === MSG.LOGIN_RESPONSE || p.type === MSG.ERROR, timeoutMs)
|
|
122
127
|
if (r1.type === MSG.ERROR) throw deVuelta(r1)
|
|
123
128
|
|
|
124
129
|
let fin
|
|
125
130
|
try {
|
|
126
|
-
fin = opaque.loginFinish({ state: start.state, response: r1.response, password })
|
|
131
|
+
fin = await opaque.loginFinish({ state: start.state, response: r1.response, password })
|
|
127
132
|
} catch (_) {
|
|
128
133
|
// OPAQUE comprueba a las dos partes: aquí se sabe que el otro lado NO conoce el
|
|
129
134
|
// registro de este usuario. Contraseña equivocada, usuario que no existe o una bóveda
|
|
@@ -198,11 +203,26 @@ function deVuelta (p) {
|
|
|
198
203
|
* quedarse bloqueado esperándola. La sesión sigue abierta allí hasta que se cierre desde la
|
|
199
204
|
* consola, y eso ya está dicho en el diseño (§3.2).
|
|
200
205
|
*/
|
|
201
|
-
export async function closeLogin ({ transport, token, user, sid, publickey, privateJwk, privateKey, timeoutMs = REPLY_TIMEOUT_MS } = {}) {
|
|
206
|
+
export async function closeLogin ({ transport, token, user, sid, publickey, privateJwk, privateKey, sign, timeoutMs = REPLY_TIMEOUT_MS } = {}) {
|
|
202
207
|
if (!transport || typeof transport.send !== 'function') throw err('no-transport', 'closeLogin: transport required')
|
|
203
208
|
if (!token || !user || !sid || !publickey) throw err('bad-input', 'closeLogin: token, user, sid and publickey are required')
|
|
204
209
|
const data = { op: 'login.close', publickey, user, sid, ts: Date.now() }
|
|
205
|
-
|
|
210
|
+
// CON LA LLAVE O CON QUIEN LA TIENE. `sign(data)` es lo normal en el ecosistema —el
|
|
211
|
+
// transporte se identifica con `identity.signData` y no con una llave suelta— y aquí hace
|
|
212
|
+
// falta de verdad: en una extensión la llave vive en el service worker y quien tiene el
|
|
213
|
+
// socket es la página, así que o se firma a distancia o la llave privada tendría que
|
|
214
|
+
// cruzar para nada. Sin ninguna de las dos no se firma, y salir no se puede fingir.
|
|
215
|
+
//
|
|
216
|
+
// Se comprueba ANTES de firmar: sin nada con qué hacerlo, WebCrypto revienta con un error
|
|
217
|
+
// suyo que no dice qué falta («code: 0»), y el de arriba es el que se puede buscar.
|
|
218
|
+
if (typeof sign !== 'function' && !privateJwk && !privateKey) {
|
|
219
|
+
throw err('no-signature', 'closeLogin: give it the device key (privateJwk/privateKey) or a sign(data)')
|
|
220
|
+
}
|
|
221
|
+
const firmado = typeof sign === 'function'
|
|
222
|
+
? await sign(data)
|
|
223
|
+
: await signWithDevice({ privateJwk, privateKey, publickey, data })
|
|
224
|
+
const signature = typeof firmado === 'string' ? firmado : firmado?.signature
|
|
225
|
+
if (typeof signature !== 'string') throw err('no-signature', 'closeLogin: sign(data) returned no signature')
|
|
206
226
|
transport.send(token, { type: MSG.LOGIN_CLOSE, data, signature })
|
|
207
227
|
try {
|
|
208
228
|
const r = await waitFor(transport, token, (p) => p.type === MSG.LOGIN_CLOSED || p.type === MSG.ERROR, timeoutMs)
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { server as opaquePorDefecto, suiteId as suitePorDefecto } from '@dotrino/opaque'
|
|
28
28
|
import { pubkeyId } from '@dotrino/identity/capabilities'
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
29
|
+
import { DEVICE_CAPS, capScope } from '@dotrino/identity/acta'
|
|
30
|
+
import { deviceIdOf } from './enroll.js'
|
|
31
31
|
import { bytesToB64url, b64urlToBytes } from './b64.js'
|
|
32
32
|
|
|
33
33
|
/** Un usuario es la parte de antes de la `@` en `nombre@AB12-CD34-EF56`. */
|
|
@@ -445,24 +445,35 @@ export function createLoginDesk ({ load, save, now = () => Date.now(), opaque =
|
|
|
445
445
|
* @param {object} opts
|
|
446
446
|
* `identity` la identidad que firma (`signDelegation`, `admitMember`)
|
|
447
447
|
* `logins` el escritorio de `createLoginDesk`
|
|
448
|
-
* `
|
|
449
|
-
*
|
|
448
|
+
* `caps` los PERMISOS del acta que lleva el aparato, cualquiera de `DEVICE_CAPS`;
|
|
449
|
+
* por defecto firmar, leer y guardar
|
|
450
450
|
*/
|
|
451
451
|
export async function registerLogin ({
|
|
452
452
|
identity, logins, user, upload, pub, encPub = null, label = '', blob,
|
|
453
|
-
|
|
453
|
+
caps = ['sign', 'read', 'store'], scope, replace = false
|
|
454
454
|
} = {}) {
|
|
455
455
|
if (replace) {
|
|
456
456
|
await logins.registerFinish({ user, upload, blob, replace: true })
|
|
457
457
|
return { ok: true, user, replaced: true }
|
|
458
458
|
}
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
// CUALQUIER PERMISO, y todos por el mismo camino (dueño, 2026-09-21: «al crear debe
|
|
460
|
+
// poder asignársele cualquier permiso»; regla dura «simplificar los procesos»). Antes
|
|
461
|
+
// llegaban como scopes del certificado con `unattended` aparte, `passwords` se rechazaba
|
|
462
|
+
// con `passwords-not-yet` y `passkeys` —que no tiene scope— no se podía asignar.
|
|
463
|
+
//
|
|
464
|
+
// `scope` ya no se acepta, y se dice: ignorarlo daría al aparato los permisos por defecto
|
|
465
|
+
// en vez de los que se eligieron, y nadie lo notaría.
|
|
466
|
+
if (scope !== undefined) {
|
|
467
|
+
throw Object.assign(new Error('registerLogin takes `caps` (account record permissions), not `scope`'), { code: 'use-caps' })
|
|
465
468
|
}
|
|
469
|
+
const pedidos = [...new Set(Array.isArray(caps) ? caps : [])]
|
|
470
|
+
const raros = pedidos.filter((c) => !DEVICE_CAPS.includes(c))
|
|
471
|
+
if (!pedidos.length || raros.length) {
|
|
472
|
+
throw Object.assign(new Error(`not device permissions: ${raros.join(', ') || '(none)'} — expected any of ${DEVICE_CAPS.join(', ')}`), { code: 'bad-caps' })
|
|
473
|
+
}
|
|
474
|
+
// El certificado lleva el scope de los permisos que lo tienen; los demás (`unattended`,
|
|
475
|
+
// `passkeys`) viven solo en el acta, que es quien decide.
|
|
476
|
+
const scopes = pedidos.map((c) => capScope(c)).filter(Boolean)
|
|
466
477
|
if (typeof identity?.admitMember !== 'function') {
|
|
467
478
|
throw Object.assign(new Error('this vault cannot add devices to the account record: no login was created'), { code: 'admit-unavailable' })
|
|
468
479
|
}
|
|
@@ -470,11 +481,9 @@ export async function registerLogin ({
|
|
|
470
481
|
await logins.registerFinish({ user, upload, pub, encPub, deviceId, label, blob })
|
|
471
482
|
try {
|
|
472
483
|
const { cert } = await identity.signDelegation(pub, scopes, { label: label || `login:${user}` })
|
|
473
|
-
|
|
474
|
-
const caps = [...new Set([...scopeToCaps(scopes), ...(unattended ? ['unattended'] : [])])]
|
|
475
|
-
await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn, caps, cert })
|
|
484
|
+
await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn: null, caps: pedidos, cert })
|
|
476
485
|
logins.setCert({ user, cert, iss: identity.me?.publickey || null })
|
|
477
|
-
return { ok: true, user, deviceId, cert, caps }
|
|
486
|
+
return { ok: true, user, deviceId, cert, caps: pedidos }
|
|
478
487
|
} catch (e) {
|
|
479
488
|
// NADA DE MEDIAS ALTAS: si no entra en el acta, no queda un usuario que pueda entrar a
|
|
480
489
|
// una cuenta que no lo reconoce. Se deshace y se dice.
|