Files
esphome/esphome/components/noise/__init__.py
T

100 lines
3.4 KiB
Python

import base64
import binascii
from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ENCRYPTION, CONF_KEY
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
noise_ns = cg.esphome_ns.namespace("noise")
CONFIG_SCHEMA = cv.Schema({})
def validate_encryption_key(value: Any) -> str:
value = cv.string_strict(value)
try:
decoded = base64.b64decode(value, validate=True)
except ValueError as err:
raise cv.Invalid("Invalid key format, please check it's using base64") from err
if len(decoded) != 32:
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
if not any(decoded):
# The device treats the all-zeros key as no key at all (it is the
# provisioning sentinel), so it must never reach a build
raise cv.Invalid(
f"The all-zeros {CONF_KEY} is reserved and provides no protection; "
f"omit the {CONF_KEY} to provision it at runtime, or generate a real "
"key with: openssl rand -base64 32"
)
# Return original data for roundtrip conversion
return value
def decode_encryption_key(value: str) -> bytes:
"""Decode a base64 encryption key to its 32 raw bytes.
a2b_base64 matches the decode the clients use (aioesphomeapi
decode_noise_psk), so both ends derive the same bytes. The length is
re-checked so a caller cannot turn an unvalidated short decode into a
zero-padded PSK.
"""
try:
decoded = binascii.a2b_base64(value)
except ValueError as err:
raise cv.Invalid("Invalid key format, please check it's using base64") from err
if len(decoded) != 32:
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
return decoded
ENCRYPTION_SCHEMA = cv.Schema(
{
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
}
)
def static_encryption_key(conf: ConfigType) -> str | None:
"""The build time key of a component config; None without one or when
the key is provisioned at runtime."""
return (conf.get(CONF_ENCRYPTION) or {}).get(CONF_KEY) or None
def new_psk_progmem(parent_id: ID, key: str) -> MockObj:
"""Emit the decoded key as a PROGMEM array; the component keeps a pointer
so the key never occupies RAM."""
return cg.progmem_array(
ID(f"{parent_id.id}_psk", is_declaration=True, type=cg.uint8),
list(decode_encryption_key(key)),
)
def encryption_schema(config: ConfigType | None) -> ConfigType:
# A bare `encryption:` block is valid; a missing key means the consumer
# falls back to its keyless behavior (api provisioning, ota inheriting
# the api key).
if config is None:
config = {}
return ENCRYPTION_SCHEMA(config)
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_NOISE")
cg.add_library("esphome/noise-c", "0.1.26")
# noise-c depends on libsodium, but declaring it here too lets the
# library manager see the full set up front instead of discovering
# libsodium only after noise-c has downloaded, so the two can download
# in parallel. The version must match noise-c's library.json.
cg.add_library("esphome/libsodium", "1.10021.8")
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")