Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 51 additions & 18 deletions tools/espota.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
sys.stderr.flush()


def serve(remote_addr, local_addr, remote_port, local_port, password, filename, command=FLASH): # noqa: C901
def serve(remote_addr, local_addr, remote_port, local_port, password, md5_target, filename, command=FLASH): # noqa: C901
# Create a TCP/IP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_address = (local_addr, local_port)
Expand Down Expand Up @@ -119,7 +119,10 @@
return 1
sock2.settimeout(TIMEOUT)
try:
data = sock2.recv(69).decode() # "AUTH " + 64-char SHA256 nonce
if md5_target:
data = sock2.recv(37).decode() # "AUTH " + 32-char MD5 nonce
else:
data = sock2.recv(69).decode() # "AUTH " + 64-char SHA256 nonce
break
except: # noqa: E722
sys.stderr.write(".")
Expand All @@ -133,32 +136,47 @@
if data != "OK":
if data.startswith("AUTH"):
nonce = data.split()[1]

# Generate client nonce (cnonce)
cnonce_text = "%s%u%s%s" % (filename, content_size, file_md5, remote_addr)
cnonce = hashlib.sha256(cnonce_text.encode()).hexdigest()

# PBKDF2-HMAC-SHA256 challenge/response protocol
# The ESP32 stores the password as SHA256 hash, so we need to hash the password first
# 1. Hash the password with SHA256 (to match ESP32 storage)
password_hash = hashlib.sha256(password.encode()).hexdigest()
if md5_target:
# Generate client nonce (cnonce)
cnonce = hashlib.md5(cnonce_text.encode()).hexdigest()

# MD5 challenge/response protocol (insecure, use only for compatibility with old firmwares)
# 1. Hash the password with MD5 (to match ESP32 storage)
password_hash = hashlib.md5(password.encode()).hexdigest()

# 2. Create challenge response
challenge = "%s:%s:%s" % (password_hash, nonce, cnonce)
response = hashlib.md5(challenge.encode()).hexdigest()
else:
# Generate client nonce (cnonce)
cnonce = hashlib.sha256(cnonce_text.encode()).hexdigest()

# PBKDF2-HMAC-SHA256 challenge/response protocol
# The ESP32 stores the password as SHA256 hash, so we need to hash the password first
# 1. Hash the password with SHA256 (to match ESP32 storage)
password_hash = hashlib.sha256(password.encode()).hexdigest()

# 2. Derive key using PBKDF2-HMAC-SHA256 with the password hash
salt = nonce + ":" + cnonce
derived_key = hashlib.pbkdf2_hmac("sha256", password_hash.encode(), salt.encode(), 10000)
derived_key_hex = derived_key.hex()
# 2. Derive key using PBKDF2-HMAC-SHA256 with the password hash
salt = nonce + ":" + cnonce
derived_key = hashlib.pbkdf2_hmac("sha256", password_hash.encode(), salt.encode(), 10000)
derived_key_hex = derived_key.hex()

# 3. Create challenge response
challenge = derived_key_hex + ":" + nonce + ":" + cnonce
response = hashlib.sha256(challenge.encode()).hexdigest()
# 3. Create challenge response
challenge = derived_key_hex + ":" + nonce + ":" + cnonce
response = hashlib.sha256(challenge.encode()).hexdigest()

sys.stderr.write("Authenticating...")
sys.stderr.flush()
message = "%d %s %s\n" % (AUTH, cnonce, response)
sock2.sendto(message.encode(), remote_address)
sock2.settimeout(10)
try:
data = sock2.recv(64).decode() # SHA256 produces 64 character response
if md5_target:
data = sock2.recv(32).decode() # MD5 produces 32 character response
else:
data = sock2.recv(64).decode() # SHA256 produces 64 character response
except: # noqa: E722
sys.stderr.write("FAIL\n")
logging.error("No Answer to our Authentication")
Expand Down Expand Up @@ -269,6 +287,14 @@

# authentication
parser.add_argument("-a", "--auth", dest="auth", help="Set authentication password.", action="store", default="")
parser.add_argument(
"-m",
"--md5-target",
dest="md5_target",
help="Target device is using MD5 checksum. This is insecure, use only for compatibility with old firmwares.",
action="store_true",
default=False,
)

# image
parser.add_argument("-f", "--file", dest="image", help="Image file.", metavar="FILE", default=None)
Expand Down Expand Up @@ -335,7 +361,14 @@
command = SPIFFS

return serve(
options.esp_ip, options.host_ip, options.esp_port, options.host_port, options.auth, options.image, command
options.esp_ip,
options.host_ip,
options.esp_port,
options.host_port,
options.auth,
options.md5_target,
options.image,
command
)


Expand Down
Loading