Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
doctests = True
# W503 and W504 are mutually exclusive. PEP 8 recommends line break before.
ignore = W503,E203
max-complexity = 20
max-complexity = 30
max-line-length = 120
select = E,W,F,C,N
2 changes: 1 addition & 1 deletion .github/pytools/Sign-File.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function FindSignTool {
if (Test-Path -Path $SignTool -PathType Leaf) {
return $SignTool
}
$sdkVers = "10.0.22000.0", "10.0.20348.0", "10.0.19041.0", "10.0.17763.0"
$sdkVers = "10.0.22000.0", "10.0.20348.0", "10.0.19041.0", "10.0.17763.0", "10.0.14393.0", "10.0.15063.0", "10.0.16299.0", "10.0.17134.0", "10.0.26100.0"
Foreach ($ver in $sdkVers)
{
$SignTool = "${env:ProgramFiles(x86)}\Windows Kits\10\bin\${ver}\x64\signtool.exe"
Expand Down
14 changes: 8 additions & 6 deletions .github/scripts/get_affected.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,11 +626,9 @@ def find_affected_sketches(changed_files: list[str]) -> None:
q = queue.Queue()

if component_mode:
print(f"Affected IDF component examples:", file=sys.stderr)
# Get all available component examples once for efficiency
all_examples = list_idf_component_examples()
else:
print(f"Affected sketches:", file=sys.stderr)
all_examples = []

for file in changed_files:
Expand All @@ -648,11 +646,9 @@ def find_affected_sketches(changed_files: list[str]) -> None:
# Check if this file belongs to an IDF component example
for example in all_examples:
if file.startswith(example + "/") and example not in affected_sketches:
print(example, file=sys.stderr)
affected_sketches.append(example)
else:
if file.endswith('.ino') and file not in affected_sketches:
print(file, file=sys.stderr)
affected_sketches.append(file)

# Continue with reverse dependency traversal
Expand Down Expand Up @@ -687,18 +683,24 @@ def find_affected_sketches(changed_files: list[str]) -> None:
if should_traverse:
q.put(dependency)
if dependency_example and dependency_example not in affected_sketches:
print(dependency_example, file=sys.stderr)
affected_sketches.append(dependency_example)
else:
q.put(dependency)
if dependency.endswith('.ino') and dependency not in affected_sketches:
print(dependency, file=sys.stderr)
affected_sketches.append(dependency)

if component_mode:
print(f"Total affected IDF component examples: {len(affected_sketches)}", file=sys.stderr)
if affected_sketches:
print("Affected IDF component examples:", file=sys.stderr)
for example in affected_sketches:
print(f" {example}", file=sys.stderr)
else:
print(f"Total affected sketches: {len(affected_sketches)}", file=sys.stderr)
if affected_sketches:
print("Affected sketches:", file=sys.stderr)
for sketch in affected_sketches:
print(f" {sketch}", file=sys.stderr)

def save_dependencies_as_json(output_file: str = "dependencies.json") -> None:
"""
Expand Down
3 changes: 2 additions & 1 deletion .github/scripts/merge_packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@


def load_package(filename):
pkg = json.load(open(filename))["packages"][0]
with open(filename) as f:
pkg = json.load(f)["packages"][0]
print("Loaded package {0} from {1}".format(pkg["name"], filename), file=sys.stderr)
print("{0} platform(s), {1} tools".format(len(pkg["platforms"]), len(pkg["tools"])), file=sys.stderr)
return pkg
Expand Down
16 changes: 12 additions & 4 deletions libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

const char *ssid = "..........";
const char *password = "..........";
uint32_t last_ota_time = 0;

void setup() {
Serial.begin(115200);
Expand All @@ -40,9 +41,13 @@ void setup() {
// No authentication by default
// ArduinoOTA.setPassword("admin");

// Password can be set with it's md5 value as well
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
// Password can be set with plain text (will be hashed internally)
// The authentication uses PBKDF2-HMAC-SHA256 with 10,000 iterations
// ArduinoOTA.setPassword("admin");

// Or set password with pre-hashed value (SHA256 hash of "admin")
// SHA256(admin) = 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
// ArduinoOTA.setPasswordHash("8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918");

ArduinoOTA
.onStart([]() {
Expand All @@ -60,7 +65,10 @@ void setup() {
Serial.println("\nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
if (millis() - last_ota_time > 500) {
Serial.printf("Progress: %u%%\n", (progress / (total / 100)));
last_ota_time = millis();
}
})
.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
Expand Down
66 changes: 44 additions & 22 deletions libraries/ArduinoOTA/src/ArduinoOTA.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
#include "ArduinoOTA.h"
#include "NetworkClient.h"
#include "ESPmDNS.h"
#include "MD5Builder.h"
#include "SHA2Builder.h"
#include "PBKDF2_HMACBuilder.h"
#include "Update.h"

// #define OTA_DEBUG Serial
Expand Down Expand Up @@ -72,18 +73,20 @@ String ArduinoOTAClass::getHostname() {

ArduinoOTAClass &ArduinoOTAClass::setPassword(const char *password) {
if (_state == OTA_IDLE && password) {
MD5Builder passmd5;
passmd5.begin();
passmd5.add(password);
passmd5.calculate();
// Hash the password with SHA256 for storage (not plain text)
SHA256Builder pass_hash;
pass_hash.begin();
pass_hash.add(password);
pass_hash.calculate();
_password.clear();
_password = passmd5.toString();
_password = pass_hash.toString();
}
return *this;
}

ArduinoOTAClass &ArduinoOTAClass::setPasswordHash(const char *password) {
if (_state == OTA_IDLE && password) {
// Store the pre-hashed password directly
_password.clear();
_password = password;
}
Expand Down Expand Up @@ -188,17 +191,18 @@ void ArduinoOTAClass::_onRx() {
_udp_ota.read();
_md5 = readStringUntil('\n');
_md5.trim();
if (_md5.length() != 32) {
if (_md5.length() != 32) { // MD5 produces 32 character hex string for firmware integrity
log_e("bad md5 length");
return;
}

if (_password.length()) {
MD5Builder nonce_md5;
nonce_md5.begin();
nonce_md5.add(String(micros()));
nonce_md5.calculate();
_nonce = nonce_md5.toString();
// Generate a random challenge (nonce)
SHA256Builder nonce_sha256;
nonce_sha256.begin();
nonce_sha256.add(String(micros()) + String(random(1000000)));
nonce_sha256.calculate();
_nonce = nonce_sha256.toString();

_udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort());
_udp_ota.printf("AUTH %s", _nonce.c_str());
Expand All @@ -222,20 +226,37 @@ void ArduinoOTAClass::_onRx() {
_udp_ota.read();
String cnonce = readStringUntil(' ');
String response = readStringUntil('\n');
if (cnonce.length() != 32 || response.length() != 32) {
if (cnonce.length() != 64 || response.length() != 64) { // SHA256 produces 64 character hex string
log_e("auth param fail");
_state = OTA_IDLE;
return;
}

String challenge = _password + ":" + String(_nonce) + ":" + cnonce;
MD5Builder _challengemd5;
_challengemd5.begin();
_challengemd5.add(challenge);
_challengemd5.calculate();
String result = _challengemd5.toString();

if (result.equals(response)) {
// Verify the challenge/response using PBKDF2-HMAC-SHA256
// The client should derive a key using PBKDF2-HMAC-SHA256 with:
// - password: the OTA password (or its hash if using setPasswordHash)
// - salt: nonce + cnonce
// - iterations: 10000 (or configurable)
// Then hash the challenge with the derived key

String salt = _nonce + ":" + cnonce;
SHA256Builder sha256;
// Use the stored password hash for PBKDF2 derivation
PBKDF2_HMACBuilder pbkdf2(&sha256, _password, salt, 10000);

pbkdf2.begin();
pbkdf2.calculate();
String derived_key = pbkdf2.toString();

// Create challenge: derived_key + nonce + cnonce
String challenge = derived_key + ":" + _nonce + ":" + cnonce;
SHA256Builder challenge_sha256;
challenge_sha256.begin();
challenge_sha256.add(challenge);
challenge_sha256.calculate();
String expected_response = challenge_sha256.toString();

if (expected_response.equals(response)) {
_udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort());
_udp_ota.print("OK");
_udp_ota.endPacket();
Expand Down Expand Up @@ -266,7 +287,8 @@ void ArduinoOTAClass::_runUpdate() {
_state = OTA_IDLE;
return;
}
Update.setMD5(_md5.c_str());

Update.setMD5(_md5.c_str()); // Note: Update library still uses MD5 for firmware integrity, this is separate from authentication

if (_start_callback) {
_start_callback();
Expand Down
2 changes: 1 addition & 1 deletion libraries/ArduinoOTA/src/ArduinoOTA.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class ArduinoOTAClass {
//Sets the password that will be required for OTA. Default NULL
ArduinoOTAClass &setPassword(const char *password);

//Sets the password as above but in the form MD5(password). Default NULL
//Sets the password as above but in the form SHA256(password). Default NULL
ArduinoOTAClass &setPasswordHash(const char *password);

//Sets the partition label to write to when updating SPIFFS. Default NULL
Expand Down
100 changes: 96 additions & 4 deletions libraries/WiFi/examples/WiFiUDPClient/udp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,96 @@
# for messages from the ESP32 board and prints them
import socket
import sys
import subprocess
import platform


def get_interface_ips():
"""Get all available interface IP addresses"""
interface_ips = []

# Try using system commands to get interface IPs
system = platform.system().lower()

try:
if system == "darwin" or system == "linux":
# Use 'ifconfig' on macOS/Linux
result = subprocess.run(["ifconfig"], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
lines = result.stdout.split("\n")
for line in lines:
if "inet " in line and "127.0.0.1" not in line:
# Extract IP address from ifconfig output
parts = line.strip().split()
for i, part in enumerate(parts):
if part == "inet":
if i + 1 < len(parts):
ip = parts[i + 1]
if ip not in interface_ips and ip != "127.0.0.1":
interface_ips.append(ip)
break
elif system == "windows":
# Use 'ipconfig' on Windows
result = subprocess.run(["ipconfig"], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
lines = result.stdout.split("\n")
for line in lines:
if "IPv4 Address" in line and "127.0.0.1" not in line:
# Extract IP address from ipconfig output
if ":" in line:
ip = line.split(":")[1].strip()
if ip not in interface_ips and ip != "127.0.0.1":
interface_ips.append(ip)
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
print("Error: Failed to get interface IPs using system commands")
print("Trying fallback methods...")

# Fallback: try to get IPs using socket methods
if not interface_ips:
try:
# Get all IP addresses associated with the hostname
hostname = socket.gethostname()
ip_list = socket.gethostbyname_ex(hostname)[2]
for ip in ip_list:
if ip not in interface_ips and ip != "127.0.0.1":
interface_ips.append(ip)
except socket.gaierror:
print("Error: Failed to get interface IPs using sockets")

# Fail if no interfaces found
if not interface_ips:
print("Error: No network interfaces found. Please check your network configuration.")
sys.exit(1)

return interface_ips


def select_interface(interface_ips):
"""Ask user to select which interface to bind to"""
if len(interface_ips) == 1:
print(f"Using interface: {interface_ips[0]}")
return interface_ips[0]

print("Multiple network interfaces detected:")
for i, ip in enumerate(interface_ips, 1):
print(f" {i}. {ip}")

while True:
try:
choice = input(f"Select interface (1-{len(interface_ips)}): ").strip()
choice_idx = int(choice) - 1
if 0 <= choice_idx < len(interface_ips):
selected_ip = interface_ips[choice_idx]
print(f"Selected interface: {selected_ip}")
return selected_ip
else:
print(f"Please enter a number between 1 and {len(interface_ips)}")
except ValueError:
print("Please enter a valid number")
except KeyboardInterrupt:
print("\nExiting...")
sys.exit(1)


try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
Expand All @@ -10,15 +100,17 @@
print("Failed to create socket. Error Code : " + str(msg[0]) + " Message " + msg[1])
sys.exit()

# Get available interfaces and let user choose
interface_ips = get_interface_ips()
selected_ip = select_interface(interface_ips)

try:
s.bind(("", 3333))
s.bind((selected_ip, 3333))
except socket.error as msg:
print("Bind failed. Error: " + str(msg[0]) + ": " + msg[1])
sys.exit()

print("Server listening")

print("Server listening")
print(f"Server listening on {selected_ip}:3333")

while 1:
d = s.recvfrom(1024)
Expand Down
Binary file modified tools/espota.exe
Binary file not shown.
Loading
Loading