Skip to content

Commit cf576d6

Browse files
committed
fix(python): Fixes for Python code scanning alerts
1 parent cb04e89 commit cf576d6

File tree

7 files changed

+233
-49
lines changed

7 files changed

+233
-49
lines changed

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

693
try:
794
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -10,15 +97,17 @@
1097
print("Failed to create socket. Error Code : " + str(msg[0]) + " Message " + msg[1])
1198
sys.exit()
1299

100+
# Get available interfaces and let user choose
101+
interface_ips = get_interface_ips()
102+
selected_ip = select_interface(interface_ips)
103+
13104
try:
14-
s.bind(("", 3333))
105+
s.bind((selected_ip, 3333))
15106
except socket.error as msg:
16107
print("Bind failed. Error: " + str(msg[0]) + ": " + msg[1])
17108
sys.exit()
18109

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

23112
while 1:
24113
d = s.recvfrom(1024)

tools/espota.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@
5454
# Constants
5555
PROGRESS_BAR_LENGTH = 60
5656

57-
5857
# update_progress(): Displays or updates a console progress bar
5958
def update_progress(progress):
6059
if PROGRESS:
@@ -94,7 +93,8 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
9493
return 1
9594

9695
content_size = os.path.getsize(filename)
97-
file_md5 = hashlib.md5(open(filename, "rb").read()).hexdigest()
96+
with open(filename, "rb") as f:
97+
file_md5 = hashlib.md5(f.read()).hexdigest()
9898
logging.info("Upload size: %d", content_size)
9999
message = "%d %d %d %s\n" % (command, local_port, content_size, file_md5)
100100

@@ -118,7 +118,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
118118
return 1
119119
sock2.settimeout(TIMEOUT)
120120
try:
121-
data = sock2.recv(37).decode()
121+
data = sock2.recv(69).decode() # "AUTH " + 64-char SHA256 nonce
122122
break
123123
except: # noqa: E722
124124
sys.stderr.write(".")
@@ -132,18 +132,32 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
132132
if data != "OK":
133133
if data.startswith("AUTH"):
134134
nonce = data.split()[1]
135+
136+
# Generate client nonce (cnonce)
135137
cnonce_text = "%s%u%s%s" % (filename, content_size, file_md5, remote_addr)
136-
cnonce = hashlib.md5(cnonce_text.encode()).hexdigest()
137-
passmd5 = hashlib.md5(password.encode()).hexdigest()
138-
result_text = "%s:%s:%s" % (passmd5, nonce, cnonce)
139-
result = hashlib.md5(result_text.encode()).hexdigest()
138+
cnonce = hashlib.sha256(cnonce_text.encode()).hexdigest()
139+
140+
# PBKDF2-HMAC-SHA256 challenge/response protocol
141+
# The ESP32 stores the password as SHA256 hash, so we need to hash the password first
142+
# 1. Hash the password with SHA256 (to match ESP32 storage)
143+
password_hash = hashlib.sha256(password.encode()).hexdigest()
144+
145+
# 2. Derive key using PBKDF2-HMAC-SHA256 with the password hash
146+
salt = nonce + ":" + cnonce
147+
derived_key = hashlib.pbkdf2_hmac('sha256', password_hash.encode(), salt.encode(), 10000)
148+
derived_key_hex = derived_key.hex()
149+
150+
# 3. Create challenge response
151+
challenge = derived_key_hex + ":" + nonce + ":" + cnonce
152+
response = hashlib.sha256(challenge.encode()).hexdigest()
153+
140154
sys.stderr.write("Authenticating...")
141155
sys.stderr.flush()
142-
message = "%d %s %s\n" % (AUTH, cnonce, result)
156+
message = "%d %s %s\n" % (AUTH, cnonce, response)
143157
sock2.sendto(message.encode(), remote_address)
144158
sock2.settimeout(10)
145159
try:
146-
data = sock2.recv(32).decode()
160+
data = sock2.recv(64).decode() # SHA256 produces 64 character response
147161
except: # noqa: E722
148162
sys.stderr.write("FAIL\n")
149163
logging.error("No Answer to our Authentication")
@@ -163,6 +177,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
163177
sock2.close()
164178

165179
logging.info("Waiting for device...")
180+
166181
try:
167182
sock.settimeout(10)
168183
connection, client_address = sock.accept()
@@ -172,6 +187,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
172187
logging.error("No response from device")
173188
sock.close()
174189
return 1
190+
175191
try:
176192
with open(filename, "rb") as f:
177193
if PROGRESS:
@@ -225,7 +241,8 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
225241
logging.error("Error response from device")
226242
connection.close()
227243
return 1
228-
244+
except Exception as e: # noqa: E722
245+
logging.error("Error: %s", str(e))
229246
finally:
230247
connection.close()
231248

0 commit comments

Comments
 (0)