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:
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