mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 08:28:12 +01:00
OTA firmware MD5 check + password support for esp-idf (#2507)
Co-authored-by: Maurice Makaay <account-github@makaay.nl>
This commit is contained in:
parent
c82d5d63e3
commit
384f8d97d8
10 changed files with 139 additions and 28 deletions
|
@ -89,6 +89,7 @@ esphome/components/mcp23x17_base/* @jesserockz
|
||||||
esphome/components/mcp23xxx_base/* @jesserockz
|
esphome/components/mcp23xxx_base/* @jesserockz
|
||||||
esphome/components/mcp2515/* @danielschramm @mvturnho
|
esphome/components/mcp2515/* @danielschramm @mvturnho
|
||||||
esphome/components/mcp9808/* @k7hpn
|
esphome/components/mcp9808/* @k7hpn
|
||||||
|
esphome/components/md5/* @esphome/core
|
||||||
esphome/components/mdns/* @esphome/core
|
esphome/components/mdns/* @esphome/core
|
||||||
esphome/components/midea/* @dudanov
|
esphome/components/midea/* @dudanov
|
||||||
esphome/components/mitsubishi/* @RubyBailey
|
esphome/components/mitsubishi/* @RubyBailey
|
||||||
|
|
1
esphome/components/md5/__init__.py
Normal file
1
esphome/components/md5/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
CODEOWNERS = ["@esphome/core"]
|
51
esphome/components/md5/md5.cpp
Normal file
51
esphome/components/md5/md5.cpp
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include "md5.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace md5 {
|
||||||
|
|
||||||
|
void MD5Digest::init() {
|
||||||
|
memset(this->digest_, 0, 16);
|
||||||
|
MD5Init(&this->ctx_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MD5Digest::add(const uint8_t *data, size_t len) { MD5Update(&this->ctx_, data, len); }
|
||||||
|
|
||||||
|
void MD5Digest::calculate() { MD5Final(this->digest_, &this->ctx_); }
|
||||||
|
|
||||||
|
void MD5Digest::get_bytes(uint8_t *output) { memcpy(output, this->digest_, 16); }
|
||||||
|
|
||||||
|
void MD5Digest::get_hex(char *output) {
|
||||||
|
for (size_t i = 0; i < 16; i++) {
|
||||||
|
sprintf(output + i * 2, "%02x", this->digest_[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MD5Digest::equals_bytes(const char *expected) {
|
||||||
|
for (size_t i = 0; i < 16; i++) {
|
||||||
|
if (expected[i] != this->digest_[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MD5Digest::equals_hex(const char *expected) {
|
||||||
|
for (size_t i = 0; i < 16; i++) {
|
||||||
|
auto high = parse_hex(expected[i * 2]);
|
||||||
|
auto low = parse_hex(expected[i * 2 + 1]);
|
||||||
|
if (!high.has_value() || !low.has_value()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto value = (*high << 4) | *low;
|
||||||
|
if (value != this->digest_[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace md5
|
||||||
|
} // namespace esphome
|
58
esphome/components/md5/md5.h
Normal file
58
esphome/components/md5/md5.h
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
#include "esp32/rom/md5_hash.h"
|
||||||
|
#define MD5_CTX_TYPE MD5Context
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_ARDUINO) && defined(USE_ESP32)
|
||||||
|
#include "rom/md5_hash.h"
|
||||||
|
#define MD5_CTX_TYPE MD5Context
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_ARDUINO) && defined(USE_ESP8266)
|
||||||
|
#include <md5.h>
|
||||||
|
#define MD5_CTX_TYPE md5_context_t
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace md5 {
|
||||||
|
|
||||||
|
class MD5Digest {
|
||||||
|
public:
|
||||||
|
MD5Digest() = default;
|
||||||
|
~MD5Digest() = default;
|
||||||
|
|
||||||
|
/// Initialize a new MD5 digest computation.
|
||||||
|
void init();
|
||||||
|
|
||||||
|
/// Add bytes of data for the digest.
|
||||||
|
void add(const uint8_t *data, size_t len);
|
||||||
|
void add(const char *data, size_t len) { this->add((const uint8_t *) data, len); }
|
||||||
|
|
||||||
|
/// Compute the digest, based on the provided data.
|
||||||
|
void calculate();
|
||||||
|
|
||||||
|
/// Retrieve the MD5 digest as bytes.
|
||||||
|
/// The output must be able to hold 16 bytes or more.
|
||||||
|
void get_bytes(uint8_t *output);
|
||||||
|
|
||||||
|
/// Retrieve the MD5 digest as hex characters.
|
||||||
|
/// The output must be able to hold 32 bytes or more.
|
||||||
|
void get_hex(char *output);
|
||||||
|
|
||||||
|
/// Compare the digest against a provided byte-encoded digest (16 bytes).
|
||||||
|
bool equals_bytes(const char *expected);
|
||||||
|
|
||||||
|
/// Compare the digest against a provided hex-encoded digest (32 bytes).
|
||||||
|
bool equals_hex(const char *expected);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
MD5_CTX_TYPE ctx_{};
|
||||||
|
uint8_t digest_[16];
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace md5
|
||||||
|
} // namespace esphome
|
|
@ -15,7 +15,7 @@ from esphome.core import CORE, coroutine_with_priority
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
AUTO_LOAD = ["socket"]
|
AUTO_LOAD = ["socket", "md5"]
|
||||||
|
|
||||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||||
CONF_ON_BEGIN = "on_begin"
|
CONF_ON_BEGIN = "on_begin"
|
||||||
|
@ -35,20 +35,12 @@ OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template())
|
||||||
OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template())
|
OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template())
|
||||||
|
|
||||||
|
|
||||||
def validate_password_support(value):
|
|
||||||
if CORE.using_arduino:
|
|
||||||
return value
|
|
||||||
if CORE.using_esp_idf:
|
|
||||||
raise cv.Invalid("Password support is not implemented yet for ESP-IDF")
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(OTAComponent),
|
cv.GenerateID(): cv.declare_id(OTAComponent),
|
||||||
cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
|
cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
|
||||||
cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232): cv.port,
|
cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232): cv.port,
|
||||||
cv.Optional(CONF_PASSWORD): cv.All(cv.string, validate_password_support),
|
cv.Optional(CONF_PASSWORD): cv.string,
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_REBOOT_TIMEOUT, default="5min"
|
CONF_REBOOT_TIMEOUT, default="5min"
|
||||||
): cv.positive_time_period_milliseconds,
|
): cv.positive_time_period_milliseconds,
|
||||||
|
|
|
@ -9,6 +9,7 @@ namespace esphome {
|
||||||
namespace ota {
|
namespace ota {
|
||||||
|
|
||||||
class ArduinoESP32OTABackend : public OTABackend {
|
class ArduinoESP32OTABackend : public OTABackend {
|
||||||
|
public:
|
||||||
OTAResponseTypes begin(size_t image_size) override;
|
OTAResponseTypes begin(size_t image_size) override;
|
||||||
void set_update_md5(const char *md5) override;
|
void set_update_md5(const char *md5) override;
|
||||||
OTAResponseTypes write(uint8_t *data, size_t len) override;
|
OTAResponseTypes write(uint8_t *data, size_t len) override;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include "ota_backend_esp_idf.h"
|
#include "ota_backend_esp_idf.h"
|
||||||
#include "ota_component.h"
|
#include "ota_component.h"
|
||||||
#include <esp_ota_ops.h>
|
#include <esp_ota_ops.h>
|
||||||
|
#include "esphome/components/md5/md5.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ota {
|
namespace ota {
|
||||||
|
@ -24,15 +25,15 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
|
||||||
}
|
}
|
||||||
return OTA_RESPONSE_ERROR_UNKNOWN;
|
return OTA_RESPONSE_ERROR_UNKNOWN;
|
||||||
}
|
}
|
||||||
|
this->md5_.init();
|
||||||
return OTA_RESPONSE_OK;
|
return OTA_RESPONSE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IDFOTABackend::set_update_md5(const char *md5) {
|
void IDFOTABackend::set_update_md5(const char *expected_md5) { memcpy(this->expected_bin_md5_, expected_md5, 32); }
|
||||||
// pass
|
|
||||||
}
|
|
||||||
|
|
||||||
OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
|
OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
|
||||||
esp_err_t err = esp_ota_write(this->update_handle_, data, len);
|
esp_err_t err = esp_ota_write(this->update_handle_, data, len);
|
||||||
|
this->md5_.add(data, len);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
|
if (err == ESP_ERR_OTA_VALIDATE_FAILED) {
|
||||||
return OTA_RESPONSE_ERROR_MAGIC;
|
return OTA_RESPONSE_ERROR_MAGIC;
|
||||||
|
@ -45,6 +46,11 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
|
||||||
}
|
}
|
||||||
|
|
||||||
OTAResponseTypes IDFOTABackend::end() {
|
OTAResponseTypes IDFOTABackend::end() {
|
||||||
|
this->md5_.calculate();
|
||||||
|
if (!this->md5_.equals_hex(this->expected_bin_md5_)) {
|
||||||
|
this->abort();
|
||||||
|
return OTA_RESPONSE_ERROR_UPDATE_END;
|
||||||
|
}
|
||||||
esp_err_t err = esp_ota_end(this->update_handle_);
|
esp_err_t err = esp_ota_end(this->update_handle_);
|
||||||
this->update_handle_ = 0;
|
this->update_handle_ = 0;
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#include "ota_component.h"
|
#include "ota_component.h"
|
||||||
#include "ota_backend.h"
|
#include "ota_backend.h"
|
||||||
#include <esp_ota_ops.h>
|
#include <esp_ota_ops.h>
|
||||||
|
#include "esphome/components/md5/md5.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ota {
|
namespace ota {
|
||||||
|
@ -20,6 +21,8 @@ class IDFOTABackend : public OTABackend {
|
||||||
private:
|
private:
|
||||||
esp_ota_handle_t update_handle_{0};
|
esp_ota_handle_t update_handle_{0};
|
||||||
const esp_partition_t *partition_;
|
const esp_partition_t *partition_;
|
||||||
|
md5::MD5Digest md5_{};
|
||||||
|
char expected_bin_md5_[32];
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ota
|
} // namespace ota
|
||||||
|
|
|
@ -8,15 +8,12 @@
|
||||||
#include "esphome/core/application.h"
|
#include "esphome/core/application.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/util.h"
|
#include "esphome/core/util.h"
|
||||||
|
#include "esphome/components/md5/md5.h"
|
||||||
#include "esphome/components/network/util.h"
|
#include "esphome/components/network/util.h"
|
||||||
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
|
||||||
#ifdef USE_OTA_PASSWORD
|
|
||||||
#include <MD5Builder.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace ota {
|
namespace ota {
|
||||||
|
|
||||||
|
@ -173,12 +170,12 @@ void OTAComponent::handle_() {
|
||||||
if (!this->password_.empty()) {
|
if (!this->password_.empty()) {
|
||||||
buf[0] = OTA_RESPONSE_REQUEST_AUTH;
|
buf[0] = OTA_RESPONSE_REQUEST_AUTH;
|
||||||
this->writeall_(buf, 1);
|
this->writeall_(buf, 1);
|
||||||
MD5Builder md5_builder{};
|
md5::MD5Digest md5{};
|
||||||
md5_builder.begin();
|
md5.init();
|
||||||
sprintf(sbuf, "%08X", random_uint32());
|
sprintf(sbuf, "%08X", random_uint32());
|
||||||
md5_builder.add(sbuf);
|
md5.add(sbuf, 8);
|
||||||
md5_builder.calculate();
|
md5.calculate();
|
||||||
md5_builder.getChars(sbuf);
|
md5.get_hex(sbuf);
|
||||||
ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf);
|
ESP_LOGV(TAG, "Auth: Nonce is %s", sbuf);
|
||||||
|
|
||||||
// Send nonce, 32 bytes hex MD5
|
// Send nonce, 32 bytes hex MD5
|
||||||
|
@ -188,10 +185,10 @@ void OTAComponent::handle_() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare challenge
|
// prepare challenge
|
||||||
md5_builder.begin();
|
md5.init();
|
||||||
md5_builder.add(this->password_.c_str());
|
md5.add(this->password_.c_str(), this->password_.length());
|
||||||
// add nonce
|
// add nonce
|
||||||
md5_builder.add(sbuf);
|
md5.add(sbuf, 32);
|
||||||
|
|
||||||
// Receive cnonce, 32 bytes hex MD5
|
// Receive cnonce, 32 bytes hex MD5
|
||||||
if (!this->readall_(buf, 32)) {
|
if (!this->readall_(buf, 32)) {
|
||||||
|
@ -201,11 +198,11 @@ void OTAComponent::handle_() {
|
||||||
sbuf[32] = '\0';
|
sbuf[32] = '\0';
|
||||||
ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf);
|
ESP_LOGV(TAG, "Auth: CNonce is %s", sbuf);
|
||||||
// add cnonce
|
// add cnonce
|
||||||
md5_builder.add(sbuf);
|
md5.add(sbuf, 32);
|
||||||
|
|
||||||
// calculate result
|
// calculate result
|
||||||
md5_builder.calculate();
|
md5.calculate();
|
||||||
md5_builder.getChars(sbuf);
|
md5.get_hex(sbuf);
|
||||||
ESP_LOGV(TAG, "Auth: Result is %s", sbuf);
|
ESP_LOGV(TAG, "Auth: Result is %s", sbuf);
|
||||||
|
|
||||||
// Receive result, 32 bytes hex MD5
|
// Receive result, 32 bytes hex MD5
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
#define USE_LOGGER
|
#define USE_LOGGER
|
||||||
#define USE_MDNS
|
#define USE_MDNS
|
||||||
#define USE_NUMBER
|
#define USE_NUMBER
|
||||||
|
#define USE_OTA_PASSWORD
|
||||||
#define USE_OTA_STATE_CALLBACK
|
#define USE_OTA_STATE_CALLBACK
|
||||||
#define USE_POWER_SUPPLY
|
#define USE_POWER_SUPPLY
|
||||||
#define USE_PROMETHEUS
|
#define USE_PROMETHEUS
|
||||||
|
|
Loading…
Reference in a new issue