❮  Entrixy-Controller

Spezifikation des BLE-Protokolls

Alles, was Sie brauchen, um Ihrem eigenen Controller — Ihrer Platine, Ihrem Mikrocontroller — Entrixy-BLE-Unterstützung zu geben. Unsere fertige Firmware macht genau das; unten stehen die genaue Protokollbeschreibung und Testvektoren, damit Sie sich Byte für Byte prüfen können.

Ein BLE-Controller braucht den Entrixy-Server nicht. Er geht überhaupt nie online: Das Telefon spricht direkt über Bluetooth mit ihm. Die Bindung an einen Besitzer erfolgt lokal (X25519 ECDH). Das Gerät bei unserem Server anzumelden, Schlüssel zu beziehen oder ein Herstellerkonto zu eröffnen, ist nicht erforderlich — Sie können leere Controller ausliefern. Referenz: esp32-example/entrixy-ble (MIT).

1. Kryptografische Primitive

Referenz: mbedTLS (MBEDTLS_ECP_DP_CURVE25519) auf dem ESP, BouncyCastle auf Android — beide Seiten stimmen überein.

2. GATT: Dienst und Merkmale

Alle UUIDs sind 128 Bit lang und basieren auf 656e7472-7869-7900-XXXX-426c45000001 („entrixy“ in ASCII).

UUID-SuffixEigenschaftZweck
0000Service-UUID (vollständig: 656e7472-7869-7900-0000-426c45000001)
0001 NONCEREADeine 8-Byte-Nonce (veraltet; ein moderner Client nimmt die Nonce aus dem Advertising, §3)
0002 FIREWRITEDer Öffnungsbefehl: 24 Byte (Besitzer) oder 47 Byte (Gast)
0003 TIMEWRITEUhrzeitkorrektur, genau 26 Byte
0004 WIFIWRITESSID und Passwort für NTP (optional)
0005 RESULTREAD+NOTIFYEin Ein-Byte-Ergebniscode nach jedem Schreibvorgang (§9)
00f0 PAIR_PUBREADNur im Kopplungsmodus: der 32 Byte lange öffentliche X25519-Schlüssel des Controllers (LE)
00f1 PAIR_DONEWRITENur im Kopplungsmodus: 36 Byte [phone_pub 32B][device_id 4B LE]
Der Name "entrixy" und die Service-UUID wandern in das scan-responsestatt in das Primärpaket: Eine 128-Bit-UUID samt Herstellerdaten passt nicht in die 31 Byte des Primärpakets. Passives Scannen unter Android sieht nur das Primärpaket, gefiltert wird deshalb nach den Herstellerdaten (Company ID 0x00E0).

3. GAP-Advertising

Alle sleep_interval Sekunden meldet der Controller seine Anwesenheit mit herstellerspezifischen Daten. Die Company ID lautet 0x00E0 (auf der Leitung E0 00). Der Block ist 23 Byte = 2 Byte Company ID plus 21 Byte Nutzdaten:

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)

Die Signatur erstreckt sich über 13 Byte (die Company ID zählt nicht mit):

mac_in = device_id(4) || counter(4) || sleep(1) || opts_cipher(4)
auth_hmac = HMAC-SHA256(owner_secret, mac_in)[0..7]
Diese 8 Byte auth_hmac dienen als nonce für ein sofortiges Öffnen ohne Rückfrage: Ein Beobachter ohne owner_secret sieht darin nur Zufall, während Besitzer oder Gast die Signatur berechnen und sofort auslösen. Der Controller führt einen Ring der letzten 16 von ihm ausgegebenen Nonces.

Optionen (opts) — XOR-Schlüsselstrom

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

Bistabiler Modus

Bei einem bistabilen Antrieb werden drei Byte an die Nutzdaten angehängt, sodass sich 26 Byte ergeben (siehe §8).

4. Kopplung

Ein neuer Controller geht von selbst für 30–90 s in den Kopplungsmodus (sonst per Taste; ein Zurücksetzen auf Werkseinstellungen ist ein 5-Sekunden-Druck). Der Austausch läuft lokal, ohne Server:

  1. Das Telefon liest PAIR_PUB (00f0) — den 32 Byte langen öffentlichen Schlüssel des Controllers.
  2. Das Telefon erzeugt sein eigenes X25519-Paar, shared = X25519(phone_priv, esp_pub).
  3. Beide Seiten leiten dasselbe Geheimnis ab:
    owner_secret = HKDF-SHA256(salt="entrixy-pair-v1", ikm=shared, info="owner-secret", L=32)
  4. Das Telefon schreibt PAIR_DONE (00f1): [phone_pub 32B][device_id 4B LE]. das Telefon erzeugt die device_id, und der Controller speichert sie.
X25519-Schlüssel sind Little-Endian (RFC 7748). owner_secret geht nie über die Luft. Kein Server ist beteiligt: Die App hat die device_id selbst erzeugt und kennt sie deshalb.

5. Öffnen durch den Besitzer (owner fire)

Ein Schreibvorgang auf FIRE (0002), 24 Byte:

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

nonce — die 8 Byte auth_hmac aus einem frischen Advertising. Der Controller prüft die Nonce gegen seinen Ring, verifiziert die Signatur, gibt einen Impuls auf das Relais und entwertet die Nonce.

6. Öffnen durch einen Gast (guest fire)

Ein Gast kennt nicht owner_secret. Der Besitzer stellt über den Server ein Bündel aus: token, owner_sig, guest_key. Ein Schreibvorgang auf FIRE, 47 Byte: [token 7B][owner_sig 16B][nonce 8B][guest_sig 16B].

FeldGrößeFormel
token7B[bleId 2B LE][valid_until 4B LE unix-sec][flags 1B]
owner_sig16BHMAC-SHA256(owner_secret, token)[0..15]
nonce8Beine frische Nonce aus dem Advertising
guest_sig16BHMAC-SHA256(guest_key, nonce)[0..15]
guest_key = HKDF-SHA256(salt=null, ikm=owner_secret, info="guest" || bleId(2B LE), L=32)

Der Controller: Nonce im Ring → owner_sig mit seinem eigenen owner_secretvalid_until gegen seine Uhr → leitet guest_keyguest_sigab. Es führt keine Gästeliste — es prüft nur die Signatur.

7. Zeitsynchronisation

Ein Gast-TTL braucht eine Uhr. Jeder vertrauenswürdige Client kann die Zeit mit einer Signatur setzen. Ein Schreibvorgang auf TIME (0003), 26 Byte:

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

Die Gastzeit ist durch eine Ratsche (kein Rückwärtsgehen), eine Obergrenze nach vorn (höchstens +24 h pro Schreibvorgang) und ein Fire-First-Gate geschützt. Eine externe RTC und NTP sind optional.

8. Bistabiler Zustand (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. Antwortcodes (RESULT, Merkmal 0005)

CodeWertCodeWert
0x00FIRE OK0x10TIME OK (owner)
0x30FIRE OK — OFFEN0x11TIME OK (guest)
0x31FIRE OK — GESCHLOSSEN0x12TIME hat die falsche Länge
0x01die Nonce ist veraltet oder nicht im Ring0x13der HMAC von TIME stimmte nicht
0x02die TTL ist abgelaufen0x15TIME lief rückwärts (Ratsche)
0x03owner_sig ist ungültig0x16TIME überschreitet die Grenze von +24 h
0x04guest_sig ist ungültig0x17TIME ohne kürzliches Auslösen
0x05falsche Länge0x20WIFI OK
0x06HKDF failed0x21WIFI hat das falsche Format
0x07der HMAC des Besitzers ist ungültig0x22der HMAC von WIFI stimmte nicht

10. Testvektoren

Deterministische Vektoren mit bekannten Ergebnissen: Lassen Sie Ihre Umsetzung laufen und vergleichen Sie die Bytes. Alle Werte sind hexadezimal, die Byte-Reihenfolge ist wie auf der Leitung. Die Kette läuft durchgehend: Kopplung → owner_secret → alles Weitere.

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)

Referenzcode und Kontakt

Eine funktionierende Umsetzung beider Seiten ist esp32-example/entrixy-ble (MIT). Um .bin für Ihre eigene Platine zu bauen, nutzen Sie den Konfigurator im Browser. Der Online-Transport ist Socket-Protokoll. Wie sich das in die offene Architektur einfügt: /open. OEM-Anfragen: hello@entrixy.com.