Una arquitectura de conocimiento cero: el servidor no tiene claves de descifrado para nada salvo los identificadores técnicos. Todo campo con significado —números de teléfono, direcciones, coordenadas, secretos de webhook, BSSID de Wi-Fi, saludos— se cifra en el dispositivo del propietario y reposa en el servidor como un blob opaco. La clave de descifrado llega al invitado dentro del enlace de invitación, en el fragmento de la dirección, que el navegador nunca envía al servidor.
Algorithm: AES-256-GCM. Disponible de serie en la JVM (mediante javax.crypto),
y con soporte del Android Keystore. Da a la vez confidencialidad y autenticación.
v1:<base64url(nonce || ciphertext || tag)>
v1: — un prefijo de versión, para cambiar el formato más adelante.nonce — 12 bytes aleatorios, nuevos en cada cifrado.ciphertext+tag — todo lo que devuelve Cipher.doFinal; la etiqueta de 128 bits está incluida.SecureRandom() (el del sistema). En Android eso es /dev/urandom + el prng de Linux.
EncryptedSharedPreferences con el nombre master_key y nunca sale del dispositivo en claro.K_obj para el almacenamiento local.encrypt_k_master(K_obj) bajo la clave obj_key_{id}.data_cipher en el servidor).K_obj_new → volver a cifrar data_cipher y todos los paquetes que lo llevaban.{obj_id: K_obj} con los objetos permitidos al invitado.bundle_cipher. El K_guest en sí el servidor no lo ve nunca.entrixy.com/key#<user_key><base64url(K_guest)> — un único bloque de 75 caracteres.
El fragmento no viaja al servidor con la petición HTTP: es una garantía básica del navegador.K_guest solo es posible emitiendo un enlace nuevo. En eso mismo se basa la revocación:
el servidor borra el registro de la clave y el invitado deja de recibir un bundle_cipher actual.| Key | Valor |
|---|---|
master_key | 32 bytes en claro (SharedPreferences los cifra de todos modos). |
obj_key_{id} | La K_obj del objeto, envuelta con K_master. |
obj_plain_{id} | (opcional) una caché del JSON descifrado, para que la interfaz se dibuje rápido. |
| Tabla / campo | Contenido |
|---|---|
numbers.data_cipher | JSON cifrado con K_obj: {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}. |
numbers.id / type | En claro: hace falta para el enrutado y para mostrar el tipo de icono. |
keys.bundle_cipher | JSON cifrado con K_guest: {obj_ids: [1,2,3], obj_keys: {1: K_obj1, 2: K_obj2, ...}, welcome_cipher: ..., ...}. |
keys.user_key / mode / force_when_busy | En claro: para el enrutado y una comprobación rápida del acceso. |
https://entrixy.com/key#<user_key(32)><K_guest_base64url(43)>
user_key y 43 caracteres de la clave de invitado.user_key — el identificador del lado del servidor. Sin él, el servidor no entrega el paquete.#...) nunca llega al registro del servidor — es una garantía del navegador./key — JS corriente: lee location.hash, analiza el bloque y entrega la clave
a la aplicación o a la versión web /app/. El servidor no ve nunca la clave./download/android,
desde donde la aplicación recoge la clave por sí misma.K_obj.K_obj → data_cipher.POST /api/number_add con data_cipher más el tipo. El servidor devuelve un id.obj_key_{id} = encrypt_k_master(K_obj).Construir el nuevo JSON localmente, cifrarlo con la K_obj → POST /api/number_update.
K_obj existente, que no cambia.
[obj1, obj2, obj3]. Se genera K_guest.{obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.K_guest → bundle_cipher.POST /api/key_create con bundle_cipher, obj_ids (en claro: para la matriz de acceso).user_key.entrixy.com/key#{user_key}{base64url(K_guest)}./key reads location.hash y entrega la clave a la aplicación o a la versión web.POST /api/key_bundle.php con user_key → recibe bundle_cipher y la lista de obj_ids.K_guest→ obteniendo {obj_keys, welcome}.K_guest y los descifrados obj_keys se guardan en EncryptedSharedPreferences del cliente invitado.data_cipher del objeto → descifrado con la obj_key_{id}.El propietario pulsa Eliminar → POST /api/key_revoke.php → el servidor borra el registro de la clave.
En la siguiente petición del paquete el invitado recibe una negativa y pierde el acceso.
El propietario pulsa Regenerar clave en los ajustes del objeto. Se genera K_obj_new,
y se vuelven a cifrar todos los bundle_cipher que contienen ese objeto. El cliente no los conoce de
memoria, así que el cliente del propietario reconstruye los paquetes: recoge el bundle_cipher de cada
una de sus claves, lo descifra, inserta K_obj_newy vuelve a cifrar.
Los nuevos blobs van al servidor en un solo lote.
Losing K_master significa perder el acceso a todos los objetos y claves; por eso la exportación es imprescindible.
K_master → y produce master_backup_blob.obj_keys del servidor —están en el paquete de las propias claves del propietario— y los vuelve a cifrar localmente.K_master, así que la frase de paso es obligatoria.Add Crypto.kt: una envoltura AES-256-GCM, generación y almacenamiento de K_master,
ayudantes para K_obj, serialización del formato v1:<base64url>.
Pruebas unitarias: cifrar → descifrar → comparar.
Al servidor todavía no va nada: solo está lista la fontanería.
Todos los campos sensibles se han mudado a numbers.data_cipher y user_keys.bundle_cipher.
Las antiguas columnas en claro (phone, label, radius, time_*, geo_*, wifi_*, share_*, has_avatar, security_level, user_keys.label, hosts.label, pending_actions.phone, hosts.last_ip) se han eliminado de la base (fase 7).
webhook_url y webhook_secret quedan en claro SOLO cuando webhook_mode='server' — sin ellos el servidor no puede enviar la petición HTTP. Con webhook_mode='phone' se cifran en data_cipher.
Al crear o actualizar, el cliente cifra el JSON y envía data_cipher. Al sincronizar lee el blob y lo descifra. Los objetos nuevos viven enteros en el blob; los viejos sin K_obj siguen usando los campos en claro por compatibilidad.
Crear una clave genera K_guest, ensambla el paquete y envía bundle_cipheral servidor.
El enlace lleva la clave en su fragmento; la página /key lee el hash y pasa la clave a la aplicación.
Una pantalla de ajustes con Exportar clave maestra: frase de paso → Argon2id → código QR. Restauración por el escáner, más pruebas de mudanza entre dispositivos. No implementado.
Una rutina única en el cliente al primer arranque de la versión nueva:
K_obj para cada uno de ellos.data_cipher.phone=NULL, …), pero solo tras la confirmación.Una auditoría de seguridad, externa o propia. Comprobamos que el servidor de verdad no ve los datos sensibles: un volcado de la base no debe contener más que blobs.
| Risk | Measure |
|---|---|
| Pérdida de K_master | Un flujo de copia de seguridad obligatorio (fase 5). Mientras no haya copia, se muestra un aviso. |
| Compromiso del dispositivo del propietario | No es del todo evitable: la entrada a la aplicación se protege con PIN o biometría, con EncryptedSharedPreferences y el Android Keystore. |
| Rotar la clave de un objeto obliga a reconstruir todos los paquetes | El cliente del propietario toma todos los bundle_cipher, locales o del servidor, los reconstruye y los envía en un solo lote. Es una operación rara. |
| El servidor manipula un paquete | La etiqueta AES-GCM no cuadrará y el cliente avisará de una clave dañada. |
| Fuga de K_guest por una captura de pantalla del código QR | Técnicamente inevitable: la clave está en el propio código. Enseña el código QR solo a un invitado en quien confíes. |
| El fragmento de la dirección queda en el historial del navegador | La página /key llama, justo después de leer el hash, a history.replaceState(..., '#'), lo que borra el fragmento. |
Última actualización: 19 de abril de 2026