eculocate

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 218b7cdb27901b45ae5085d6eb19ce1f39e3c23a
parent 1a5314d2a390e84d0a817dbe2068b3a0fa659ef7
Author: Daniel Barlow <dan@telent.net>
Date:   Tue, 17 Feb 2026 12:37:59 +0000

ble: encrypt wifi password

BLE "just works" pairing dos not protect against active MITM,
so we need to handle this ourselves. Use nacl/libsodium
crypto_box to encrypt and authenticate the wifi password

this also adds a step to the interaction between client and device:
after writing the password, we now write a "true" to the "connecting"
characteristic to kick off the actual conection attempt.

Diffstat:
M.gitignore | 1+
Mclient/ble.py | 31++++++++++++++++++++++++++++---
Mecu-esp32/src/wibble.rs | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Mnotes.md | 3++-
4 files changed, 101 insertions(+), 17 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -2,3 +2,4 @@ *.pem *~ target/ +keys/ diff --git a/client/ble.py b/client/ble.py @@ -6,12 +6,18 @@ from bleak import BleakClient,BleakScanner import sys import os +import nacl +from pathlib import Path +from nacl.public import PrivateKey, Box, PublicKey +from nacl.encoding import RawEncoder +import sys + readline.parse_and_bind("tab: complete") address = "FF:E4:05:1A:8F:FF" UUIDS = { - 'connection_state': "76be25c2-0801-44fc-a97c-bcb56c69c926", + 'connecting': "76be25c2-0801-44fc-a97c-bcb56c69c926", 'max_index': "76be25c2-0802-44fc-a97c-bcb56c69c926", 'current_index': "76be25c2-0803-44fc-a97c-bcb56c69c926", 'current_network': "76be25c2-0804-44fc-a97c-bcb56c69c926", @@ -34,6 +40,23 @@ async def show_networks(client): networks.append(current_network) print(f"{i}: {current_network}") +with open("keys/client.key", "rb") as f: + secret = PrivateKey(f.read(), encoder=RawEncoder) + +with open("keys/device.pub", "rb") as f: + rcpt = PublicKey(f.read(), encoder=RawEncoder) + +sbox = Box(secret, rcpt) + +def encrypt_password(plaintext): + mybytes = sbox.encrypt(plaintext.encode('UTF-8')) + r = bytearray(mybytes) + r.insert(0, len(mybytes)) + return r + +# encrypt_password("hello world") + + async def choose_network(client, name): for i in range(1, max_index + 1): if networks[i].find(name) >= 0: @@ -44,8 +67,10 @@ async def choose_network(client, name): await client.write_gatt_char(UUIDS['current_index'], bytes([chosen])) new_index = await client.read_gatt_char(UUIDS['current_index']) print(f"set to {new_index[0]}") - secret = os.getenv('PASSWORD') - await client.write_gatt_char(UUIDS['secret'], bytearray(secret.encode('utf-8'))) + secret = encrypt_password(os.getenv('PASSWORD')) + print(f"about to write secret {len(secret)}") + await client.write_gatt_char(UUIDS['secret'], secret) + await client.write_gatt_char(UUIDS['connecting'], bytes([1])) async def main(address): diff --git a/ecu-esp32/src/wibble.rs b/ecu-esp32/src/wibble.rs @@ -6,7 +6,13 @@ use log::{warn,info}; use embassy_futures::join::join; use embassy_futures::select::select; +use crypto_box::{ + aead::Aead, + SalsaBox, PublicKey, SecretKey, Nonce +}; + use alloc::vec::Vec; +use alloc::string::String; use crate::ota::Flasher; @@ -26,6 +32,9 @@ struct Server { #[gatt_service(uuid = "76be25c2-0000-44fc-a97c-bcb56c69c926")] struct WifiProvisioningService { + #[characteristic(uuid = "76be25c2-0801-44fc-a97c-bcb56c69c926", read, notify, value = false)] + connecting: bool, + #[characteristic(uuid = "76be25c2-0802-44fc-a97c-bcb56c69c926", read, notify, value = 0)] max_network_index: u8, @@ -40,9 +49,9 @@ struct WifiProvisioningService { current_network: [u8; 40], #[characteristic(uuid = "76be25c2-0805-44fc-a97c-bcb56c69c926", - value = [0; 64], + value = [0; (24 + 16 + 64 + 1)], write)] - secret: [u8; 64] + secret: [u8; 24 + 16 + 64 + 1] } @@ -103,6 +112,40 @@ async fn ble_task<C: Controller, P: PacketPool>(mut runner: Runner<'_, C, P>) { } } +const CLIENT_PUBLIC_KEY_BYTES: &[u8; 32] = include_bytes!("../../keys/client.pub"); +const SECRET_KEY_BYTES : &[u8; 32] = include_bytes!("../../keys/device.key"); + +#[derive(Debug)] +enum DecryptError { + Crypto(crypto_box::aead::Error), + Encoding(alloc::str::Utf8Error) +} +impl From<crypto_box::aead::Error> for DecryptError { + fn from(e: crypto_box::aead::Error) -> Self { + Self::Crypto(e) + } +} +impl From<alloc::str::Utf8Error> for DecryptError { + fn from(e: alloc::str::Utf8Error) -> Self { Self::Encoding(e) } +} + +fn decrypt_secret(ciphertext : &[u8]) -> Result<String,DecryptError> { + let client_public_key = PublicKey::from(*CLIENT_PUBLIC_KEY_BYTES); + let secret_key= SecretKey::from(*SECRET_KEY_BYTES); + + let len : usize = ciphertext[0].into(); + let ciphertext = &ciphertext[1..len+1]; + + let (nonce, encrypted) = ciphertext.split_at(24); + let nonce = Nonce::from_slice(nonce); + + let sbox = SalsaBox::new(&client_public_key, &secret_key); + let plain_bytes = sbox.decrypt(nonce, encrypted)?; + + Ok(String::from(core::str::from_utf8(&plain_bytes[..])?)) +} + + /// Stream Events until the connection closes. /// /// This function will handle the GATT events and process them. @@ -135,17 +178,20 @@ async fn gatt_events_task<P: PacketPool>(server: &Server<'_>, } else { warn!("index {:?} out of range", i); } - } else if event.handle() == wps.secret.handle { - info!("[gatt] got secret {:?}", event.data()); - let n = server.get(&wps.current_network).unwrap(); - let n_s = core::str::from_utf8(&n[8..]).unwrap(); - let n0 = - if let Some(i) = n_s.find("\0") { i } else { n_s.len() }; - flasher.set_var("ssid", Some(&n_s[..n0])); - flasher.set_var("secret", Some(core::str::from_utf8(event.data()).unwrap())); - info!("[gatt] deep breath then reboot"); - Timer::after(Duration::from_millis(2000)).await; - esp_hal::system::software_reset(); + } else if event.handle() == wps.connecting.handle { + let network = server.get(&wps.current_network).unwrap(); + let password = decrypt_secret(&server.get(&wps.secret).unwrap()); + + match password { + Ok(plaintext) => { + write_wifi_config( + flasher, &network[8..], &plaintext[..] + ).await; + }, + Err(e) => { + warn!("decdrpt error: {:?}", e); + } + } } }, _ => {} @@ -165,6 +211,17 @@ async fn gatt_events_task<P: PacketPool>(server: &Server<'_>, Ok(()) } +async fn write_wifi_config(flasher: &mut Flasher<'_>, ssid: &[u8], password: &str) -> Result<(),esp_nvs::error::Error> { + let n_s = core::str::from_utf8(&ssid).unwrap(); + let n0 = + if let Some(i) = n_s.find("\0") { i } else { n_s.len() }; + flasher.set_var("ssid", Some(&n_s[..n0]))?; + flasher.set_var("secret", Some(password))?; + info!("[gatt] deep breath then reboot"); + Timer::after(Duration::from_millis(2000)).await; + esp_hal::system::software_reset(); +} + /// Create an advertiser to use to connect to a BLE Central, and wait for it to connect. async fn advertise<'values, 'server, C: Controller>( name: &'values str, diff --git a/notes.md b/notes.md @@ -157,7 +157,8 @@ O wifi provisioning over ble X make it choose a network when psk is provided X reboot after writing to nvs X delete network if it doesn't connect - - add pairing & encrypt the psk + _ add pairing + X encrypt the psk - add client side to eculocate.py - android stuffz - switch to _tcp in dns-sd domain