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.
1. Kryptografische Primitive
- HMAC-SHA256 — Signaturen und Ableitung. Beim Kürzen auf eine bestimmte Länge werden die ersten N Bytes des Ergebnisses genommen.
- HKDF-SHA256 (RFC 5869) — Schlüsselableitung. Wenn
salt = null, wird ein Nullvektor von 32 Byte verwendet. - X25519 (RFC 7748) — ECDH bei der Kopplung. Der private Schlüssel wird geklemmt:
k[0] &= 0xF8; k[31] = (k[31] & 0x7F) | 0x40.
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-Suffix | Eigenschaft | Zweck |
|---|---|---|
0000 | — | Service-UUID (vollständig: 656e7472-7869-7900-0000-426c45000001) |
0001 NONCE | READ | eine 8-Byte-Nonce (veraltet; ein moderner Client nimmt die Nonce aus dem Advertising, §3) |
0002 FIRE | WRITE | Der Öffnungsbefehl: 24 Byte (Besitzer) oder 47 Byte (Gast) |
0003 TIME | WRITE | Uhrzeitkorrektur, genau 26 Byte |
0004 WIFI | WRITE | SSID und Passwort für NTP (optional) |
0005 RESULT | READ+NOTIFY | Ein Ein-Byte-Ergebniscode nach jedem Schreibvorgang (§9) |
00f0 PAIR_PUB | READ | Nur im Kopplungsmodus: der 32 Byte lange öffentliche X25519-Schlüssel des Controllers (LE) |
00f1 PAIR_DONE | WRITE | Nur im Kopplungsmodus: 36 Byte [phone_pub 32B][device_id 4B LE] |
"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]
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:
- Das Telefon liest
PAIR_PUB(00f0) — den 32 Byte langen öffentlichen Schlüssel des Controllers. - Das Telefon erzeugt sein eigenes X25519-Paar,
shared = X25519(phone_priv, esp_pub). - Beide Seiten leiten dasselbe Geheimnis ab:
owner_secret = HKDF-SHA256(salt="entrixy-pair-v1", ikm=shared, info="owner-secret", L=32)
- Das Telefon schreibt
PAIR_DONE(00f1):[phone_pub 32B][device_id 4B LE]. das Telefon erzeugt die device_id, und der Controller speichert sie.
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].
| Feld | Größe | Formel |
|---|---|---|
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 | eine frische Nonce aus dem Advertising |
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)
Der Controller: Nonce im Ring → owner_sig mit seinem eigenen owner_secret → valid_until gegen seine Uhr → leitet guest_key → guest_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 ]
bleId = 0→ Besitzer,key = owner_secret.bleId ≠ 0→ Gast,key = guest_key(wie in §6).
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)
| Code | Wert | Code | Wert |
|---|---|---|---|
0x00 | FIRE OK | 0x10 | TIME OK (owner) |
0x30 | FIRE OK — OFFEN | 0x11 | TIME OK (guest) |
0x31 | FIRE OK — GESCHLOSSEN | 0x12 | TIME hat die falsche Länge |
0x01 | die Nonce ist veraltet oder nicht im Ring | 0x13 | der HMAC von TIME stimmte nicht |
0x02 | die TTL ist abgelaufen | 0x15 | TIME lief rückwärts (Ratsche) |
0x03 | owner_sig ist ungültig | 0x16 | TIME überschreitet die Grenze von +24 h |
0x04 | guest_sig ist ungültig | 0x17 | TIME ohne kürzliches Auslösen |
0x05 | falsche Länge | 0x20 | WIFI OK |
0x06 | HKDF failed | 0x21 | WIFI hat das falsche Format |
0x07 | der HMAC des Besitzers ist ungültig | 0x22 | der 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.