@dotrino/identity 0.93.0 → 0.95.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 +2 -1
- package/vault/index.html +2 -0
- package/vault/vault.js +38 -1
- package/vault/vendor/opaque/VERSION.txt +6 -0
- package/vault/vendor/opaque/build/opaque.js +588 -0
- package/vault/vendor/opaque/build/wasm-bytes.js +2 -0
- package/vault/vendor/opaque/src/index.js +123 -0
- package/vault/vendor/vault/VERSION.txt +3 -1
- package/vault/vendor/vault/b64.js +36 -0
- package/vault/vendor/vault/enroll.js +39 -12
- package/vault/vendor/vault/index.js +97 -3
- package/vault/vendor/vault/passwordLogins.js +431 -0
- package/vault/vendor/vault/protocol.js +13 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @dotrino/opaque — OPAQUE (RFC 9807) para el ecosistema Dotrino.
|
|
3
|
+
*
|
|
4
|
+
* Comprueba una contraseña SIN que el servidor la vea nunca y sin entregar nada con qué
|
|
5
|
+
* adivinarla desde fuera. Lo usan las tres versiones del vault (el binario, la pestaña y la
|
|
6
|
+
* extensión) y el gestor, para el aparato que se abre con usuario y contraseña.
|
|
7
|
+
*
|
|
8
|
+
* Aquí no hay criptografía propia: el protocolo es `opaque-ke` (Meta, auditado por NCC
|
|
9
|
+
* Group) compilado a WASM desde su código fuente en nuestro CI. Este archivo solo pone
|
|
10
|
+
* nombres y comprueba lo que entra.
|
|
11
|
+
*
|
|
12
|
+
* Todo lo que entra y sale son cadenas base64url. Los errores llevan `code`:
|
|
13
|
+
* · `bad-input` — falta un dato o no se puede leer
|
|
14
|
+
* · `login-failed` — contraseña equivocada, usuario inexistente, identificadores que no
|
|
15
|
+
* casan o un mensaje alterado. A propósito son el MISMO código: distinguirlos
|
|
16
|
+
* le diría a quien prueba qué usuarios existen.
|
|
17
|
+
* · `protocol` — cualquier otro fallo del protocolo
|
|
18
|
+
* · `internal` — un fallo que no viene del protocolo (no debería pasar)
|
|
19
|
+
*/
|
|
20
|
+
import * as wasm from '../build/opaque.js'
|
|
21
|
+
import wasmBytes from '../build/wasm-bytes.js'
|
|
22
|
+
|
|
23
|
+
export class OpaqueError extends Error {
|
|
24
|
+
constructor (code, message) {
|
|
25
|
+
super(message)
|
|
26
|
+
this.name = 'OpaqueError'
|
|
27
|
+
this.code = code
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let ready = false
|
|
32
|
+
function init () {
|
|
33
|
+
if (ready) return
|
|
34
|
+
const bin = Uint8Array.from(atob(wasmBytes), (c) => c.charCodeAt(0))
|
|
35
|
+
wasm.initSync({ module: bin })
|
|
36
|
+
ready = true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const CODE = /^(bad-input|login-failed|protocol): ([\s\S]*)$/
|
|
40
|
+
|
|
41
|
+
function call (fn, ...args) {
|
|
42
|
+
init()
|
|
43
|
+
try {
|
|
44
|
+
return fn(...args)
|
|
45
|
+
} catch (e) {
|
|
46
|
+
const msg = String(e?.message ?? e)
|
|
47
|
+
const m = CODE.exec(msg)
|
|
48
|
+
if (m) throw new OpaqueError(m[1], m[2])
|
|
49
|
+
throw new OpaqueError('internal', msg)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function need (name, v) {
|
|
54
|
+
if (typeof v !== 'string' || !v) throw new OpaqueError('bad-input', `${name} is required`)
|
|
55
|
+
return v
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Los identificadores que se atan al intercambio. Si se usan, las DOS puntas tienen que
|
|
60
|
+
* pasar los mismos; si no casan, el inicio falla como una contraseña equivocada.
|
|
61
|
+
*/
|
|
62
|
+
function idents (identifiers) {
|
|
63
|
+
if (identifiers == null || typeof identifiers !== 'object') throw new OpaqueError('bad-input', 'identifiers must be an object')
|
|
64
|
+
const { client, server } = identifiers
|
|
65
|
+
for (const [k, v] of [['identifiers.client', client], ['identifiers.server', server]]) {
|
|
66
|
+
if (v !== undefined && (typeof v !== 'string' || !v)) throw new OpaqueError('bad-input', `${k} must be a non-empty string`)
|
|
67
|
+
}
|
|
68
|
+
return [client, server]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** La suite y sus parámetros. Se guarda junto a cada registro. */
|
|
72
|
+
export function suiteId () {
|
|
73
|
+
return call(wasm.suiteId)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const server = {
|
|
77
|
+
/** La preparación del servidor: SECRETA, una por bóveda. Sin ella no se puede comprobar nada. */
|
|
78
|
+
createSetup () {
|
|
79
|
+
return call(wasm.serverCreateSetup)
|
|
80
|
+
},
|
|
81
|
+
publicKey ({ setup } = {}) {
|
|
82
|
+
return call(wasm.serverPublicKey, need('setup', setup))
|
|
83
|
+
},
|
|
84
|
+
registrationResponse ({ setup, request, credentialId } = {}) {
|
|
85
|
+
return call(wasm.serverRegistrationResponse, need('setup', setup), need('request', request), need('credentialId', credentialId))
|
|
86
|
+
},
|
|
87
|
+
/** Lo que se guarda del usuario (el «registro»). */
|
|
88
|
+
registrationFinish ({ upload } = {}) {
|
|
89
|
+
return call(wasm.serverRegistrationFinish, need('upload', upload))
|
|
90
|
+
},
|
|
91
|
+
/**
|
|
92
|
+
* `record` tiene que venir SIEMPRE: el registro, o `null` si el usuario no existe. Con
|
|
93
|
+
* `null` responde igual, así que desde fuera no se sabe qué usuarios hay.
|
|
94
|
+
*/
|
|
95
|
+
loginStart ({ setup, record, request, credentialId, identifiers = {} } = {}) {
|
|
96
|
+
if (record !== null && (typeof record !== 'string' || !record)) {
|
|
97
|
+
throw new OpaqueError('bad-input', 'record must be the stored record, or null for an unknown user')
|
|
98
|
+
}
|
|
99
|
+
return call(wasm.serverLoginStart, need('setup', setup), record ?? undefined, need('request', request), need('credentialId', credentialId), ...idents(identifiers))
|
|
100
|
+
},
|
|
101
|
+
loginFinish ({ state, finalization, identifiers = {} } = {}) {
|
|
102
|
+
return call(wasm.serverLoginFinish, need('state', state), need('finalization', finalization), ...idents(identifiers))
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const client = {
|
|
107
|
+
registrationStart ({ password } = {}) {
|
|
108
|
+
return call(wasm.clientRegistrationStart, need('password', password))
|
|
109
|
+
},
|
|
110
|
+
/** Devuelve `upload` (para el servidor) y `exportKey`, que solo sale de la contraseña. */
|
|
111
|
+
registrationFinish ({ state, response, password, identifiers = {} } = {}) {
|
|
112
|
+
return call(wasm.clientRegistrationFinish, need('state', state), need('response', response), need('password', password), ...idents(identifiers))
|
|
113
|
+
},
|
|
114
|
+
loginStart ({ password } = {}) {
|
|
115
|
+
return call(wasm.clientLoginStart, need('password', password))
|
|
116
|
+
},
|
|
117
|
+
/** Devuelve `finalization` (para el servidor), `sessionKey` y el mismo `exportKey` del registro. */
|
|
118
|
+
loginFinish ({ state, response, password, identifiers = {} } = {}) {
|
|
119
|
+
return call(wasm.clientLoginFinish, need('state', state), need('response', response), need('password', password), ...idents(identifiers))
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default { suiteId, server, client, OpaqueError }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
Copia vendorizada de @dotrino/vault@0.
|
|
1
|
+
Copia vendorizada de @dotrino/vault@0.66.0 (dotrino-vault/lib/src/{index,enroll,protocol,passwordLogins,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
|
|
5
5
|
@dotrino/proxy-client (= ../proxy-client/), todos por el import map de index.html.
|
|
6
|
+
passwordLogins.js es el aparato que se abre con usuario y contraseña: lo carga
|
|
7
|
+
vault.js SOLO cuando esta pestaña es bóveda, porque arrastra el OPAQUE en WASM.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* b64.js — base64url a mano, sin `Buffer` ni `btoa`.
|
|
3
|
+
*
|
|
4
|
+
* Vive aparte porque lo usan piezas que corren en los tres sitios: el binario (Node), la
|
|
5
|
+
* pestaña y la extensión. `Buffer` no existe en el navegador y `btoa` no existe en algunos
|
|
6
|
+
* workers, así que la única forma de tener UNA implementación es esta.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const B64_STD = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
10
|
+
|
|
11
|
+
export function bytesToB64url (bytes) {
|
|
12
|
+
let out = ''
|
|
13
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
14
|
+
const a = bytes[i]; const b = bytes[i + 1]; const c = bytes[i + 2]
|
|
15
|
+
out += B64_STD[a >> 2]
|
|
16
|
+
out += B64_STD[((a & 3) << 4) | ((b ?? 0) >> 4)]
|
|
17
|
+
if (b === undefined) break
|
|
18
|
+
out += B64_STD[((b & 15) << 2) | ((c ?? 0) >> 6)]
|
|
19
|
+
if (c === undefined) break
|
|
20
|
+
out += B64_STD[c & 63]
|
|
21
|
+
}
|
|
22
|
+
return out.replace(/\+/g, '-').replace(/\//g, '_')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function b64urlToBytes (s) {
|
|
26
|
+
const clean = String(s).replace(/-/g, '+').replace(/_/g, '/').replace(/[^A-Za-z0-9+/]/g, '')
|
|
27
|
+
const out = []
|
|
28
|
+
let acc = 0; let bits = 0
|
|
29
|
+
for (const ch of clean) {
|
|
30
|
+
const v = B64_STD.indexOf(ch)
|
|
31
|
+
if (v < 0) return null
|
|
32
|
+
acc = (acc << 6) | v; bits += 6
|
|
33
|
+
if (bits >= 8) { bits -= 8; out.push((acc >> bits) & 0xff) }
|
|
34
|
+
}
|
|
35
|
+
return Uint8Array.from(out)
|
|
36
|
+
}
|
|
@@ -349,23 +349,50 @@ export function createEnrollDesk ({
|
|
|
349
349
|
return { ok: true, deviceId: pend.deviceId, adopting: true }
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
const { cert } = await identity.signDelegation(pend.dpub, pend.scope, { ttlMs: pend.ttlMs, label: pend.label })
|
|
353
|
-
|
|
354
352
|
// Aprobar un emparejamiento ES admitir al dispositivo en el perfil: el cert es la
|
|
355
353
|
// credencial y el acta es la política, y no tiene sentido emitir una sin la otra.
|
|
356
354
|
// Las capacidades salen del scope que se pidió al emparejar (cert ∩ acta, §2.3).
|
|
357
|
-
|
|
355
|
+
//
|
|
356
|
+
// NADA DE REPLIEGUES (2026-09-17). Esto antes estaba envuelto en un `catch` que solo
|
|
357
|
+
// anotaba el error, y además se saltaba en silencio si la identidad no sabía admitir o si
|
|
358
|
+
// el scope no daba ninguna capacidad: el aparato recibía su certificado SIN estar en el
|
|
359
|
+
// acta, y el fallo aparecía después y en otro sitio. Ahora, si no se puede admitir, no
|
|
360
|
+
// se entrega nada y se dice.
|
|
361
|
+
if (typeof identity.admitMember !== 'function') {
|
|
362
|
+
throw Object.assign(new Error('this vault cannot add devices to the account record: no certificate was issued'), { code: 'admit-unavailable' })
|
|
363
|
+
}
|
|
364
|
+
// PERMISOS, no tipos (2026-08-22): las capacidades son las del scope ENTERO. Un
|
|
365
|
+
// cajón (`secrets:<ns>`) suma `secrets` y fija el CN; no borra lo demás — un bot
|
|
366
|
+
// con `sign,secrets:eco` firma como aparato del acta Y lee solo su cajón.
|
|
367
|
+
const cn = scopeToCn(pend.scope)
|
|
368
|
+
const caps = [...new Set([...scopeToCaps(pend.scope), ...(cn ? ['secrets'] : [])])]
|
|
369
|
+
if (!caps.length) {
|
|
370
|
+
throw Object.assign(new Error('the pairing scope grants no permission: no certificate was issued'), { code: 'empty-scope' })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const { cert } = await identity.signDelegation(pend.dpub, pend.scope, { ttlMs: pend.ttlMs, label: pend.label })
|
|
374
|
+
|
|
358
375
|
try {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
376
|
+
await identity.admitMember({ pub: pend.dpub, encPub: pend.encPub || null, label: pend.label || '', cn, caps, cert, continuity: pend.continuity || null })
|
|
377
|
+
} catch (e) {
|
|
378
|
+
// El certificado ya está firmado y no se entrega: se revoca, para que no quede uno
|
|
379
|
+
// válido suelto. Si ni eso sale, se dice también.
|
|
380
|
+
let revoked = true
|
|
381
|
+
try { await identity.revokeDelegation(cert.nonce) } catch (re) {
|
|
382
|
+
revoked = false
|
|
383
|
+
log('[vault] could not revoke the undelivered certificate %s: %s', cert.nonce, re.message)
|
|
366
384
|
}
|
|
367
|
-
|
|
368
|
-
|
|
385
|
+
audit('rejected', { what: 'approve', device: pend.deviceId, reason: 'admit-failed' })
|
|
386
|
+
reply(pend.from, { type: MSG_ERROR, error: 'the vault could not add this device to the account: pairing failed, try again' })
|
|
387
|
+
pending.delete(pend.token)
|
|
388
|
+
fire(onPendingChange)
|
|
389
|
+
log('[vault] could not add %s to the record, no certificate was delivered: %s', pend.deviceId, e.message)
|
|
390
|
+
throw Object.assign(
|
|
391
|
+
new Error(`could not add the device to the account record (${e.message}): no certificate was delivered${revoked ? '' : ', and the signed one could not be revoked'}`),
|
|
392
|
+
{ code: 'admit-failed', cause: e }
|
|
393
|
+
)
|
|
394
|
+
}
|
|
395
|
+
const record = (await identity.profileActa?.())?.acta || null
|
|
369
396
|
|
|
370
397
|
audit('enroll', { device: pend.deviceId, label: pend.label || '', scope: pend.scope })
|
|
371
398
|
// Echamos el código tipeado junto al cert: el DISPOSITIVO acepta solo si coincide
|
|
@@ -46,11 +46,16 @@ export { deviceIdOf }
|
|
|
46
46
|
* `me.publickey`, `signData`, `signDelegation`, `listDelegations`, `revokeDelegation`.
|
|
47
47
|
* @param {object} [opts]
|
|
48
48
|
* @param {string} [opts.proxyUrl='wss://proxy.dotrino.com']
|
|
49
|
+
* @param {object} [opts.logins] escritorio de `createLoginDesk` (`./password-logins`) para
|
|
50
|
+
* ENTRAR CON USUARIO Y CONTRASEÑA. Lo monta quien levanta esta bóveda, porque en el
|
|
51
|
+
* navegador no hay archivos y el estado tiene que guardarlo él. Sin él, esta bóveda
|
|
52
|
+
* contesta `logins-unavailable` — no se inventa un almacén.
|
|
49
53
|
* @returns {Promise<object>} handle: { iss, proxy, client, startPairing, stopPairing,
|
|
50
|
-
* approve, reject, listPending, listMachines, revoke, getSelfCert,
|
|
51
|
-
*
|
|
54
|
+
* approve, reject, listPending, listMachines, revoke, getSelfCert, listLogins,
|
|
55
|
+
* loginRegisterBegin, loginRegisterFinish, loginBegin, loginEnd, closeLogin,
|
|
56
|
+
* clearLoginBlock, removeLogin, onPendingChange, onAdopted, close }
|
|
52
57
|
*/
|
|
53
|
-
export async function startDeviceVault (identity, { proxyUrl, client: injectedClient } = {}) {
|
|
58
|
+
export async function startDeviceVault (identity, { proxyUrl, client: injectedClient, logins = null } = {}) {
|
|
54
59
|
const iss = identity.me?.publickey
|
|
55
60
|
if (!iss) throw new Error('no identity: create/unlock your identity before using this device as a vault')
|
|
56
61
|
const proxy = proxyUrl || 'wss://proxy.dotrino.com'
|
|
@@ -328,8 +333,72 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
328
333
|
else if (p.type === MSG.GET) handle('get', handleGet(_from, p), _from)
|
|
329
334
|
else if (p.type === MSG.STORE) handle('store', handleStore(_from, p), _from)
|
|
330
335
|
else if (p.type === MSG.CHECK) handle('check', handleCheck(_from, p), _from)
|
|
336
|
+
// ENTRAR CON USUARIO Y CONTRASEÑA. La misma pieza que usa el binario
|
|
337
|
+
// (`passwordLogins.js`), con el almacén que le ponga quien monta esta bóveda: en el
|
|
338
|
+
// navegador no hay archivos, así que lo pone el iframe de identidad.
|
|
339
|
+
else if (p.type === MSG.LOGIN_START) handle('login', handleLoginStart(_from, p), _from)
|
|
340
|
+
else if (p.type === MSG.LOGIN_FINISH) handle('login', handleLoginFinish(_from, p), _from)
|
|
341
|
+
else if (p.type === MSG.LOGIN_CLOSE) handle('login', handleLoginClose(_from, p), _from)
|
|
331
342
|
})
|
|
332
343
|
|
|
344
|
+
// --- ENTRAR CON USUARIO Y CONTRASEÑA ---------------------------------------------------
|
|
345
|
+
//
|
|
346
|
+
// Sin almacén no se inventa nada: se contesta que aquí no hay inicios de sesión. Es una
|
|
347
|
+
// bóveda que no atiende esto, no una que dice que la contraseña está mal.
|
|
348
|
+
const noLogins = (from) => send(from, { type: MSG.ERROR, error: 'this vault does not keep password logins', code: 'logins-unavailable' })
|
|
349
|
+
|
|
350
|
+
/** Lo mismo desde la consola: sin almacén no se administra nada, y se dice. */
|
|
351
|
+
function needLogins () {
|
|
352
|
+
if (!logins) throw Object.assign(new Error('this vault does not keep password logins: no store was given to startDeviceVault'), { code: 'logins-unavailable' })
|
|
353
|
+
return logins
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function loginError (from, e) {
|
|
357
|
+
const code = e?.code
|
|
358
|
+
if (code === 'too-many-tries') return send(from, { type: MSG.ERROR, error: e.message, code, waitMs: e.waitMs || 0 })
|
|
359
|
+
if (code === 'login-failed' || code === 'no-exchange' || code === 'bad-input' || code === 'bad-user') {
|
|
360
|
+
return send(from, { type: MSG.ERROR, error: e.message, code })
|
|
361
|
+
}
|
|
362
|
+
send(from, { type: MSG.ERROR, error: 'login failed', code: 'login-error' })
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function handleLoginStart (from, p) {
|
|
366
|
+
if (!logins) return noLogins(from)
|
|
367
|
+
try {
|
|
368
|
+
const { lid, response } = logins.loginBegin({ user: p?.user, request: p?.request })
|
|
369
|
+
send(from, { type: MSG.LOGIN_RESPONSE, lid, response })
|
|
370
|
+
} catch (e) { loginError(from, e) }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function handleLoginFinish (from, p) {
|
|
374
|
+
if (!logins) return noLogins(from)
|
|
375
|
+
try {
|
|
376
|
+
const r = logins.loginEnd({ lid: p?.lid, finalization: p?.finalization, label: p?.label })
|
|
377
|
+
const acta = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
378
|
+
send(from, { type: MSG.LOGIN_OK, sid: r.sid, blob: r.blob, cert: r.cert, iss: r.iss || iss, acta })
|
|
379
|
+
} catch (e) { loginError(from, e) }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** Salir va firmado con la llave que acaba de abrir: cerrar la de otro sería echarlo. */
|
|
383
|
+
async function handleLoginClose (from, p) {
|
|
384
|
+
if (!logins) return noLogins(from)
|
|
385
|
+
const d = p?.data
|
|
386
|
+
if (typeof d?.ts !== 'number' || Math.abs(Date.now() - d.ts) > FRESH_WINDOW_MS) {
|
|
387
|
+
return send(from, { type: MSG.ERROR, error: 'stale request', code: 'stale' })
|
|
388
|
+
}
|
|
389
|
+
if (d?.op !== 'login.close' || typeof d.publickey !== 'string' || typeof d.user !== 'string' || typeof d.sid !== 'string') {
|
|
390
|
+
return send(from, { type: MSG.ERROR, error: 'unauthorized: shape', code: 'bad-input' })
|
|
391
|
+
}
|
|
392
|
+
if (!(await verifyDeviceSig({ publickey: d.publickey, data: d, signature: p.signature }))) {
|
|
393
|
+
return send(from, { type: MSG.ERROR, error: 'unauthorized: bad-signature', code: 'bad-signature' })
|
|
394
|
+
}
|
|
395
|
+
const mine = logins.list().find((x) => x.user === d.user)
|
|
396
|
+
if (!mine || mine.pub !== d.publickey) {
|
|
397
|
+
return send(from, { type: MSG.ERROR, error: 'unauthorized: that key does not own this login', code: 'not-yours' })
|
|
398
|
+
}
|
|
399
|
+
send(from, { type: MSG.LOGIN_CLOSED, ok: logins.closeSession({ user: d.user, sid: d.sid }).ok })
|
|
400
|
+
}
|
|
401
|
+
|
|
333
402
|
/**
|
|
334
403
|
* Máquinas enroladas bajo esta identidad (P), vigentes, con scope de firma y label
|
|
335
404
|
* propio (excluye navegadores enrolados con label 'cli', que no atienden peticiones).
|
|
@@ -368,6 +437,31 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
368
437
|
// exactamente esto.
|
|
369
438
|
revoke: (nonce) => desk.revoke(nonce),
|
|
370
439
|
getSelfCert,
|
|
440
|
+
// ENTRAR CON USUARIO Y CONTRASEÑA, desde la consola de esta bóveda. Es la misma pieza
|
|
441
|
+
// que el binario (`registerLogin`), para que el aparato que sale de aquí sea idéntico.
|
|
442
|
+
// Sin almacén no hay nada que administrar, y se dice en vez de contestar una lista vacía.
|
|
443
|
+
listLogins: () => needLogins().list(),
|
|
444
|
+
loginRegisterBegin: (opts) => needLogins().registerBegin(opts),
|
|
445
|
+
// El alta se trae cuando se usa: arrastra el OPAQUE en WASM (unos 260 KB) y la mayoría
|
|
446
|
+
// de las apps que usan este módulo no crean ningún inicio de sesión.
|
|
447
|
+
loginRegisterFinish: async (opts) => {
|
|
448
|
+
const { registerLogin } = await import('./passwordLogins.js')
|
|
449
|
+
return registerLogin({ ...opts, identity, logins: needLogins() })
|
|
450
|
+
},
|
|
451
|
+
// Entrar desde la propia consola, sin pasar por el proxio. Hace falta para cambiar la
|
|
452
|
+
// contraseña: el paquete de llaves se abre con la vieja y se vuelve a cerrar con la nueva.
|
|
453
|
+
loginBegin: ({ user, request }) => needLogins().loginBegin({ user, request }),
|
|
454
|
+
loginEnd: ({ lid, finalization, label }) => needLogins().loginEnd({ lid, finalization, label }),
|
|
455
|
+
closeLogin: ({ user, sid }) => needLogins().closeSession({ user, sid }),
|
|
456
|
+
clearLoginBlock: ({ user }) => needLogins().clearBlock({ user }),
|
|
457
|
+
removeLogin: ({ user }) => {
|
|
458
|
+
const store = needLogins()
|
|
459
|
+
const found = store.list().find((x) => x.user === user)
|
|
460
|
+
if (!found) return { ok: false }
|
|
461
|
+
store.remove({ user })
|
|
462
|
+
// Quitarlo de aquí no lo saca del acta: lo suyo es revocar el aparato, y eso ya existe.
|
|
463
|
+
return { ok: true, pub: found.pub, deviceId: found.deviceId }
|
|
464
|
+
},
|
|
371
465
|
onPendingChange (fn) { _onPendingChange = fn || (() => {}) },
|
|
372
466
|
/** Camino A: la cuenta del aparato quedó adoptada por esta bóveda. */
|
|
373
467
|
onAdopted (fn) { _onAdopted = fn || (() => {}) },
|