❮  Controladores Entrixy

Especificação do protocolo BLE

Tudo o que é preciso para acrescentar suporte a BLE do Entrixy ao seu próprio controlador — a sua placa, o seu microcontrolador. O nosso firmware já pronto faz exatamente isto; abaixo está a descrição precisa do protocolo e os vetores de teste para se verificar byte a byte.

Um controlador BLE não precisa do servidor Entrixy. Nunca vai à internet: o telemóvel fala com ele diretamente por Bluetooth. A ligação a um proprietário é local (X25519 ECDH). Registar o aparelho no nosso servidor, obter chaves ou abrir uma conta de fabricante não é necessário — pode enviar controladores vazios. Referência: esp32-example/entrixy-ble (MIT).

1. Primitivas criptográficas

Referência: mbedTLS (MBEDTLS_ECP_DP_CURVE25519) no ESP, BouncyCastle no Android — os dois lados coincidem.

2. GATT: serviço e características

Todos os UUID são de 128 bits e baseiam-se em 656e7472-7869-7900-XXXX-426c45000001 («entrixy» em ASCII).

Sufixo do UUIDPropriedadeFinalidade
0000UUID do serviço (completo: 656e7472-7869-7900-0000-426c45000001)
0001 NONCEREADum nonce de 8 bytes (legado; um cliente moderno tira o nonce do anúncio, §3)
0002 FIREWRITEO comando de abertura: 24 bytes (proprietário) ou 47 bytes (convidado)
0003 TIMEWRITECorreção do relógio, exatamente 26 bytes
0004 WIFIWRITESSID e palavra-passe para NTP (opcional)
0005 RESULTREAD+NOTIFYUm código de resultado de um byte após cada escrita (§9)
00f0 PAIR_PUBREADSó em modo de emparelhamento: a chave pública X25519 de 32 bytes do controlador (LE)
00f1 PAIR_DONEWRITESó em modo de emparelhamento: 36 bytes [phone_pub 32B][device_id 4B LE]
O nome "entrixy" e o UUID do serviço vão para o scan-responseem vez do pacote primário: um UUID de 128 bits mais os dados de fabricante não cabem nos 31 bytes do primário. A pesquisa passiva do Android só vê o primário, por isso a filtragem faz-se pelos dados de fabricante (company id 0x00E0).

3. Anúncio GAP

A cada sleep_interval segundos, o controlador anuncia a sua presença com dados específicos do fabricante. O company id é 0x00E0 (no ar E0 00). O bloco tem 23 bytes = 2 bytes de company id mais 21 bytes de valor:

offset  size  field
[0..1]   2    Company ID = E0 00
[2..5]   4    device_id            (LE)
[6..9]   4    counter              (LE, monotonic, anti-replay, survives deep sleep)
[10..17] 8    auth_hmac            = HMAC(owner_secret, mac_in)[0..7]
[18]     1    sleep_interval_s     (plaintext)
[19..22] 4    opts_cipher          (battery, status, fw, hw — encrypted)

A assinatura cobre 13 bytes (o company id não é incluído):

mac_in = device_id(4) || counter(4) || sleep(1) || opts_cipher(4)
auth_hmac = HMAC-SHA256(owner_secret, mac_in)[0..7]
Estes 8 bytes auth_hmac servem de nonce para uma abertura imediata sem ida e volta: um observador sem owner_secret vê-os como aleatórios, ao passo que o proprietário ou o convidado calcula a assinatura e dispara de imediato. O controlador mantém um anel com os últimos 16 nonces que emitiu.

Opções (opts) — fluxo de chave XOR

ks = HMAC-SHA256(owner_secret, "ks" || device_id(4 LE) || counter(4 LE))[0..3]
opts_cipher[i] = opts_plain[i] XOR ks[i]
opts_plain[0] = battery %   [1] = status   [2] = fw (major=(b>>4)&0xF, minor=b&0xF)   [3] = hw

Modo biestável

Para um motor biestável acrescentam-se três bytes ao valor, ficando 26 bytes (ver §8).

4. Emparelhamento

Um controlador novo entra sozinho em modo de emparelhamento durante 30–90 s (caso contrário, pelo botão; a reposição de fábrica é uma pressão de 5 segundos). A troca é local, sem servidor:

  1. O telemóvel lê PAIR_PUB (00f0) — a chave pública de 32 bytes do controlador.
  2. O telemóvel gera o seu próprio par X25519, shared = X25519(phone_priv, esp_pub).
  3. Ambos os lados derivam o mesmo segredo:
    owner_secret = HKDF-SHA256(salt="entrixy-pair-v1", ikm=shared, info="owner-secret", L=32)
  4. O telemóvel escreve PAIR_DONE (00f1): [phone_pub 32B][device_id 4B LE]. o telemóvel gera o device_id, e o controlador guarda-o.
As chaves X25519 são little-endian (RFC 7748). owner_secret nunca passa pelo ar. Nenhum servidor intervém: a aplicação gerou ela própria o device_id, e é por isso que o conhece.

5. Abertura pelo proprietário (owner fire)

Uma escrita em FIRE (0002), 24 bytes:

[nonce 8B][ HMAC-SHA256(owner_secret, nonce)[0..15] 16B ]

nonce — os 8 bytes auth_hmac de um anúncio recente. O controlador verifica o nonce contra o seu anel, valida a assinatura, dá um impulso ao relé e invalida o nonce.

6. Abertura por um convidado (guest fire)

Um convidado não conhece owner_secret. O proprietário emite um pacote através do servidor: token, owner_sig, guest_key. Uma escrita em FIRE, 47 bytes: [token 7B][owner_sig 16B][nonce 8B][guest_sig 16B].

CampoTamanhoFórmula
token7B[bleId 2B LE][valid_until 4B LE unix-sec][flags 1B]
owner_sig16BHMAC-SHA256(owner_secret, token)[0..15]
nonce8Bum nonce recente do anúncio
guest_sig16BHMAC-SHA256(guest_key, nonce)[0..15]
guest_key = HKDF-SHA256(salt=null, ikm=owner_secret, info="guest" || bleId(2B LE), L=32)

O controlador: nonce no anel → owner_sig com a sua própria owner_secretvalid_until contra o seu relógio → deriva guest_keyguest_sig. Não mantém nenhuma lista de convidados — apenas verifica a assinatura.

7. Sincronização da hora

O TTL de um convidado precisa de um relógio. Qualquer cliente de confiança pode acertar a hora com uma assinatura. Uma escrita em TIME (0003), 26 bytes:

[bleId 2B LE][epoch_ms 8B LE][ HMAC-SHA256(key, payload[0..9])[0..15] 16B ]

A hora dos convidados está protegida por um roquete (não pode recuar), um teto para a frente (não mais de +24 h por escrita) e uma barreira fire-first. Um RTC externo e o NTP são opcionais.

8. Estado biestável (K_state)

K_state = HKDF-SHA256(salt=null, ikm=owner_secret, info="ble-state-v1", L=16)
[..21] state_enc = state_byte XOR K_state[counter & 15]     // bit0: 1=open, 0=closed
[22..23] state_mac = HMAC-SHA256(K_state, counter(4B LE) || state_enc)[0..1]   // 2 bytes

9. Códigos de resposta (RESULT, característica 0005)

CódigoValorCódigoValor
0x00FIRE OK0x10TIME OK (owner)
0x30FIRE OK — ABERTO0x11TIME OK (guest)
0x31FIRE OK — FECHADO0x12TIME tem um comprimento incorreto
0x01o nonce está velho ou não está no anel0x13o HMAC de TIME não coincidiu
0x02o TTL expirou0x15TIME recuou (roquete)
0x03owner_sig é inválida0x16TIME ultrapassa o teto de +24 h
0x04guest_sig é inválida0x17TIME sem uma abertura recente
0x05comprimento incorreto0x20WIFI OK
0x06HKDF failed0x21WIFI tem um formato incorreto
0x07o HMAC do proprietário é inválido0x22o HMAC de WIFI não coincidiu

10. Vetores de teste

Vetores determinísticos com resposta conhecida: execute a sua implementação e compare os bytes. Todos os valores são hexadecimais e a ordem dos bytes é a do ar. A cadeia corre de ponta a ponta: emparelhamento → owner_secret → tudo o resto.

Pairing (X25519 → owner_secret)

esp_priv   (clamped) = 0002030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f60
phone_priv (clamped) = 201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030241
esp_pub              = 07a37cbc142093c8b755dc1b10e86cb426374ad16aa853ed0bdfc0b2b86d1c7c
phone_pub            = 0d799600f6ffaee2e121e6b8f7a05dc66874b51db3102d0d71f799a09cb4c461
shared_z             = 53126e95ac6e407e8a412fdf82c87f1be45a2251edf9422ad00df2e83aaebd19
owner_secret         = 3609fa67bd15cf2bbaecdd5305feea0d48e1f21d714d01b29082a5fec459c64b
  = HKDF(salt="entrixy-pair-v1", ikm=shared_z, info="owner-secret", L=32)

Advertise (did=0x0A0B0C0D, counter=7, sleep=1, opts=batt100/status1/fw v2.3/hw5)

ks[0..3]     = ee0a8499
opts_cipher  = 8a0ba79c
auth_hmac    = 1f2d8012178a6e06   (= nonce)
mfg    (23B) = e0000d0c0b0a070000001f2d8012178a6e06018a0ba79c

Owner fire (24B)

fire = 1f2d8012178a6e06eaafaae40500580a774b62be6e9b6aa2
  = nonce || HMAC(owner_secret, nonce)[0..15]

Guest fire (47B) — bleId=0x0042, valid_until=0x6890ABCD, flags=0x01

token     (7B)  = 4200cdab906801
owner_sig (16B) = faa3a99a76dfba0521fb4eb4bd508559   = HMAC(owner_secret, token)[0..15]
guest_key (32B) = 248b57d94ccafeb3cba598c6b81fc18617c1b54da1386ca1ed99bcd5dd012ff5
  = HKDF(salt=null, ikm=owner_secret, info="guest"||bleId_LE, L=32)
guest_sig (16B) = 7a6e02ae269827b78b7c49840c40476e   = HMAC(guest_key, nonce)[0..15]
fire      (47B) = 4200cdab906801faa3a99a76dfba0521fb4eb4bd5085591f2d8012178a6e067a6e02ae269827b78b7c49840c40476e

Time-sync (26B) — owner (bleId=0), epoch_ms=1750000000000

time = 000000dc2074970100005e1120cdb0bb4c8f5ee22f2b1920cb99
  = [bleId 2B LE][epoch_ms 8B LE][ HMAC(owner_secret, payload)[0..15] ]

K_state (16B)

K_state = 19bcfc41f61a103d79e9967c21403a93
  = HKDF(salt=null, ikm=owner_secret, info="ble-state-v1", L=16)

Código de referência e contacto

Uma implementação funcional dos dois lados é esp32-example/entrixy-ble (MIT). Para compilar .bin para a sua própria placa, use o configurador no navegador. O transporte online é protocolo de socket. Como isto se enquadra na arquitetura aberta: /open. Pedidos OEM: hello@entrixy.com.