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 38aed79

Browse files
fix(ota): Use secure password hashing algorithm
1 parent 3e730de commit 38aed79

File tree

4 files changed

+78
-35
lines changed

4 files changed

+78
-35
lines changed

‎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 "SHA256Builder.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

‎tools/espota.py

Lines changed: 21 additions & 8 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:
@@ -119,7 +118,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
119118
return 1
120119
sock2.settimeout(TIMEOUT)
121120
try:
122-
data = sock2.recv(37).decode()
121+
data = sock2.recv(69).decode()# "AUTH " + 64-char SHA3-256 nonce
123122
break
124123
except: # noqa: E722
125124
sys.stderr.write(".")
@@ -133,18 +132,32 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
133132
if data != "OK":
134133
if data.startswith("AUTH"):
135134
nonce = data.split()[1]
135+
136+
# Generate client nonce (cnonce)
136137
cnonce_text = "%s%u%s%s" % (filename, content_size, file_md5, remote_addr)
137-
cnonce = hashlib.md5(cnonce_text.encode()).hexdigest()
138-
passmd5 = hashlib.md5(password.encode()).hexdigest()
139-
result_text = "%s:%s:%s" % (passmd5, nonce, cnonce)
140-
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+
141154
sys.stderr.write("Authenticating...")
142155
sys.stderr.flush()
143-
message = "%d %s %s\n" % (AUTH, cnonce, result)
156+
message = "%d %s %s\n" % (AUTH, cnonce, response)
144157
sock2.sendto(message.encode(), remote_address)
145158
sock2.settimeout(10)
146159
try:
147-
data = sock2.recv(32).decode()
160+
data = sock2.recv(64).decode()# SHA256 produces 64 character response
148161
except: # noqa: E722
149162
sys.stderr.write("FAIL\n")
150163
logging.error("No Answer to our Authentication")

0 commit comments

Comments
(0)

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