Entrixy — cifrado de extremo a extremo

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.

Decidido el 2026-04-19. El trabajo avanza por etapas: véase la sección Fases.

1. Objetivos y límites

2. Primitiva criptográfica

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.

Formato del texto cifrado

v1:<base64url(nonce || ciphertext || tag)>

Fuente de entropía

SecureRandom() (el del sistema). En Android eso es /dev/urandom + el prng de Linux.

3. Jerarquía de claves

K_master: la clave maestra del propietario

K_obj: la clave del objeto

K_guest: la clave del paquete de invitado

Changing 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.

4. Qué se guarda dónde

En el dispositivo del propietario (EncryptedSharedPreferences)

KeyValor
master_key32 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.

En el servidor

Tabla / campoContenido
numbers.data_cipherJSON cifrado con K_obj: {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}.
numbers.id / typeEn claro: hace falta para el enrutado y para mostrar el tipo de icono.
keys.bundle_cipherJSON 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_busyEn claro: para el enrutado y una comprobación rápida del acceso.
El saludo al invitado se cifra aparte dentro del paquete, así que el servidor tampoco lo ve.
https://entrixy.com/key#<user_key(32)><K_guest_base64url(43)>

6. Flujos principales

Crear un objeto

  1. El propietario pulsa Añadir: se genera K_obj.
  2. Los campos se empaquetan en JSON y se cifran con K_objdata_cipher.
  3. POST /api/number_add con data_cipher más el tipo. El servidor devuelve un id.
  4. Se guarda localmente: obj_key_{id} = encrypt_k_master(K_obj).

Actualizar un objeto

Construir el nuevo JSON localmente, cifrarlo con la K_objPOST /api/number_update. K_obj existente, que no cambia.

Crear una clave de invitado

  1. El propietario ha elegido [obj1, obj2, obj3]. Se genera K_guest.
  2. Se ensambla el JSON del paquete: {obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.
  3. Se cifra con K_guestbundle_cipher.
  4. POST /api/key_create con bundle_cipher, obj_ids (en claro: para la matriz de acceso).
  5. El servidor devuelve user_key.
  6. Al propietario se le muestra un código QR con el enlace entrixy.com/key#{user_key}{base64url(K_guest)}.

El invitado acepta la clave

  1. El invitado abre el código QR o el enlace. La página /key reads location.hash y entrega la clave a la aplicación o a la versión web.
  2. El cliente pide POST /api/key_bundle.php con user_key → recibe bundle_cipher y la lista de obj_ids.
  3. Descifra el paquete con K_guest→ obteniendo {obj_keys, welcome}.
  4. K_guest y los descifrados obj_keys se guardan en EncryptedSharedPreferences del cliente invitado.
  5. A partir de ahí, en cada llamada: GET del cifrado data_cipher del objeto → descifrado con la obj_key_{id}.

Revocar una clave de invitado

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.

Rotar la clave de un objeto

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.

7. Copia de seguridad y restauración

Esta sección describe un plan que todavía no está implementado: hoy no hay exportación de la clave maestra en la aplicación.

Losing K_master significa perder el acceso a todos los objetos y claves; por eso la exportación es imprescindible.

Sin frase de paso, el código QR de respaldo es sencillamente K_master, así que la frase de paso es obligatoria.

8. Fases de despliegue

Fase 1: el almacén criptográfico del cliente

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.

Fase 2: migración del esquema del servidor (hecho)

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.

Fase 3: el cliente lee y escribe el blob

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.

Fase 4: el paquete de invitado

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.

Fase 5: copia de seguridad y restauració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.

Fase 6: migración de los datos existentes

Una rutina única en el cliente al primer arranque de la versión nueva:

  1. Leer todos sus objetos del servidor (los antiguos campos en claro).
  2. Generar una K_obj para cada uno de ellos.
  3. Cifrar los campos y enviar data_cipher.
  4. El servidor limpia las columnas antiguas (phone=NULL, …), pero solo tras la confirmación.
Tras una migración correcta, los campos obsoletos se eliminan con un ALTER.

Fase 7: auditoría

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.

9. Riesgos y medidas

RiskMeasure
Pérdida de K_masterUn flujo de copia de seguridad obligatorio (fase 5). Mientras no haya copia, se muestra un aviso.
Compromiso del dispositivo del propietarioNo 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 paquetesEl 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 paqueteLa 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 QRTé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 navegadorLa 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