Skip to content

Commit 38aed79

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