Skip to content

Commit c291c26

Browse files
committed
fix(ota): Use SHA3 for sensitive information
1 parent 4e34b64 commit c291c26

File tree

4 files changed

+42
-30
lines changed

4 files changed

+42
-30
lines changed

libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino

Lines changed: 8 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,9 @@ 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 it's SHA3_256 value as well
45+
// SHA3_256(admin) = fb001dfcffd1c899f3297871406242f097aecf1a5342ccf3ebcd116146188e4b
46+
// ArduinoOTA.setPasswordHash("fb001dfcffd1c899f3297871406242f097aecf1a5342ccf3ebcd116146188e4b");
4647

4748
ArduinoOTA
4849
.onStart([]() {
@@ -60,7 +61,10 @@ void setup() {
6061
Serial.println("\nEnd");
6162
})
6263
.onProgress([](unsigned int progress, unsigned int total) {
63-
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
64+
if (millis() - last_ota_time > 500) {
65+
Serial.printf("Progress: %u%%\n", (progress / (total / 100)));
66+
last_ota_time = millis();
67+
}
6468
})
6569
.onError([](ota_error_t error) {
6670
Serial.printf("Error[%u]: ", error);

libraries/ArduinoOTA/src/ArduinoOTA.cpp

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
#include "ArduinoOTA.h"
2020
#include "NetworkClient.h"
2121
#include "ESPmDNS.h"
22-
#include "MD5Builder.h"
22+
#include "SHA3Builder.h"
2323
#include "Update.h"
2424

2525
// #define OTA_DEBUG Serial
@@ -72,12 +72,12 @@ String ArduinoOTAClass::getHostname() {
7272

7373
ArduinoOTAClass &ArduinoOTAClass::setPassword(const char *password) {
7474
if (_state == OTA_IDLE && password) {
75-
MD5Builder passmd5;
76-
passmd5.begin();
77-
passmd5.add(password);
78-
passmd5.calculate();
75+
SHA3Builder passsha3;
76+
passsha3.begin();
77+
passsha3.add(password);
78+
passsha3.calculate();
7979
_password.clear();
80-
_password = passmd5.toString();
80+
_password = passsha3.toString();
8181
}
8282
return *this;
8383
}
@@ -188,17 +188,17 @@ void ArduinoOTAClass::_onRx() {
188188
_udp_ota.read();
189189
_md5 = readStringUntil('\n');
190190
_md5.trim();
191-
if (_md5.length() != 32) {
191+
if (_md5.length() != 32) { // MD5 produces 32 character hex string for firmware integrity
192192
log_e("bad md5 length");
193193
return;
194194
}
195195

196196
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();
197+
SHA3Builder nonce_sha3;
198+
nonce_sha3.begin();
199+
nonce_sha3.add(String(micros()));
200+
nonce_sha3.calculate();
201+
_nonce = nonce_sha3.toString();
202202

203203
_udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort());
204204
_udp_ota.printf("AUTH %s", _nonce.c_str());
@@ -222,18 +222,26 @@ void ArduinoOTAClass::_onRx() {
222222
_udp_ota.read();
223223
String cnonce = readStringUntil(' ');
224224
String response = readStringUntil('\n');
225-
if (cnonce.length() != 32 || response.length() != 32) {
225+
if (cnonce.length() != 64 || response.length() != 64) { // SHA3-256 produces 64 character hex string
226226
log_e("auth param fail");
227227
_state = OTA_IDLE;
228228
return;
229229
}
230230

231231
String challenge = _password + ":" + String(_nonce) + ":" + cnonce;
232-
MD5Builder _challengemd5;
233-
_challengemd5.begin();
234-
_challengemd5.add(challenge);
235-
_challengemd5.calculate();
236-
String result = _challengemd5.toString();
232+
SHA3Builder _challengesha3;
233+
_challengesha3.begin();
234+
_challengesha3.add(challenge);
235+
_challengesha3.calculate();
236+
String result = _challengesha3.toString();
237+
238+
// Debug logging
239+
log_d("Challenge: %s", challenge.c_str());
240+
log_d("Expected: %s", result.c_str());
241+
log_d("Received: %s", response.c_str());
242+
log_d("Password hash: %s", _password.c_str());
243+
log_d("Nonce: %s", _nonce.c_str());
244+
log_d("CNonce: %s", cnonce.c_str());
237245

238246
if (result.equals(response)) {
239247
_udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort());
@@ -266,7 +274,7 @@ void ArduinoOTAClass::_runUpdate() {
266274
_state = OTA_IDLE;
267275
return;
268276
}
269-
Update.setMD5(_md5.c_str());
277+
Update.setMD5(_md5.c_str()); // Note: Update library still uses MD5 for firmware integrity, this is separate from authentication
270278

271279
if (_start_callback) {
272280
_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 SHA3_256(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: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
119119
return 1
120120
sock2.settimeout(TIMEOUT)
121121
try:
122-
data = sock2.recv(37).decode()
122+
data = sock2.recv(69).decode() # "AUTH " + 64-char SHA3-256 nonce
123123
break
124124
except: # noqa: E722
125125
sys.stderr.write(".")
@@ -134,17 +134,17 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
134134
if data.startswith("AUTH"):
135135
nonce = data.split()[1]
136136
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()
137+
cnonce = hashlib.sha3_256(cnonce_text.encode()).hexdigest()
138+
passsha3 = hashlib.sha3_256(password.encode()).hexdigest()
139+
result_text = "%s:%s:%s" % (passsha3, nonce, cnonce)
140+
result = hashlib.sha3_256(result_text.encode()).hexdigest()
141141
sys.stderr.write("Authenticating...")
142142
sys.stderr.flush()
143143
message = "%d %s %s\n" % (AUTH, cnonce, result)
144144
sock2.sendto(message.encode(), remote_address)
145145
sock2.settimeout(10)
146146
try:
147-
data = sock2.recv(32).decode()
147+
data = sock2.recv(64).decode() # SHA3-256 produces 64 character response
148148
except: # noqa: E722
149149
sys.stderr.write("FAIL\n")
150150
logging.error("No Answer to our Authentication")

0 commit comments

Comments
 (0)