diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 8b5eaeeed2..2de9582bf1 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -107,6 +107,89 @@ std::optional parse_record_slot(const std::string &key) { } // namespace +namespace { + +// The reason passed to on_pairing_failed automations, in the protocol's own spelling. +StringRef pair_abort_reason_to_string(sendspin::SendspinPairAbortReason reason) { + using sendspin::SendspinPairAbortReason; + switch (reason) { + case SendspinPairAbortReason::ATTEMPT_TIMEOUT: + return StringRef::from_lit("attempt_timeout"); + case SendspinPairAbortReason::CONCURRENT_ATTEMPT: + return StringRef::from_lit("concurrent_attempt"); + case SendspinPairAbortReason::METHOD_NOT_SUPPORTED: + return StringRef::from_lit("method_not_supported"); + case SendspinPairAbortReason::PAIRING_CODE_MISMATCH: + return StringRef::from_lit("pairing_code_mismatch"); + case SendspinPairAbortReason::USER_CANCELLED: + return StringRef::from_lit("user_cancelled"); + case SendspinPairAbortReason::UNKNOWN: + break; + } + return StringRef::from_lit("unknown"); +} + +// Zeroes through a volatile pointer so dead-store elimination cannot drop it. +void secure_wipe(void *data, size_t len) { + volatile auto *p = static_cast(data); + for (size_t i = 0; i < len; ++i) { + p[i] = 0; + } +} + +// Call once per key, from setup(). +ESPPreferenceObject make_blob_pref(const std::string &name, size_t size) { + return global_preferences->make_preference(size, fnv1a_hash("sendspin_" + name)); +} + +// Loads straight into the returned buffer, which the library wipes after use. A failed load can +// leave part of a stored private key or PSK behind, so that path wipes it here. This only covers +// this buffer: the preference backend keeps its own copy of a pending write until it syncs. +std::optional> read_blob(ESPPreferenceObject &pref, size_t size) { + std::vector out(size); + if (!pref.load(out.data(), out.size())) { + secure_wipe(out.data(), out.size()); + return std::nullopt; + } + return out; +} + +// The library always writes a key's fixed size; any other length is misuse and is not stored. +bool write_blob(ESPPreferenceObject &pref, const char *key, size_t size, const uint8_t *data, size_t len) { + if (len != size) { + ESP_LOGW(TAG, "\"%s\" blob of %zu bytes does not match its size of %zu; rejecting write", key, len, size); + return false; + } + if (!pref.save(data, len)) { + ESP_LOGW(TAG, "Failed to persist \"%s\" blob (%zu bytes)", key, len); + return false; + } + return true; +} + +// "rec_" to n; nullopt for any other key or a slot with no storage here. +std::optional parse_record_slot(const std::string &key) { + const char *const prefix = sendspin::persistence_keys::RECORD_SLOT_PREFIX; + const size_t prefix_len = std::strlen(prefix); + if (key.size() <= prefix_len || key.compare(0, prefix_len, prefix) != 0) { + return std::nullopt; + } + size_t slot = 0; + for (size_t i = prefix_len; i < key.size(); i++) { + const char c = key[i]; + if (c < '0' || c > '9') { + return std::nullopt; + } + slot = slot * 10 + static_cast(c - '0'); + if (slot >= SENDSPIN_RECORD_SLOTS) { + return std::nullopt; + } + } + return slot; +} + +} // namespace + #ifdef USE_SENDSPIN_ARTWORK // Indexed by the library enums, which start at zero and are contiguous. static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"};