diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index aca0901d2b..576887f6dc 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -1,13 +1,12 @@ -import base64 from pathlib import Path import re -import secrets from typing import Any from ruamel.yaml import YAML from esphome import git import esphome.codegen as cg +from esphome.components.noise import generate_encryption_key from esphome.components.packages import validate_source_shorthand import esphome.config_validation as cv from esphome.const import CONF_ESPHOME, CONF_PROJECT, CONF_REF, CONF_WIFI @@ -175,8 +174,7 @@ def import_config( "esphome": esphome_core, } if encryption: - noise_psk = secrets.token_bytes(32) - key = base64.b64encode(noise_psk).decode() + key = generate_encryption_key() config["api"] = {"encryption": {"key": key}} output = dump(config) diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index 1769c11018..781079c190 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -1,5 +1,6 @@ import base64 import binascii +import secrets from typing import Any import esphome.codegen as cg @@ -22,6 +23,11 @@ noise_ns = cg.esphome_ns.namespace("noise") CONFIG_SCHEMA = cv.Schema({}) +def generate_encryption_key() -> str: + """A fresh random key in the base64 form the yaml takes.""" + return base64.b64encode(secrets.token_bytes(32)).decode() + + def validate_encryption_key(value: Any) -> str: value = cv.string_strict(value) try: diff --git a/esphome/wizard.py b/esphome/wizard.py index 897d5f60a1..10ea0d0b4c 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,4 +1,3 @@ -import base64 from pathlib import Path import secrets import string @@ -7,6 +6,7 @@ import unicodedata import voluptuous as vol +from esphome.components.noise import generate_encryption_key import esphome.config_validation as cv from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD from esphome.core import CORE @@ -516,9 +516,7 @@ def wizard(path: Path) -> int: safe_print() sleep(0.5) - # Generate encryption key (32 bytes, base64 encoded) for secure API communication - noise_psk = secrets.token_bytes(32) - api_encryption_key = base64.b64encode(noise_psk).decode() + api_encryption_key = generate_encryption_key() safe_print( "For secure API communication, I've generated a random encryption key." diff --git a/tests/component_tests/noise/test_encryption_key.py b/tests/component_tests/noise/test_encryption_key.py index 2b79bd5464..3a895e25de 100644 --- a/tests/component_tests/noise/test_encryption_key.py +++ b/tests/component_tests/noise/test_encryption_key.py @@ -5,7 +5,11 @@ from __future__ import annotations import pytest from esphome import config_validation as cv -from esphome.components.noise import decode_encryption_key, validate_encryption_key +from esphome.components.noise import ( + decode_encryption_key, + generate_encryption_key, + validate_encryption_key, +) KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" @@ -42,3 +46,11 @@ def test_validate_encryption_key_rejects_all_zeros() -> None: key, so it never reaches a build.""" with pytest.raises(cv.Invalid, match="all-zeros key is reserved"): validate_encryption_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") + + +def test_generate_encryption_key_passes_the_validator() -> None: + """A fresh key is 32 random bytes in the base64 form the yaml takes.""" + key = generate_encryption_key() + assert validate_encryption_key(key) == key + assert len(decode_encryption_key(key)) == 32 + assert key != generate_encryption_key()