Skip to content

Commit 99b029e

Browse files
committed
fix(ota): Add legacy option for devices using MD5 authentication
1 parent a4feb6c commit 99b029e

File tree

1 file changed

+51
-18
lines changed

1 file changed

+51
-18
lines changed

tools/espota.py

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def update_progress(progress):
8181
sys.stderr.flush()
8282

8383

84-
def serve(remote_addr, local_addr, remote_port, local_port, password, filename, command=FLASH): # noqa: C901
84+
def serve(remote_addr, local_addr, remote_port, local_port, password, md5_target, filename, command=FLASH): # noqa: C901
8585
# Create a TCP/IP socket
8686
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
8787
server_address = (local_addr, local_port)
@@ -119,7 +119,10 @@ 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(69).decode() # "AUTH " + 64-char SHA256 nonce
122+
if md5_target:
123+
data = sock2.recv(37).decode() # "AUTH " + 32-char MD5 nonce
124+
else:
125+
data = sock2.recv(69).decode() # "AUTH " + 64-char SHA256 nonce
123126
break
124127
except: # noqa: E722
125128
sys.stderr.write(".")
@@ -133,32 +136,47 @@ def serve(remote_addr, local_addr, remote_port, local_port, password, filename,
133136
if data != "OK":
134137
if data.startswith("AUTH"):
135138
nonce = data.split()[1]
136-
137-
# Generate client nonce (cnonce)
138139
cnonce_text = "%s%u%s%s" % (filename, content_size, file_md5, remote_addr)
139-
cnonce = hashlib.sha256(cnonce_text.encode()).hexdigest()
140140

141-
# PBKDF2-HMAC-SHA256 challenge/response protocol
142-
# The ESP32 stores the password as SHA256 hash, so we need to hash the password first
143-
# 1. Hash the password with SHA256 (to match ESP32 storage)
144-
password_hash = hashlib.sha256(password.encode()).hexdigest()
141+
if md5_target:
142+
# Generate client nonce (cnonce)
143+
cnonce = hashlib.md5(cnonce_text.encode()).hexdigest()
144+
145+
# MD5 challenge/response protocol (insecure, use only for compatibility with old firmwares)
146+
# 1. Hash the password with MD5 (to match ESP32 storage)
147+
password_hash = hashlib.md5(password.encode()).hexdigest()
148+
149+
# 2. Create challenge response
150+
challenge = "%s:%s:%s" % (password_hash, nonce, cnonce)
151+
response = hashlib.md5(challenge.encode()).hexdigest()
152+
else:
153+
# Generate client nonce (cnonce)
154+
cnonce = hashlib.sha256(cnonce_text.encode()).hexdigest()
155+
156+
# PBKDF2-HMAC-SHA256 challenge/response protocol
157+
# The ESP32 stores the password as SHA256 hash, so we need to hash the password first
158+
# 1. Hash the password with SHA256 (to match ESP32 storage)
159+
password_hash = hashlib.sha256(password.encode()).hexdigest()
145160

146-
# 2. Derive key using PBKDF2-HMAC-SHA256 with the password hash
147-
salt = nonce + ":" + cnonce
148-
derived_key = hashlib.pbkdf2_hmac("sha256", password_hash.encode(), salt.encode(), 10000)
149-
derived_key_hex = derived_key.hex()
161+
# 2. Derive key using PBKDF2-HMAC-SHA256 with the password hash
162+
salt = nonce + ":" + cnonce
163+
derived_key = hashlib.pbkdf2_hmac("sha256", password_hash.encode(), salt.encode(), 10000)
164+
derived_key_hex = derived_key.hex()
150165

151-
# 3. Create challenge response
152-
challenge = derived_key_hex + ":" + nonce + ":" + cnonce
153-
response = hashlib.sha256(challenge.encode()).hexdigest()
166+
# 3. Create challenge response
167+
challenge = derived_key_hex + ":" + nonce + ":" + cnonce
168+
response = hashlib.sha256(challenge.encode()).hexdigest()
154169

155170
sys.stderr.write("Authenticating...")
156171
sys.stderr.flush()
157172
message = "%d %s %s\n" % (AUTH, cnonce, response)
158173
sock2.sendto(message.encode(), remote_address)
159174
sock2.settimeout(10)
160175
try:
161-
data = sock2.recv(64).decode() # SHA256 produces 64 character response
176+
if md5_target:
177+
data = sock2.recv(32).decode() # MD5 produces 32 character response
178+
else:
179+
data = sock2.recv(64).decode() # SHA256 produces 64 character response
162180
except: # noqa: E722
163181
sys.stderr.write("FAIL\n")
164182
logging.error("No Answer to our Authentication")
@@ -269,6 +287,14 @@ def parse_args(unparsed_args):
269287

270288
# authentication
271289
parser.add_argument("-a", "--auth", dest="auth", help="Set authentication password.", action="store", default="")
290+
parser.add_argument(
291+
"-m",
292+
"--md5-target",
293+
dest="md5_target",
294+
help="Target device is using MD5 checksum. This is insecure, use only for compatibility with old firmwares.",
295+
action="store_true",
296+
default=False,
297+
)
272298

273299
# image
274300
parser.add_argument("-f", "--file", dest="image", help="Image file.", metavar="FILE", default=None)
@@ -335,7 +361,14 @@ def main(args):
335361
command = SPIFFS
336362

337363
return serve(
338-
options.esp_ip, options.host_ip, options.esp_port, options.host_port, options.auth, options.image, command
364+
options.esp_ip,
365+
options.host_ip,
366+
options.esp_port,
367+
options.host_port,
368+
options.auth,
369+
options.md5_target,
370+
options.image,
371+
command
339372
)
340373

341374

0 commit comments

Comments
 (0)