Entrixy — cifragem ponta a ponta

Uma arquitetura de conhecimento zero: o servidor não tem chaves de decifragem para nada além dos identificadores técnicos. Todos os campos com significado — números de telefone, endereços, coordenadas, segredos de webhook, BSSID de Wi-Fi, saudações — são cifrados no dispositivo do proprietário e ficam no servidor como um blob opaco. A chave de decifragem chega ao convidado dentro da ligação de convite, no fragmento do endereço, que o navegador nunca envia ao servidor.

Decidido a 2026-04-19. O trabalho avança por etapas — ver a secção Fases.

1. Objetivos e limites

2. Primitiva criptográfica

Algorithm: AES-256-GCM. Disponível de origem na JVM (através de javax.crypto), e suportado pelo Android Keystore. Dá em simultâneo confidencialidade e autenticação.

Formato do texto cifrado

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

Fonte de entropia

SecureRandom() (o do sistema). No Android é /dev/urandom + o prng do Linux.

3. Hierarquia de chaves

K_master — a chave-mestra do proprietário

K_obj — a chave do objeto

K_guest — a chave do pacote de convidado

Changing K_guest só é possível emitindo uma nova ligação. É nisso que assenta a revogação: o servidor apaga o registo da chave e o convidado deixa de receber um bundle_cipher atual.

4. O que se guarda onde

No dispositivo do proprietário (EncryptedSharedPreferences)

KeyValor
master_key32 bytes em claro (o SharedPreferences cifra-os de qualquer modo).
obj_key_{id}A K_obj do objeto, envolvida com K_master.
obj_plain_{id}(opcional) uma cache do JSON decifrado, para a interface desenhar depressa.

No servidor

Tabela / campoConteúdo
numbers.data_cipherJSON cifrado com K_obj: {phone, url, secret, geo_lat, geo_lon, share_lat, share_lon, snapshot, welcome, label}.
numbers.id / typeEm claro — necessário para o encaminhamento e para mostrar o tipo de ícone.
keys.bundle_cipherJSON cifrado com K_guest: {obj_ids: [1,2,3], obj_keys: {1: K_obj1, 2: K_obj2, ...}, welcome_cipher: ..., ...}.
keys.user_key / mode / force_when_busyEm claro — para o encaminhamento e uma verificação rápida do acesso.
A saudação ao convidado é cifrada à parte dentro do pacote, por isso o servidor também não a vê.
https://entrixy.com/key#<user_key(32)><K_guest_base64url(43)>

6. Fluxos principais

Criar um objeto

  1. O proprietário carrega em Adicionar — gera-se K_obj.
  2. Os campos são empacotados em JSON e cifrados com K_objdata_cipher.
  3. POST /api/number_add com data_cipher mais o tipo. O servidor devolve um id.
  4. Guarda-se localmente: obj_key_{id} = encrypt_k_master(K_obj).

Atualizar um objeto

Construir o novo JSON localmente, cifrá-lo com a K_objPOST /api/number_update. K_obj existente, que não muda.

Criar uma chave de convidado

  1. O proprietário escolheu [obj1, obj2, obj3]. Gera-se K_guest.
  2. Monta-se o JSON do pacote: {obj_ids: [...], obj_keys: {1: K_obj1, ...}, welcome_cipher: ...}.
  3. Cifrado com K_guestbundle_cipher.
  4. POST /api/key_create com bundle_cipher, obj_ids (em claro — para a matriz de acesso).
  5. O servidor devolve user_key.
  6. Ao proprietário mostra-se um código QR com a ligação entrixy.com/key#{user_key}{base64url(K_guest)}.

O convidado aceita a chave

  1. O convidado abre o código QR ou a ligação. A página /key reads location.hash e entrega a chave à aplicação ou à versão web.
  2. O cliente pede POST /api/key_bundle.php com user_key → recebe bundle_cipher e a lista de obj_ids.
  3. Decifra o pacote com K_guest→ obtendo {obj_keys, welcome}.
  4. K_guest e os decifrados obj_keys são guardados em EncryptedSharedPreferences do cliente convidado.
  5. A partir daí, a cada chamada: GET do cifrado data_cipher do objeto → decifragem com a obj_key_{id}.

Revogar uma chave de convidado

O proprietário carrega em Apagar → POST /api/key_revoke.php → o servidor apaga o registo da chave. No pedido seguinte do pacote o convidado é recusado e perde o acesso.

Rodar a chave de um objeto

O proprietário carrega em Regenerar chave nas definições do objeto. Gera-se K_obj_new, e voltam a cifrar-se todos os bundle_cipher que contêm esse objeto. O cliente não os sabe de cor, por isso o cliente do proprietário reconstrói os pacotes: vai buscar o bundle_cipher de cada uma das suas chaves, decifra-o, insere K_obj_newe volta a cifrar. Os novos blobs seguem para o servidor num só lote.

7. Cópia de segurança e restauro

Esta secção descreve um plano que ainda não está implementado: hoje não há exportação da chave-mestra na aplicação.

Losing K_master significa perder o acesso a todos os objetos e chaves, e por isso a exportação é indispensável.

Sem frase-passe, o código QR de cópia é apenas K_master, por isso a frase-passe é obrigatória.

8. Fases de implantação

Fase 1 — o cofre criptográfico no cliente

Add Crypto.kt: um invólucro AES-256-GCM, geração e armazenamento de K_master, auxiliares para K_obj, serialização do formato v1:<base64url>. Testes unitários: cifrar → decifrar → comparar. Para o servidor ainda não vai nada — a canalização está apenas pronta.

Fase 2 — migração do esquema do servidor (feito)

Todos os campos sensíveis mudaram-se para numbers.data_cipher e user_keys.bundle_cipher. As antigas colunas em claro (phone, label, radius, time_*, geo_*, wifi_*, share_*, has_avatar, security_level, user_keys.label, hosts.label, pending_actions.phone, hosts.last_ip) foram removidas da base (fase 7). webhook_url e webhook_secret ficam em claro SÓ quando webhook_mode='server' — sem eles o servidor não consegue enviar o pedido HTTP. Com webhook_mode='phone' são cifrados em data_cipher.

Fase 3 — o cliente lê e escreve o blob

Ao criar ou atualizar, o cliente cifra o JSON e envia o data_cipher. Ao sincronizar lê o blob e decifra-o. Os objetos novos vivem inteiramente no blob; os antigos sem K_obj continuam a usar os campos em claro por retrocompatibilidade.

Fase 4 — o pacote de convidado

Criar uma chave gera K_guest, monta o pacote e envia bundle_cipherpara o servidor. A ligação leva a chave no fragmento; a página /key lê o hash e passa a chave à aplicação.

Fase 5 — cópia de segurança e restauro

Um ecrã de definições com Exportar chave-mestra: frase-passe → Argon2id → código QR. Restauro pelo leitor, mais testes de mudança entre dispositivos. Não implementado.

Fase 6 — migração dos dados existentes

Uma rotina única no cliente no primeiro arranque da nova versão:

  1. Ler todos os seus objetos do servidor (os antigos campos em claro).
  2. Gerar uma K_obj para cada um deles.
  3. Cifrar os campos e enviar data_cipher.
  4. O servidor limpa as colunas antigas (phone=NULL, …) — mas só após confirmação.
Depois de a migração correr bem, os campos obsoletos são removidos com um ALTER.

Fase 7 — auditoria

Uma auditoria de segurança, externa ou nossa. Verificamos que o servidor realmente não vê dados sensíveis: um despejo da base não deve conter nada além de blobs.

9. Riscos e medidas

RiskMeasure
Perda de K_masterUm fluxo de cópia de segurança obrigatório (fase 5). Enquanto não houver cópia, mostra-se um aviso.
Comprometimento do dispositivo do proprietárioNão é totalmente evitável: a entrada na aplicação é protegida por PIN ou biometria, com EncryptedSharedPreferences e o Android Keystore.
Rodar a chave de um objeto obriga a reconstruir todos os pacotesO cliente do proprietário pega em todos os bundle_cipher, locais ou do servidor, reconstrói-os e envia-os num só lote. É uma operação rara.
O servidor adultera um pacoteA etiqueta AES-GCM não confere e o cliente avisa de uma chave danificada.
Fuga de K_guest por uma captura de ecrã do código QRTecnicamente inevitável: a chave está no próprio código. Mostre o código QR apenas a um convidado de confiança.
O fragmento do endereço fica no histórico do navegadorA página /key chama, logo após ler o hash, history.replaceState(..., '#'), o que apaga o fragmento.

Última atualização: 19 de abril de 2026