@dotrino/identity 0.120.0 → 0.121.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/vault/originPolicy.js +65 -0
- package/vault/vault.js +5 -0
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUIÉN PUEDE PEDIR QUÉ al iframe de identidad, por el origen que pone el navegador.
|
|
3
|
+
*
|
|
4
|
+
* El iframe habla con todo el ecosistema (`*.dotrino.com`, el espejo de GitHub y, en desarrollo,
|
|
5
|
+
* localhost y la red local). Eso está bien para firmar, cifrar o leer el perfil: es para lo que
|
|
6
|
+
* está. Pero hay pedidos que deciden QUIÉN ES de tu cuenta o se llevan la llave, y esos no los
|
|
7
|
+
* puede hacer cualquier página que se cargue en esos orígenes. Regla del dueño (2026-10-18):
|
|
8
|
+
* **ningún aparato entra a una cuenta sin invitación**, y una app cualquiera llamando a
|
|
9
|
+
* `admitMember` es justo eso.
|
|
10
|
+
*
|
|
11
|
+
* Esos pedidos solo los atiende la CONSOLA: `vault.dotrino.com` y `profile.dotrino.com`. Los
|
|
12
|
+
* orígenes locales valen únicamente si el propio iframe corre en local (desarrollo y pruebas):
|
|
13
|
+
* en producción, una página de localhost de tu máquina no administra tu cuenta.
|
|
14
|
+
*/
|
|
15
|
+
const LOCAL = /^https?:\/\/(localhost|127\.0\.0\.1|192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})(:\d+)?$/
|
|
16
|
+
const CONSOLE = /^https:\/\/(vault|profile)\.dotrino\.com$/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lo que solo pide la consola: entrar y salir del acta, cambiar permisos, la llave en crudo,
|
|
20
|
+
* borrar un perfil, la bóveda de esta pestaña (sus emparejamientos, sus altas con contraseña,
|
|
21
|
+
* sus respaldos) y la contraseña del perfil.
|
|
22
|
+
*/
|
|
23
|
+
export const CONSOLE_ONLY = Object.freeze([
|
|
24
|
+
'admitMember', 'removeMember', 'setCaps', 'setCapsMany', 'setLabel', 'setMemberEncPub', 'handoverMaster',
|
|
25
|
+
'renounceCaps', 'absorbRenounce', 'adoptActa', 'adoptActaChain', 'joinProfile', 'rotateSealKey', 'setSealKeyProvider',
|
|
26
|
+
'revokeDevice', 'vaultPair', 'vaultAdopt', 'vaultUnpair', 'prepareForAdoption', 'setSelfVault',
|
|
27
|
+
'exportIdentity', 'importIdentity', 'deleteProfile', 'setProfilePassword', 'removeProfilePassword',
|
|
28
|
+
'selfVaultPairing', 'selfVaultApprove', 'selfVaultLoginAdd',
|
|
29
|
+
'selfVaultBackupDrawers', 'selfVaultBackupExport', 'selfVaultBackupPlan', 'selfVaultBackupImport'
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
/** Además de la consola, quién más puede pedir uno de esos (su herramienta lo necesita). */
|
|
33
|
+
const ALSO = Object.freeze({
|
|
34
|
+
// El gestor de contraseñas vuelve a envolver sus llaves para los aparatos que entran.
|
|
35
|
+
rewrapFor: /^https:\/\/pass\.dotrino\.com$/
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export function originPolicy ({ selfOrigin }) {
|
|
39
|
+
const selfIsLocal = LOCAL.test(String(selfOrigin || ''))
|
|
40
|
+
const isConsole = (origin) => CONSOLE.test(String(origin || '')) || (selfIsLocal && LOCAL.test(String(origin || '')))
|
|
41
|
+
const only = new Set(CONSOLE_ONLY)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Lanza `console-only` si `origin` no puede pedir `method` con estos parámetros. Lo demás pasa.
|
|
45
|
+
* `me` es la llave pública del perfil activo (para el certificado de uno mismo).
|
|
46
|
+
*/
|
|
47
|
+
function check (method, params, origin, { me = null } = {}) {
|
|
48
|
+
if (isConsole(origin)) return
|
|
49
|
+
const deny = (why) => { throw Object.assign(new Error(`${method}: ${why}`), { code: 'console-only' }) }
|
|
50
|
+
if (only.has(method)) deny('only the vault console (vault.dotrino.com, profile.dotrino.com) can ask for this')
|
|
51
|
+
if (ALSO[method] && !ALSO[method].test(String(origin || ''))) deny('this origin cannot ask for this')
|
|
52
|
+
if (method === 'signDelegation') {
|
|
53
|
+
// Un certificado con poder sobre la bóveda (`vault:*`) es meter un aparato: eso es de la
|
|
54
|
+
// consola, con su invitación. Una app sí firma lo suyo (geo: los aparatos de here) y su
|
|
55
|
+
// propio certificado (`sub` = el perfil: el de terminal y el de ia).
|
|
56
|
+
const scopes = Array.isArray(params?.scope) ? params.scope : [params?.scope]
|
|
57
|
+
const vaultPower = scopes.some((s) => typeof s !== 'string' || s.startsWith('vault:'))
|
|
58
|
+
if (vaultPower && !(me && params?.sub === me)) deny('a certificate with vault powers is issued from the vault console, with an invitation')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { check, isConsole }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default { originPolicy, CONSOLE_ONLY }
|
package/vault/vault.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './peerStore.js'
|
|
15
15
|
import { createIdentityCore } from './core.js'
|
|
16
16
|
import { keyLabel } from './capabilities.js'
|
|
17
|
+
import { originPolicy } from './originPolicy.js'
|
|
17
18
|
import { withExternalKeys, appBridge } from './externalKeys.js'
|
|
18
19
|
import { nativeBackends } from './nativeStore.js'
|
|
19
20
|
import { useBackend as usePeerBackend } from './peerStore.js'
|
|
@@ -151,6 +152,7 @@ import { useBackend as usePeerBackend } from './peerStore.js'
|
|
|
151
152
|
})
|
|
152
153
|
|
|
153
154
|
const { handlers } = core
|
|
155
|
+
const policy = originPolicy({ selfOrigin: window.location.origin })
|
|
154
156
|
|
|
155
157
|
// ---- Control de ORIGEN (crítico): la identidad solo habla con el ecosistema. ----
|
|
156
158
|
// Sin esto, CUALQUIER web podía embeber este iframe y llamar `exportIdentity`
|
|
@@ -537,6 +539,9 @@ import { useBackend as usePeerBackend } from './peerStore.js'
|
|
|
537
539
|
const handler = selfHandlers[method] || handlers[method]
|
|
538
540
|
if (!handler) return reply({ error: `Unknown method: ${method}` })
|
|
539
541
|
try {
|
|
542
|
+
// LO QUE DECIDE QUIÉN ES DE LA CUENTA solo lo pide la consola (`originPolicy.js`): una app
|
|
543
|
+
// del ecosistema firma y cifra, pero no mete aparatos ni se lleva la llave.
|
|
544
|
+
policy.check(method, params, event.origin, { me: (await handlers.getMe().catch(() => null))?.publickey || null })
|
|
540
545
|
// EL ORIGEN LO PONE EL IFRAME, no quien llama: va pisado a propósito. Es el único
|
|
541
546
|
// dato que la aplicación no puede falsificar —el navegador lo garantiza— y de él
|
|
542
547
|
// depende a quién se le concedió qué.
|