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.
1. Primitivas criptográficas
- HMAC-SHA256 — assinaturas e derivação. O truncamento a um dado comprimento toma os primeiros N bytes do resultado.
- HKDF-SHA256 (RFC 5869) — derivação de chaves. Se
salt = null, é usado um vetor de zeros de 32 bytes. - X25519 (RFC 7748) — ECDH durante o emparelhamento. A chave privada é ajustada (clamping):
k[0] &= 0xF8; k[31] = (k[31] & 0x7F) | 0x40.
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 UUID | Propriedade | Finalidade |
|---|---|---|
0000 | — | UUID do serviço (completo: 656e7472-7869-7900-0000-426c45000001) |
0001 NONCE | READ | um nonce de 8 bytes (legado; um cliente moderno tira o nonce do anúncio, §3) |
0002 FIRE | WRITE | O comando de abertura: 24 bytes (proprietário) ou 47 bytes (convidado) |
0003 TIME | WRITE | Correção do relógio, exatamente 26 bytes |
0004 WIFI | WRITE | SSID e palavra-passe para NTP (opcional) |
0005 RESULT | READ+NOTIFY | Um código de resultado de um byte após cada escrita (§9) |
00f0 PAIR_PUB | READ | Só em modo de emparelhamento: a chave pública X25519 de 32 bytes do controlador (LE) |
00f1 PAIR_DONE | WRITE | Só em modo de emparelhamento: 36 bytes [phone_pub 32B][device_id 4B LE] |
"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]
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:
- O telemóvel lê
PAIR_PUB(00f0) — a chave pública de 32 bytes do controlador. - O telemóvel gera o seu próprio par X25519,
shared = X25519(phone_priv, esp_pub). - Ambos os lados derivam o mesmo segredo:
owner_secret = HKDF-SHA256(salt="entrixy-pair-v1", ikm=shared, info="owner-secret", L=32)
- 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.
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].
| Campo | Tamanho | Fórmula |
|---|---|---|
token | 7B | [bleId 2B LE][valid_until 4B LE unix-sec][flags 1B] |
owner_sig | 16B | HMAC-SHA256(owner_secret, token)[0..15] |
nonce | 8B | um nonce recente do anúncio |
guest_sig | 16B | HMAC-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_secret → valid_until contra o seu relógio → deriva guest_key → guest_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 ]
bleId = 0→ proprietário,key = owner_secret.bleId ≠ 0→ convidado,key = guest_key(como no §6).
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ódigo | Valor | Código | Valor |
|---|---|---|---|
0x00 | FIRE OK | 0x10 | TIME OK (owner) |
0x30 | FIRE OK — ABERTO | 0x11 | TIME OK (guest) |
0x31 | FIRE OK — FECHADO | 0x12 | TIME tem um comprimento incorreto |
0x01 | o nonce está velho ou não está no anel | 0x13 | o HMAC de TIME não coincidiu |
0x02 | o TTL expirou | 0x15 | TIME recuou (roquete) |
0x03 | owner_sig é inválida | 0x16 | TIME ultrapassa o teto de +24 h |
0x04 | guest_sig é inválida | 0x17 | TIME sem uma abertura recente |
0x05 | comprimento incorreto | 0x20 | WIFI OK |
0x06 | HKDF failed | 0x21 | WIFI tem um formato incorreto |
0x07 | o HMAC do proprietário é inválido | 0x22 | o 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.