Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 .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
97 changes: 93 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,93 @@
# 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 +97,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