@dotrino/vault 0.92.0 → 0.93.1
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/bin/dotrino-env.js +38 -38
- package/package.json +1 -1
- package/src/admin.js +2 -2
- package/src/atrest.js +4 -4
- package/src/browser.js +10 -10
- package/src/core/approvals.js +3 -2
- package/src/core/libVersion.js +2 -2
- package/src/core/sealer.js +2 -2
- package/src/core/sealers.js +15 -15
- package/src/core/secretsStore.js +66 -66
- package/src/core/subacta.js +8 -8
- package/src/core/threadStore.js +2 -2
- package/src/core/transport.js +31 -31
- package/src/core/vault.js +247 -214
- package/src/enroll.js +1 -1
- package/src/env.js +10 -10
- package/src/index.js +7 -7
- package/src/kek.js +6 -6
- package/src/localdesk.js +10 -10
- package/src/lock.js +18 -18
- package/src/loginClient.js +23 -23
- package/src/passwordLogins.js +8 -8
- package/src/proc.js +5 -5
- package/src/procContext.js +2 -2
- package/src/service.js +67 -43
- package/src/sshKeys.js +3 -3
- package/src/version.js +4 -4
package/src/enroll.js
CHANGED
|
@@ -272,7 +272,7 @@ export function createEnrollDesk ({
|
|
|
272
272
|
return { ignored: true }
|
|
273
273
|
}
|
|
274
274
|
if (!pend || Date.now() > pend.exp) {
|
|
275
|
-
audit('rejected', { what: 'hello', reason: '
|
|
275
|
+
audit('rejected', { what: 'hello', reason: 'no-session' })
|
|
276
276
|
return fail(from, 'no pairing session open for that code', String(p?.sn || ''))
|
|
277
277
|
}
|
|
278
278
|
// La respuesta va FIRMADA por la maestra y el `sn` va dentro de lo firmado. Eso ata
|
package/src/env.js
CHANGED
|
@@ -64,14 +64,14 @@ export function serviceDir (vault, ns) {
|
|
|
64
64
|
// Con dos, en cambio, elegir por el usuario sería adivinar cuál de sus cuentas usar — así
|
|
65
65
|
// que se para y se dice cuáles hay. Eso es lo contrario de un repliegue: no se resuelve
|
|
66
66
|
// una ambigüedad a la brava, se señala.
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
if (
|
|
70
|
-
throw new Error(`"${ns}" is enrolled in more than one vault (${
|
|
67
|
+
const where = listEnrolled().filter((e) => !e.legacy && e.ns === ns)
|
|
68
|
+
if (where.length === 1) return path.join(serviceRoot(), where[0].vault, ns)
|
|
69
|
+
if (where.length > 1) {
|
|
70
|
+
throw new Error(`"${ns}" is enrolled in more than one vault (${where.map((e) => e.vault).join(', ')}): pick one with --vault <label>`)
|
|
71
71
|
}
|
|
72
|
-
const
|
|
73
|
-
throw new Error(
|
|
74
|
-
? `"${ns}" is not enrolled on this machine (there is: ${
|
|
72
|
+
const others = listEnrolled().filter((e) => !e.legacy)
|
|
73
|
+
throw new Error(others.length
|
|
74
|
+
? `"${ns}" is not enrolled on this machine (there is: ${others.map((e) => `${e.vault}/${e.ns}`).join(', ')})`
|
|
75
75
|
: `"${ns}" is not enrolled on this machine: run \`dotrino-env enroll --vault <label> --ns ${ns}\` first`)
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -94,9 +94,9 @@ export function listEnrolled () {
|
|
|
94
94
|
// Un enrolamiento del formato viejo es un directorio con `service-identity.json`
|
|
95
95
|
// DENTRO; uno del nuevo tiene subdirectorios. Así se distinguen sin adivinar.
|
|
96
96
|
if (readServiceIdentity(path.join(serviceRoot(), d.name))) { out.push({ legacy: true, ns: d.name }); continue }
|
|
97
|
-
let
|
|
98
|
-
try {
|
|
99
|
-
for (const ns of
|
|
97
|
+
let children = []
|
|
98
|
+
try { children = fs.readdirSync(path.join(serviceRoot(), d.name)) } catch (_) { continue }
|
|
99
|
+
for (const ns of children) {
|
|
100
100
|
if (isValidSecretsNs(ns) && readServiceIdentity(path.join(serviceRoot(), d.name, ns))) out.push({ vault: d.name, ns })
|
|
101
101
|
}
|
|
102
102
|
}
|
package/src/index.js
CHANGED
|
@@ -72,7 +72,7 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
72
72
|
* Sin acta van nulos y `verifyDelegation` contesta `no-acta`: no hay con qué decidir, así
|
|
73
73
|
* que no se decide que sí.
|
|
74
74
|
*/
|
|
75
|
-
async function
|
|
75
|
+
async function actaContext () {
|
|
76
76
|
const acta = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
77
77
|
if (!acta) return { actaSeq: null, sealers: null }
|
|
78
78
|
return { actaSeq: acta.seq, sealers: sealersOf(acta) }
|
|
@@ -81,7 +81,7 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
81
81
|
let _selfCert = null
|
|
82
82
|
const getSelfCert = async () => {
|
|
83
83
|
// Se rehace cuando el acta cambia, no cuando pasa el tiempo: el papel ya no caduca.
|
|
84
|
-
const { actaSeq } = await
|
|
84
|
+
const { actaSeq } = await actaContext()
|
|
85
85
|
if (_selfCert && _selfCert.seq === actaSeq) return _selfCert
|
|
86
86
|
const { cert } = await identity.signDelegation(iss, SIGN_SCOPE)
|
|
87
87
|
_selfCert = cert
|
|
@@ -187,7 +187,7 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
187
187
|
if (typeof d.ts !== 'number' || Math.abs(Date.now() - d.ts) > FRESH_WINDOW_MS) {
|
|
188
188
|
return send(from, { type: MSG.ERROR, error: 'stale request: ts outside the ±5 min window (possible replay, or the device clock is off)' })
|
|
189
189
|
}
|
|
190
|
-
const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, ...(await
|
|
190
|
+
const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, ...(await actaContext()), revoked: await revocationSet() })
|
|
191
191
|
if (!chk.ok) return send(from, { type: MSG.ERROR, error: 'unauthorized: ' + chk.reason })
|
|
192
192
|
// Reusar el label del cert original (si sigue registrado en delegations).
|
|
193
193
|
const { issued } = await identity.listDelegations()
|
|
@@ -210,7 +210,7 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
210
210
|
const d = p?.data
|
|
211
211
|
if (!d || !p.signature || !p.cert) return send(from, { type: MSG.ERROR, error: 'invalid request' })
|
|
212
212
|
if (typeof d.ts !== 'number' || Math.abs(Date.now() - d.ts) > FRESH_WINDOW_MS) return
|
|
213
|
-
const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, ...(await
|
|
213
|
+
const chk = await verifyChain({ data: d, signature: p.signature, cert: p.cert, ...(await actaContext()) })
|
|
214
214
|
if (!chk.ok) return send(from, { type: MSG.ERROR, error: 'unauthorized: ' + chk.reason })
|
|
215
215
|
const { issued, revoked, revokedCerts } = await identity.listDelegations()
|
|
216
216
|
const devices = await Promise.all((issued || []).map(async (x) => ({
|
|
@@ -248,7 +248,7 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
248
248
|
const chk = await verifyChain({
|
|
249
249
|
data: d, signature: p.signature, cert: p.cert,
|
|
250
250
|
...(expectedScope ? { expectedScope } : {}),
|
|
251
|
-
...(await
|
|
251
|
+
...(await actaContext()), revoked: await revocationSet(),
|
|
252
252
|
})
|
|
253
253
|
if (!chk.ok) {
|
|
254
254
|
send(from, { type: MSG.ERROR, error: 'unauthorized: ' + chk.reason })
|
|
@@ -261,7 +261,7 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
261
261
|
if (expectedScope) {
|
|
262
262
|
const record = (await identity.profileActa?.().catch(() => null))?.acta || null
|
|
263
263
|
if (record && !memberCanScope(record, chk.device, expectedScope)) {
|
|
264
|
-
send(from, { type: MSG.ERROR, error: 'unauthorized:
|
|
264
|
+
send(from, { type: MSG.ERROR, error: 'unauthorized: record — this member no longer has that permission' })
|
|
265
265
|
return null
|
|
266
266
|
}
|
|
267
267
|
}
|
|
@@ -356,7 +356,7 @@ export async function startDeviceVault (identity, { proxyUrl, client: injectedCl
|
|
|
356
356
|
if (p.type === MSG.HELLO) handle('hello', desk.handleHello(_from, p), _from)
|
|
357
357
|
else if (p.type === MSG.ENROLL) handle('enroll', desk.handleEnroll(_from, p), _from)
|
|
358
358
|
// Camino A: el aparato devuelve su acta sellada admitiendo a esta bóveda.
|
|
359
|
-
else if (p.type === MSG.ACTA_SEALED) handle('
|
|
359
|
+
else if (p.type === MSG.ACTA_SEALED) handle('record-sealed', desk.handleActaSealed(_from, p), _from)
|
|
360
360
|
else if (p.type === MSG.RENEW) handle('renew', handleRenew(_from, p), _from)
|
|
361
361
|
else if (p.type === MSG.DEVICES) handle('devices', handleDevices(_from, p), _from)
|
|
362
362
|
// Lo que faltaba para que un aparato enrolado aquí pueda hacer lo mismo que contra
|
package/src/kek.js
CHANGED
|
@@ -182,21 +182,21 @@ function commandKey (dir, cfg, magic) {
|
|
|
182
182
|
*/
|
|
183
183
|
export function assertSameMachine (dir, material, hasData) {
|
|
184
184
|
const f = path.join(dir, MACHINE_FILE)
|
|
185
|
-
const
|
|
186
|
-
let
|
|
187
|
-
try {
|
|
185
|
+
const fingerprint = crypto.createHash('sha256').update('dotrino-atrest-machine\u0000' + material).digest('hex').slice(0, 32)
|
|
186
|
+
let previous = null
|
|
187
|
+
try { previous = fs.readFileSync(f, 'utf8').trim() } catch (_) {}
|
|
188
188
|
|
|
189
|
-
if (
|
|
189
|
+
if (previous && previous !== fingerprint && hasData) {
|
|
190
190
|
throw new KekError('kek-machine-changed',
|
|
191
191
|
'this data was written by a DIFFERENT machine and the key is derived from the machine itself, so it cannot be opened here. ' +
|
|
192
192
|
'In a container this is usually the container id changing on every run: the fix is to stop deriving the key from the machine — ' +
|
|
193
193
|
'give the profile a KMS provider (see docs/llaves-de-hardware.md) and re-create it, or restore this volume on the original host. ' +
|
|
194
194
|
'Nothing was modified.')
|
|
195
195
|
}
|
|
196
|
-
if (!
|
|
196
|
+
if (!previous) {
|
|
197
197
|
try {
|
|
198
198
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
199
|
-
fs.writeFileSync(f,
|
|
199
|
+
fs.writeFileSync(f, fingerprint + '\n', { mode: 0o600 })
|
|
200
200
|
} catch (_) { /* si no se puede escribir la huella, no se bloquea el arranque */ }
|
|
201
201
|
}
|
|
202
202
|
}
|
package/src/localdesk.js
CHANGED
|
@@ -60,28 +60,28 @@ export async function connectLocal ({ masterId, timeoutMs = 3000 } = {}) {
|
|
|
60
60
|
s.once('error', (e) => { clearTimeout(t); reject(new Error('no local vault desk at ' + socketPath + ': ' + e.message)) })
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
const
|
|
63
|
+
const listeners = new Set()
|
|
64
64
|
let buf = ''
|
|
65
65
|
sock.setEncoding('utf8')
|
|
66
66
|
sock.on('data', (chunk) => {
|
|
67
67
|
buf += chunk
|
|
68
68
|
let i
|
|
69
69
|
while ((i = buf.indexOf('\n')) >= 0) {
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
70
|
+
const line = buf.slice(0, i); buf = buf.slice(i + 1)
|
|
71
|
+
if (!line.trim()) continue
|
|
72
72
|
let p = null
|
|
73
|
-
try { p = JSON.parse(
|
|
73
|
+
try { p = JSON.parse(line) } catch (_) { continue }
|
|
74
74
|
// `from` es el mismo para todo: al otro lado solo hay una bóveda.
|
|
75
|
-
for (const fn of
|
|
75
|
+
for (const fn of listeners) { try { fn('local', p) } catch (_) {} }
|
|
76
76
|
}
|
|
77
77
|
})
|
|
78
78
|
|
|
79
|
-
const
|
|
79
|
+
const send = (obj) => { sock.write(JSON.stringify(obj) + '\n') }
|
|
80
80
|
return {
|
|
81
81
|
local: true,
|
|
82
82
|
token: 'local',
|
|
83
|
-
send: (_to, obj) =>
|
|
84
|
-
sendByPubkey: (_pub, obj) =>
|
|
83
|
+
send: (_to, obj) => send(obj),
|
|
84
|
+
sendByPubkey: (_pub, obj) => send(obj),
|
|
85
85
|
// El mostrador local no pide identificarse: la puerta la abre el certificado que va
|
|
86
86
|
// dentro de cada mensaje, igual que por el proxio. Identificarse ahí sirve para ser
|
|
87
87
|
// DIRECCIONABLE, y aquí no hay a quién direccionar.
|
|
@@ -96,8 +96,8 @@ export async function connectLocal ({ masterId, timeoutMs = 3000 } = {}) {
|
|
|
96
96
|
identifyAs: async () => ({ ok: true }),
|
|
97
97
|
on: (ev, fn) => {
|
|
98
98
|
if (ev !== 'message') return () => {}
|
|
99
|
-
|
|
100
|
-
return () =>
|
|
99
|
+
listeners.add(fn)
|
|
100
|
+
return () => listeners.delete(fn)
|
|
101
101
|
},
|
|
102
102
|
close: () => { try { sock.destroy() } catch (_) {} }
|
|
103
103
|
}
|
package/src/lock.js
CHANGED
|
@@ -42,7 +42,7 @@ export const HEARTBEAT_MS = 10_000
|
|
|
42
42
|
* carga o un disco de red lento no pueden costar que otro le quite el candado a uno vivo. */
|
|
43
43
|
export const STALE_MS = 60_000
|
|
44
44
|
|
|
45
|
-
const
|
|
45
|
+
const whoAmI = () => ({ pid: process.pid, host: os.hostname(), desde: Date.now() })
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Toma el candado del directorio, o explica quién lo tiene y se rinde.
|
|
@@ -52,12 +52,12 @@ export function takeLock (dir, { staleMs = STALE_MS, heartbeatMs = HEARTBEAT_MS
|
|
|
52
52
|
const f = path.join(dir, LOCK_FILE)
|
|
53
53
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const attemptOnce = () => {
|
|
56
56
|
try {
|
|
57
57
|
// 'wx' = O_CREAT|O_EXCL: o lo creo yo, o alguien lo tiene. Esa atomicidad es lo
|
|
58
58
|
// único que aquí hace de exclusión.
|
|
59
59
|
const fd = fs.openSync(f, 'wx', 0o600)
|
|
60
|
-
fs.writeSync(fd, JSON.stringify(
|
|
60
|
+
fs.writeSync(fd, JSON.stringify(whoAmI()))
|
|
61
61
|
fs.closeSync(fd)
|
|
62
62
|
return true
|
|
63
63
|
} catch (e) {
|
|
@@ -66,44 +66,44 @@ export function takeLock (dir, { staleMs = STALE_MS, heartbeatMs = HEARTBEAT_MS
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
if (!
|
|
70
|
-
const
|
|
69
|
+
if (!attemptOnce()) {
|
|
70
|
+
const age = (() => {
|
|
71
71
|
try { return Date.now() - fs.statSync(f).mtimeMs } catch (_) { return Infinity }
|
|
72
72
|
})()
|
|
73
|
-
let
|
|
74
|
-
try {
|
|
73
|
+
let owner = null
|
|
74
|
+
try { owner = JSON.parse(fs.readFileSync(f, 'utf8')) } catch (_) {}
|
|
75
75
|
|
|
76
|
-
if (
|
|
76
|
+
if (age <= staleMs) {
|
|
77
77
|
const e = new Error(
|
|
78
|
-
`another vault is running on this data (host ${
|
|
79
|
-
`last heartbeat ${Math.round(
|
|
78
|
+
`another vault is running on this data (host ${owner?.host || '?'}, process ${owner?.pid ?? '?'}, ` +
|
|
79
|
+
`last heartbeat ${Math.round(age / 1000)}s ago)`
|
|
80
80
|
)
|
|
81
81
|
e.code = 'vault-locked'
|
|
82
|
-
e.owner =
|
|
82
|
+
e.owner = owner
|
|
83
83
|
throw e
|
|
84
84
|
}
|
|
85
85
|
// Abandonado: se quita y se vuelve a intentar UNA vez. Si en ese hueco entró otro,
|
|
86
86
|
// pierde este — que es lo correcto: dos que ven el candado viejo a la vez, solo uno
|
|
87
87
|
// gana el `O_EXCL`.
|
|
88
88
|
try { fs.unlinkSync(f) } catch (_) {}
|
|
89
|
-
if (!
|
|
89
|
+
if (!attemptOnce()) {
|
|
90
90
|
const e = new Error('another vault took this data first')
|
|
91
91
|
e.code = 'vault-locked'
|
|
92
92
|
throw e
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
const
|
|
96
|
+
const heartbeat = setInterval(() => {
|
|
97
97
|
// `utimes` y no reescribir: tocar la fecha basta y no puede dejar el archivo a medias.
|
|
98
98
|
try { fs.utimesSync(f, new Date(), new Date()) } catch (_) {}
|
|
99
99
|
}, heartbeatMs)
|
|
100
|
-
|
|
100
|
+
heartbeat.unref?.()
|
|
101
101
|
|
|
102
|
-
let
|
|
102
|
+
let loose = false
|
|
103
103
|
const release = () => {
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
clearInterval(
|
|
104
|
+
if (loose) return
|
|
105
|
+
loose = true
|
|
106
|
+
clearInterval(heartbeat)
|
|
107
107
|
// Solo se borra SI SIGUE SIENDO MÍO. Si otro me lo quitó por viejo, borrarlo aquí le
|
|
108
108
|
// quitaría a él un candado que sí es suyo.
|
|
109
109
|
try {
|
package/src/loginClient.js
CHANGED
|
@@ -54,18 +54,18 @@ export const WAKE_WAIT_MS = 30_000
|
|
|
54
54
|
*/
|
|
55
55
|
function waitFor (transport, from, match, timeoutMs) {
|
|
56
56
|
return new Promise((resolve, reject) => {
|
|
57
|
-
let
|
|
58
|
-
const fin = (fn, arg) => { if (!
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
if (from &&
|
|
57
|
+
let ready = false
|
|
58
|
+
const fin = (fn, arg) => { if (!ready) { ready = true; clearTimeout(clock); remove(); fn(arg) } }
|
|
59
|
+
const clock = setTimeout(() => fin(reject, err('no-answer', 'the vault did not answer in time')), timeoutMs)
|
|
60
|
+
const listener = (who, payload) => {
|
|
61
|
+
if (from && who !== from) return
|
|
62
62
|
const p = typeof payload === 'string' ? parseJson(payload) : payload
|
|
63
63
|
if (p && match(p)) fin(resolve, p)
|
|
64
64
|
}
|
|
65
|
-
const off = transport.on('message',
|
|
66
|
-
const
|
|
65
|
+
const off = transport.on('message', listener)
|
|
66
|
+
const remove = () => {
|
|
67
67
|
if (typeof off === 'function') return off()
|
|
68
|
-
transport.off?.('message',
|
|
68
|
+
transport.off?.('message', listener)
|
|
69
69
|
}
|
|
70
70
|
})
|
|
71
71
|
}
|
|
@@ -103,8 +103,8 @@ export async function loginWithPassword ({ transport, address, password, label =
|
|
|
103
103
|
let tokens = await transport.list(vaultChannel(code))
|
|
104
104
|
// UNA BÓVEDA QUE DUERME (la de una Lambda): el proxio la está despertando y lo dice. Se
|
|
105
105
|
// espera a que se anuncie en vez de contestar que esa cuenta no existe.
|
|
106
|
-
const
|
|
107
|
-
while (!tokens.length && tokens.waking && Date.now() <
|
|
106
|
+
const until = Date.now() + WAKE_WAIT_MS
|
|
107
|
+
while (!tokens.length && tokens.waking && Date.now() < until) {
|
|
108
108
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
109
109
|
tokens = await transport.list(vaultChannel(code))
|
|
110
110
|
}
|
|
@@ -115,25 +115,25 @@ export async function loginWithPassword ({ transport, address, password, label =
|
|
|
115
115
|
// De una en una, y parando en cuanto la contraseña resulte estar mal: cada intento GASTA
|
|
116
116
|
// uno del freno en la bóveda que lo atiende (`passwordLogins.js`), así que repartir el
|
|
117
117
|
// mismo error entre las réplicas solo sirve para bloquearse en todas a la vez.
|
|
118
|
-
let
|
|
118
|
+
let last = null
|
|
119
119
|
for (const token of tokens) {
|
|
120
120
|
try {
|
|
121
|
-
return await
|
|
121
|
+
return await oneAttempt({ transport, token, user, code, dir, password, label, timeoutMs, opaque })
|
|
122
122
|
} catch (e) {
|
|
123
123
|
// Se para: son cosas del que entra, y probar con otra bóveda no las arregla.
|
|
124
124
|
if (e?.code === 'login-failed' || e?.code === 'too-many-tries' || e?.code === 'wrong-account' ||
|
|
125
125
|
e?.code === 'bad-blob' || e?.code === 'bad-keys' || e?.code === 'bad-reply') throw e
|
|
126
|
-
|
|
126
|
+
last = e // «esa no atiende inicios de sesión», «no contestó»: quizá la siguiente sí
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
-
throw
|
|
129
|
+
throw last || err('no-vault', 'none of the vaults on that address could let you in')
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
async function
|
|
132
|
+
async function oneAttempt ({ transport, token, user, code, dir, password, label, timeoutMs, opaque }) {
|
|
133
133
|
const start = await opaque.loginStart({ password })
|
|
134
134
|
transport.send(token, { type: MSG.LOGIN_START, user, request: start.request })
|
|
135
135
|
const r1 = await waitFor(transport, token, (p) => p.type === MSG.LOGIN_RESPONSE || p.type === MSG.ERROR, timeoutMs)
|
|
136
|
-
if (r1.type === MSG.ERROR) throw
|
|
136
|
+
if (r1.type === MSG.ERROR) throw back(r1)
|
|
137
137
|
|
|
138
138
|
let fin
|
|
139
139
|
try {
|
|
@@ -148,7 +148,7 @@ async function unIntento ({ transport, token, user, code, dir, password, label,
|
|
|
148
148
|
|
|
149
149
|
transport.send(token, { type: MSG.LOGIN_FINISH, lid: r1.lid, finalization: fin.finalization, label })
|
|
150
150
|
const ok = await waitFor(transport, token, (p) => p.type === MSG.LOGIN_OK || p.type === MSG.ERROR, timeoutMs)
|
|
151
|
-
if (ok.type === MSG.ERROR) throw
|
|
151
|
+
if (ok.type === MSG.ERROR) throw back(ok)
|
|
152
152
|
|
|
153
153
|
const acta = ok.acta
|
|
154
154
|
const cert = ok.cert
|
|
@@ -168,9 +168,9 @@ async function unIntento ({ transport, token, user, code, dir, password, label,
|
|
|
168
168
|
if (!keys?.sign) throw err('bad-blob', 'the key package does not carry a signing key')
|
|
169
169
|
|
|
170
170
|
// 4. La llave que salió del paquete tiene que SER la del papel. Se prueba firmando.
|
|
171
|
-
const
|
|
172
|
-
const { signature } = await signWithDevice({ privateJwk: keys.sign, data:
|
|
173
|
-
if (!(await verifyDeviceSig({ publickey: cert.sub, data:
|
|
171
|
+
const challenge = { op: 'login.proof', sub: cert.sub, sid: ok.sid, ts: Date.now() }
|
|
172
|
+
const { signature } = await signWithDevice({ privateJwk: keys.sign, data: challenge })
|
|
173
|
+
if (!(await verifyDeviceSig({ publickey: cert.sub, data: challenge, signature }))) {
|
|
174
174
|
throw err('bad-keys', 'the keys in the package are not the ones this certificate is for')
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -196,7 +196,7 @@ async function unIntento ({ transport, token, user, code, dir, password, label,
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
/** Un error de la bóveda con su `code` intacto: `too-many-tries` se arregla esperando. */
|
|
199
|
-
function
|
|
199
|
+
function back (p) {
|
|
200
200
|
const code = p.code || 'login-error'
|
|
201
201
|
const e = err(code, p.error || 'the vault refused the login')
|
|
202
202
|
if (typeof p.waitMs === 'number') e.waitMs = p.waitMs
|
|
@@ -227,10 +227,10 @@ export async function closeLogin ({ transport, token, user, sid, publickey, priv
|
|
|
227
227
|
if (typeof sign !== 'function' && !privateJwk && !privateKey) {
|
|
228
228
|
throw err('no-signature', 'closeLogin: give it the device key (privateJwk/privateKey) or a sign(data)')
|
|
229
229
|
}
|
|
230
|
-
const
|
|
230
|
+
const signed = typeof sign === 'function'
|
|
231
231
|
? await sign(data)
|
|
232
232
|
: await signWithDevice({ privateJwk, privateKey, publickey, data })
|
|
233
|
-
const signature = typeof
|
|
233
|
+
const signature = typeof signed === 'string' ? signed : signed?.signature
|
|
234
234
|
if (typeof signature !== 'string') throw err('no-signature', 'closeLogin: sign(data) returned no signature')
|
|
235
235
|
transport.send(token, { type: MSG.LOGIN_CLOSE, data, signature })
|
|
236
236
|
try {
|
package/src/passwordLogins.js
CHANGED
|
@@ -67,7 +67,7 @@ export const loginAddress = (user, fingerprint) => `${user}@${accountCode(finger
|
|
|
67
67
|
*/
|
|
68
68
|
export async function accountFingerprint (identity) {
|
|
69
69
|
const pid = (await identity?.profileActa?.().catch(() => null))?.acta?.profileId
|
|
70
|
-
if (!pid) throw err('no-
|
|
70
|
+
if (!pid) throw err('no-record', 'this vault has no account record yet: there is no address to build')
|
|
71
71
|
return (await pubkeyId(pid)).slice(0, 16)
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -489,14 +489,14 @@ export async function registerLogin ({
|
|
|
489
489
|
if (scope !== undefined) {
|
|
490
490
|
throw Object.assign(new Error('registerLogin takes `caps` (account record permissions), not `scope`'), { code: 'use-caps' })
|
|
491
491
|
}
|
|
492
|
-
const
|
|
493
|
-
const
|
|
494
|
-
if (!
|
|
495
|
-
throw Object.assign(new Error(`not device permissions: ${
|
|
492
|
+
const requests = [...new Set(Array.isArray(caps) ? caps : [])]
|
|
493
|
+
const oddOnes = requests.filter((c) => !DEVICE_CAPS.includes(c))
|
|
494
|
+
if (!requests.length || oddOnes.length) {
|
|
495
|
+
throw Object.assign(new Error(`not device permissions: ${oddOnes.join(', ') || '(none)'} — expected any of ${DEVICE_CAPS.join(', ')}`), { code: 'bad-caps' })
|
|
496
496
|
}
|
|
497
497
|
// El certificado lleva el scope de los permisos que lo tienen; los demás (`unattended`,
|
|
498
498
|
// `passkeys`) viven solo en el acta, que es quien decide.
|
|
499
|
-
const scopes =
|
|
499
|
+
const scopes = requests.map((c) => capScope(c)).filter(Boolean)
|
|
500
500
|
if (typeof identity?.admitMember !== 'function') {
|
|
501
501
|
throw Object.assign(new Error('this vault cannot add devices to the account record: no login was created'), { code: 'admit-unavailable' })
|
|
502
502
|
}
|
|
@@ -504,9 +504,9 @@ export async function registerLogin ({
|
|
|
504
504
|
await logins.registerFinish({ user, upload, pub, encPub, deviceId, label, blob })
|
|
505
505
|
try {
|
|
506
506
|
const { cert } = await identity.signDelegation(pub, scopes, { label: label || `login:${user}` })
|
|
507
|
-
await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn: null, caps:
|
|
507
|
+
await identity.admitMember({ pub, encPub, label: label || `login:${user}`, cn: null, caps: requests, cert })
|
|
508
508
|
logins.setCert({ user, cert, iss: identity.me?.publickey || null })
|
|
509
|
-
return { ok: true, user, deviceId, cert, caps:
|
|
509
|
+
return { ok: true, user, deviceId, cert, caps: requests }
|
|
510
510
|
} catch (e) {
|
|
511
511
|
// NADA DE MEDIAS ALTAS: si no entra en el acta, no queda un usuario que pueda entrar a
|
|
512
512
|
// una cuenta que no lo reconoce. Se deshace y se dice.
|
package/src/proc.js
CHANGED
|
@@ -89,8 +89,8 @@ export function readProcContext (pid) {
|
|
|
89
89
|
* Node, y entonces no hay nada que comprobar al otro lado.
|
|
90
90
|
*/
|
|
91
91
|
export function selfContext () {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
92
|
+
const measured = readProcContext(process.pid)
|
|
93
|
+
if (measured) return { ...measured, user: userName(), host: os.hostname() }
|
|
94
94
|
const { argv, truncated } = trimArgv([process.execPath, ...process.argv.slice(1)])
|
|
95
95
|
return {
|
|
96
96
|
pid: process.pid,
|
|
@@ -111,9 +111,9 @@ export function selfContext () {
|
|
|
111
111
|
* secretos, así que se refresca solo cuando cambia de máquina o de sistema.
|
|
112
112
|
*/
|
|
113
113
|
export function selfMachine () {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
return cleanMachine({ kind: 'node', os: `${
|
|
114
|
+
const kind = os.type()
|
|
115
|
+
const system = kind === 'Darwin' ? 'macOS' : kind === 'Windows_NT' ? 'Windows' : kind
|
|
116
|
+
return cleanMachine({ kind: 'node', os: `${system} ${os.release()}`, name: os.hostname() })
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
const safeCwd = () => { try { return process.cwd() } catch (_) { return '' } }
|
package/src/procContext.js
CHANGED
|
@@ -90,8 +90,8 @@ export const sameCommand = (a, b) => {
|
|
|
90
90
|
/** El comando en una línea, para un log o una pantalla estrecha. */
|
|
91
91
|
export function commandLine (ctx, max = 160) {
|
|
92
92
|
const argv = Array.isArray(ctx?.argv) ? ctx.argv : []
|
|
93
|
-
const
|
|
94
|
-
return
|
|
93
|
+
const line = argv.length ? argv.join(' ') : (ctx?.exe || '')
|
|
94
|
+
return line.length > max ? line.slice(0, max - 1) + '…' : line
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
export default { sanitizeContext, commandFingerprint, sameCommand, commandLine, trimArgv }
|