Skip to content

Commit 7436eab

Browse files
authored
Merge pull request #11668 from espressif/fix/py
fix(python): Fixes for Python code scanning alerts
2 parents c3f9513 + 03d0566 commit 7436eab

File tree

12 files changed

+246
-56
lines changed

12 files changed

+246
-56
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
doctests = True
66
# W503 and W504 are mutually exclusive. PEP 8 recommends line break before.
77
ignore = W503,E203
8-
max-complexity = 20
8+
max-complexity = 30
99
max-line-length = 120
1010
select = E,W,F,C,N

.github/pytools/Sign-File.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function FindSignTool {
1919
if (Test-Path -Path $SignTool -PathType Leaf) {
2020
return $SignTool
2121
}
22-
$sdkVers = "10.0.22000.0", "10.0.20348.0", "10.0.19041.0", "10.0.17763.0"
22+
$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"
2323
Foreach ($ver in $sdkVers)
2424
{
2525
$SignTool = "${env:ProgramFiles(x86)}\Windows Kits\10\bin\${ver}\x64\signtool.exe"

.github/scripts/get_affected.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -626,11 +626,9 @@ def find_affected_sketches(changed_files: list[str]) -> None:
626626
q = queue.Queue()
627627

628628
if component_mode:
629-
print(f"Affected IDF component examples:", file=sys.stderr)
630629
# Get all available component examples once for efficiency
631630
all_examples = list_idf_component_examples()
632631
else:
633-
print(f"Affected sketches:", file=sys.stderr)
634632
all_examples = []
635633

636634
for file in changed_files:
@@ -648,11 +646,9 @@ def find_affected_sketches(changed_files: list[str]) -> None:
648646
# Check if this file belongs to an IDF component example
649647
for example in all_examples:
650648
if file.startswith(example + "/") and example not in affected_sketches:
651-
print(example, file=sys.stderr)
652649
affected_sketches.append(example)
653650
else:
654651
if file.endswith('.ino') and file not in affected_sketches:
655-
print(file, file=sys.stderr)
656652
affected_sketches.append(file)
657653

658654
# Continue with reverse dependency traversal
@@ -687,18 +683,24 @@ def find_affected_sketches(changed_files: list[str]) -> None:
687683
if should_traverse:
688684
q.put(dependency)
689685
if dependency_example and dependency_example not in affected_sketches:
690-
print(dependency_example, file=sys.stderr)
691686
affected_sketches.append(dependency_example)
692687
else:
693688
q.put(dependency)
694689
if dependency.endswith('.ino') and dependency not in affected_sketches:
695-
print(dependency, file=sys.stderr)
696690
affected_sketches.append(dependency)
697691

698692
if component_mode:
699693
print(f"Total affected IDF component examples: {len(affected_sketches)}", file=sys.stderr)
694+
if affected_sketches:
695+
print("Affected IDF component examples:", file=sys.stderr)
696+
for example in affected_sketches:
697+
print(f" {example}", file=sys.stderr)
700698
else:
701699
print(f"Total affected sketches: {len(affected_sketches)}", file=sys.stderr)
700+
if affected_sketches:
701+
print("Affected sketches:", file=sys.stderr)
702+
for sketch in affected_sketches:
703+
print(f" {sketch}", file=sys.stderr)
702704

703705
def save_dependencies_as_json(output_file: str = "dependencies.json") -> None:
704706
"""

.github/scripts/merge_packages.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717

1818

1919
def load_package(filename):
20-
pkg = json.load(open(filename))["packages"][0]
20+
with open(filename) as f:
21+
pkg = json.load(f)["packages"][0]
2122
print("Loaded package {0} from {1}".format(pkg["name"], filename), file=sys.stderr)
2223
print("{0} platform(s), {1} tools".format(len(pkg["platforms"]), len(pkg["tools"])), file=sys.stderr)
2324
return pkg

libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
const char *ssid = "..........";
2121
const char *password = "..........";
22+
uint32_t last_ota_time = 0;
2223

2324
void setup() {
2425
Serial.begin(115200);
@@ -40,9 +41,13 @@ void setup() {
4041
// No authentication by default
4142
// ArduinoOTA.setPassword("admin");
4243

43-
// Password can be set with it's md5 value as well
44-
// MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
45-
// ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
44+
// Password can be set with plain text (will be hashed internally)
45+
// The authentication uses PBKDF2-HMAC-SHA256 with 10,000 iterations
46+
// ArduinoOTA.setPassword("admin");
47+
48+
// Or set password with pre-hashed value (SHA256 hash of "admin")
49+
// SHA256(admin) = 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
50+
// ArduinoOTA.setPasswordHash("8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918");
4651

4752
ArduinoOTA
4853
.onStart([]() {
@@ -60,7 +65,10 @@ void setup() {
6065
Serial.println("\nEnd");
6166
})
6267
.onProgress([](unsigned int progress, unsigned int total) {
63-
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
68+
if (millis() - last_ota_time > 500) {
69+
Serial.printf("Progress: %u%%\n", (progress / (total / 100)));
70+
last_ota_time = millis();
71+
}
6472
})
6573
.onError([](ota_error_t error) {
6674
Serial.printf("Error[%u]: ", error);

libraries/ArduinoOTA/src/ArduinoOTA.cpp

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
#include "ArduinoOTA.h"
2020
#include "NetworkClient.h"
2121
#include "ESPmDNS.h"
22-
#include "MD5Builder.h"
22+
#include "SHA2Builder.h"
23+
#include "PBKDF2_HMACBuilder.h"
2324
#include "Update.h"
2425

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

7374
ArduinoOTAClass &ArduinoOTAClass::setPassword(const char *password) {
7475
if (_state == OTA_IDLE && password) {
75-
MD5Builder passmd5;
76-
passmd5.begin();
77-
passmd5.add(password);
78-
passmd5.calculate();
76+
// Hash the password with SHA256 for storage (not plain text)
77+
SHA256Builder pass_hash;
78+
pass_hash.begin();
79+
pass_hash.add(password);
80+
pass_hash.calculate();
7981
_password.clear();
80-
_password = passmd5.toString();
82+
_password = pass_hash.toString();
8183
}
8284
return *this;
8385
}
8486

8587
ArduinoOTAClass &ArduinoOTAClass::setPasswordHash(const char *password) {
8688
if (_state == OTA_IDLE && password) {
89+
// Store the pre-hashed password directly
8790
_password.clear();
8891
_password = password;
8992
}
@@ -188,17 +191,18 @@ void ArduinoOTAClass::_onRx() {
188191
_udp_ota.read();
189192
_md5 = readStringUntil('\n');
190193
_md5.trim();
191-
if (_md5.length() != 32) {
194+
if (_md5.length() != 32) { // MD5 produces 32 character hex string for firmware integrity
192195
log_e("bad md5 length");
193196
return;
194197
}
195198

196199
if (_password.length()) {
197-
MD5Builder nonce_md5;
198-
nonce_md5.begin();
199-
nonce_md5.add(String(micros()));
200-
nonce_md5.calculate();
201-
_nonce = nonce_md5.toString();
200+
// Generate a random challenge (nonce)
201+
SHA256Builder nonce_sha256;
202+
nonce_sha256.begin();
203+
nonce_sha256.add(String(micros()) + String(random(1000000)));
204+
nonce_sha256.calculate();
205+
_nonce = nonce_sha256.toString();
202206

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

231-
String challenge = _password + ":" + String(_nonce) + ":" + cnonce;
232-
MD5Builder _challengemd5;
233-
_challengemd5.begin();
234-
_challengemd5.add(challenge);
235-
_challengemd5.calculate();
236-
String result = _challengemd5.toString();
237-
238-
if (result.equals(response)) {
235+
// Verify the challenge/response using PBKDF2-HMAC-SHA256
236+
// The client should derive a key using PBKDF2-HMAC-SHA256 with:
237+
// - password: the OTA password (or its hash if using setPasswordHash)
238+
// - salt: nonce + cnonce
239+
// - iterations: 10000 (or configurable)
240+
// Then hash the challenge with the derived key
241+
242+
String salt = _nonce + ":" + cnonce;
243+
SHA256Builder sha256;
244+
// Use the stored password hash for PBKDF2 derivation
245+
PBKDF2_HMACBuilder pbkdf2(&sha256, _password, salt, 10000);
246+
247+
pbkdf2.begin();
248+
pbkdf2.calculate();
249+
String derived_key = pbkdf2.toString();
250+
251+
// Create challenge: derived_key + nonce + cnonce
252+
String challenge = derived_key + ":" + _nonce + ":" + cnonce;
253+
SHA256Builder challenge_sha256;
254+
challenge_sha256.begin();
255+
challenge_sha256.add(challenge);
256+
challenge_sha256.calculate();
257+
String expected_response = challenge_sha256.toString();
258+
259+
if (expected_response.equals(response)) {
239260
_udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort());
240261
_udp_ota.print("OK");
241262
_udp_ota.endPacket();
@@ -266,7 +287,8 @@ void ArduinoOTAClass::_runUpdate() {
266287
_state = OTA_IDLE;
267288
return;
268289
}
269-
Update.setMD5(_md5.c_str());
290+
291+
Update.setMD5(_md5.c_str()); // Note: Update library still uses MD5 for firmware integrity, this is separate from authentication
270292

271293
if (_start_callback) {
272294
_start_callback();

libraries/ArduinoOTA/src/ArduinoOTA.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class ArduinoOTAClass {
5454
//Sets the password that will be required for OTA. Default NULL
5555
ArduinoOTAClass &setPassword(const char *password);
5656

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

6060
//Sets the partition label to write to when updating SPIFFS. Default NULL

libraries/WiFi/examples/WiFiUDPClient/udp_server.py

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,96 @@
22
# for messages from the ESP32 board and prints them
33
import socket
44
import sys
5+
import subprocess
6+
import platform
7+
8+
9+
def get_interface_ips():
10+
"""Get all available interface IP addresses"""
11+
interface_ips = []
12+
13+
# Try using system commands to get interface IPs
14+
system = platform.system().lower()
15+
16+
try:
17+
if system == "darwin" or system == "linux":
18+
# Use 'ifconfig' on macOS/Linux
19+
result = subprocess.run(["ifconfig"], capture_output=True, text=True, timeout=5)
20+
if result.returncode == 0:
21+
lines = result.stdout.split("\n")
22+
for line in lines:
23+
if "inet " in line and "127.0.0.1" not in line:
24+
# Extract IP address from ifconfig output
25+
parts = line.strip().split()
26+
for i, part in enumerate(parts):
27+
if part == "inet":
28+
if i + 1 < len(parts):
29+
ip = parts[i + 1]
30+
if ip not in interface_ips and ip != "127.0.0.1":
31+
interface_ips.append(ip)
32+
break
33+
elif system == "windows":
34+
# Use 'ipconfig' on Windows
35+
result = subprocess.run(["ipconfig"], capture_output=True, text=True, timeout=5)
36+
if result.returncode == 0:
37+
lines = result.stdout.split("\n")
38+
for line in lines:
39+
if "IPv4 Address" in line and "127.0.0.1" not in line:
40+
# Extract IP address from ipconfig output
41+
if ":" in line:
42+
ip = line.split(":")[1].strip()
43+
if ip not in interface_ips and ip != "127.0.0.1":
44+
interface_ips.append(ip)
45+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
46+
print("Error: Failed to get interface IPs using system commands")
47+
print("Trying fallback methods...")
48+
49+
# Fallback: try to get IPs using socket methods
50+
if not interface_ips:
51+
try:
52+
# Get all IP addresses associated with the hostname
53+
hostname = socket.gethostname()
54+
ip_list = socket.gethostbyname_ex(hostname)[2]
55+
for ip in ip_list:
56+
if ip not in interface_ips and ip != "127.0.0.1":
57+
interface_ips.append(ip)
58+
except socket.gaierror:
59+
print("Error: Failed to get interface IPs using sockets")
60+
61+
# Fail if no interfaces found
62+
if not interface_ips:
63+
print("Error: No network interfaces found. Please check your network configuration.")
64+
sys.exit(1)
65+
66+
return interface_ips
67+
68+
69+
def select_interface(interface_ips):
70+
"""Ask user to select which interface to bind to"""
71+
if len(interface_ips) == 1:
72+
print(f"Using interface: {interface_ips[0]}")
73+
return interface_ips[0]
74+
75+
print("Multiple network interfaces detected:")
76+
for i, ip in enumerate(interface_ips, 1):
77+
print(f" {i}. {ip}")
78+
79+
while True:
80+
try:
81+
choice = input(f"Select interface (1-{len(interface_ips)}): ").strip()
82+
choice_idx = int(choice) - 1
83+
if 0 <= choice_idx < len(interface_ips):
84+
selected_ip = interface_ips[choice_idx]
85+
print(f"Selected interface: {selected_ip}")
86+
return selected_ip
87+
else:
88+
print(f"Please enter a number between 1 and {len(interface_ips)}")
89+
except ValueError:
90+
print("Please enter a valid number")
91+
except KeyboardInterrupt:
92+
print("\nExiting...")
93+
sys.exit(1)
94+
595

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

103+
# Get available interfaces and let user choose
104+
interface_ips = get_interface_ips()
105+
selected_ip = select_interface(interface_ips)
106+
13107
try:
14-
s.bind(("", 3333))
108+
s.bind((selected_ip, 3333))
15109
except socket.error as msg:
16110
print("Bind failed. Error: " + str(msg[0]) + ": " + msg[1])
17111
sys.exit()
18112

19-
print("Server listening")
20-
21-
print("Server listening")
113+
print(f"Server listening on {selected_ip}:3333")
22114

23115
while 1:
24116
d = s.recvfrom(1024)

tools/espota.exe

159 KB
Binary file not shown.

0 commit comments

Comments
 (0)