@dotrino/vault 0.68.0 → 0.69.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/loginClient.js +28 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotrino/vault",
3
- "version": "0.68.0",
3
+ "version": "0.69.0",
4
4
  "description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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 opaque } from '@dotrino/opaque'
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
- const { signature } = await signWithDevice({ privateJwk, privateKey, publickey, data })
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)