Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit cf576d6

Browse files
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)

AltStyle によって変換されたページ (->オリジナル) /