Home Assistant Unofficial Reference 2024.12.1
helpers.py
Go to the documentation of this file.
1 """Helpers for mobile_app."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Mapping
6 from http import HTTPStatus
7 import logging
8 from typing import Any
9 
10 from aiohttp.web import Response, json_response
11 from nacl.encoding import Base64Encoder, HexEncoder, RawEncoder
12 from nacl.secret import SecretBox
13 
14 from homeassistant.const import ATTR_DEVICE_ID, CONTENT_TYPE_JSON
15 from homeassistant.core import Context, HomeAssistant
16 from homeassistant.helpers.device_registry import DeviceInfo
17 from homeassistant.helpers.json import json_bytes
18 from homeassistant.util.json import JsonValueType, json_loads
19 
20 from .const import (
21  ATTR_APP_DATA,
22  ATTR_APP_ID,
23  ATTR_APP_NAME,
24  ATTR_APP_VERSION,
25  ATTR_DEVICE_NAME,
26  ATTR_MANUFACTURER,
27  ATTR_MODEL,
28  ATTR_NO_LEGACY_ENCRYPTION,
29  ATTR_OS_VERSION,
30  ATTR_SUPPORTS_ENCRYPTION,
31  CONF_SECRET,
32  CONF_USER_ID,
33  DATA_DELETED_IDS,
34  DOMAIN,
35 )
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 
41  key_encoder: type[RawEncoder | HexEncoder],
42 ) -> Callable[[bytes, bytes], bytes]:
43  """Return decryption function and length of key.
44 
45  Async friendly.
46  """
47 
48  def decrypt(ciphertext: bytes, key: bytes) -> bytes:
49  """Decrypt ciphertext using key."""
50  return SecretBox(key, encoder=key_encoder).decrypt(
51  ciphertext, encoder=Base64Encoder
52  )
53 
54  return decrypt
55 
56 
58  key_encoder: type[RawEncoder | HexEncoder],
59 ) -> Callable[[bytes, bytes], bytes]:
60  """Return encryption function and length of key.
61 
62  Async friendly.
63  """
64 
65  def encrypt(ciphertext: bytes, key: bytes) -> bytes:
66  """Encrypt ciphertext using key."""
67  return SecretBox(key, encoder=key_encoder).encrypt(
68  ciphertext, encoder=Base64Encoder
69  )
70 
71  return encrypt
72 
73 
75  key: str | bytes,
76  ciphertext: bytes,
77  key_bytes: bytes,
78  key_encoder: type[RawEncoder | HexEncoder],
79 ) -> JsonValueType | None:
80  """Decrypt encrypted payload."""
81  try:
82  decrypt = setup_decrypt(key_encoder)
83  except OSError:
84  _LOGGER.warning("Ignoring encrypted payload because libsodium not installed")
85  return None
86 
87  if key is None:
88  _LOGGER.warning("Ignoring encrypted payload because no decryption key known")
89  return None
90 
91  msg_bytes = decrypt(ciphertext, key_bytes)
92  message = json_loads(msg_bytes)
93  _LOGGER.debug("Successfully decrypted mobile_app payload")
94  return message
95 
96 
97 def decrypt_payload(key: str, ciphertext: bytes) -> JsonValueType | None:
98  """Decrypt encrypted payload."""
99  return _decrypt_payload_helper(key, ciphertext, key.encode("utf-8"), HexEncoder)
100 
101 
102 def _convert_legacy_encryption_key(key: str) -> bytes:
103  """Convert legacy encryption key."""
104  keylen = SecretBox.KEY_SIZE
105  key_bytes = key.encode("utf-8")
106  key_bytes = key_bytes[:keylen]
107  return key_bytes.ljust(keylen, b"\0")
108 
109 
110 def decrypt_payload_legacy(key: str, ciphertext: bytes) -> JsonValueType | None:
111  """Decrypt encrypted payload."""
113  key, ciphertext, _convert_legacy_encryption_key(key), RawEncoder
114  )
115 
116 
117 def registration_context(registration: Mapping[str, Any]) -> Context:
118  """Generate a context from a request."""
119  return Context(user_id=registration[CONF_USER_ID])
120 
121 
123  headers: dict | None = None, status: HTTPStatus = HTTPStatus.OK
124 ) -> Response:
125  """Return a Response with empty JSON object and a 200."""
126  return Response(
127  text="{}", status=status, content_type=CONTENT_TYPE_JSON, headers=headers
128  )
129 
130 
132  code: str,
133  message: str,
134  status: HTTPStatus = HTTPStatus.BAD_REQUEST,
135  headers: dict | None = None,
136 ) -> Response:
137  """Return an error Response."""
138  return json_response(
139  {"success": False, "error": {"code": code, "message": message}},
140  status=status,
141  headers=headers,
142  )
143 
144 
145 def safe_registration(registration: dict) -> dict:
146  """Return a registration without sensitive values."""
147  # Sensitive values: webhook_id, secret, cloudhook_url
148  return {
149  ATTR_APP_DATA: registration[ATTR_APP_DATA],
150  ATTR_APP_ID: registration[ATTR_APP_ID],
151  ATTR_APP_NAME: registration[ATTR_APP_NAME],
152  ATTR_APP_VERSION: registration[ATTR_APP_VERSION],
153  ATTR_DEVICE_NAME: registration[ATTR_DEVICE_NAME],
154  ATTR_MANUFACTURER: registration[ATTR_MANUFACTURER],
155  ATTR_MODEL: registration[ATTR_MODEL],
156  ATTR_OS_VERSION: registration[ATTR_OS_VERSION],
157  ATTR_SUPPORTS_ENCRYPTION: registration[ATTR_SUPPORTS_ENCRYPTION],
158  }
159 
160 
161 def savable_state(hass: HomeAssistant) -> dict:
162  """Return a clean object containing things that should be saved."""
163  return {
164  DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS],
165  }
166 
167 
169  data: Any,
170  *,
171  registration: Mapping[str, Any],
172  status: HTTPStatus = HTTPStatus.OK,
173  headers: Mapping[str, str] | None = None,
174 ) -> Response:
175  """Return a encrypted response if registration supports it."""
176  json_data = json_bytes(data)
177 
178  if registration[ATTR_SUPPORTS_ENCRYPTION]:
179  encrypt = setup_encrypt(
180  HexEncoder if ATTR_NO_LEGACY_ENCRYPTION in registration else RawEncoder
181  )
182 
183  if ATTR_NO_LEGACY_ENCRYPTION in registration:
184  key: bytes = registration[CONF_SECRET]
185  else:
186  key = _convert_legacy_encryption_key(registration[CONF_SECRET])
187 
188  enc_data = encrypt(json_data, key).decode("utf-8")
189  json_data = json_bytes({"encrypted": True, "encrypted_data": enc_data})
190 
191  return Response(
192  body=json_data, status=status, content_type=CONTENT_TYPE_JSON, headers=headers
193  )
194 
195 
196 def device_info(registration: dict) -> DeviceInfo:
197  """Return the device info for this registration."""
198  return DeviceInfo(
199  identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])},
200  manufacturer=registration[ATTR_MANUFACTURER],
201  model=registration[ATTR_MODEL],
202  name=registration[ATTR_DEVICE_NAME],
203  sw_version=registration[ATTR_OS_VERSION],
204  )
JsonValueType|None decrypt_payload(str key, bytes ciphertext)
Definition: helpers.py:97
Callable[[bytes, bytes], bytes] setup_decrypt(type[RawEncoder|HexEncoder] key_encoder)
Definition: helpers.py:42
DeviceInfo device_info(dict registration)
Definition: helpers.py:196
Response error_response(str code, str message, HTTPStatus status=HTTPStatus.BAD_REQUEST, dict|None headers=None)
Definition: helpers.py:136
dict safe_registration(dict registration)
Definition: helpers.py:145
Context registration_context(Mapping[str, Any] registration)
Definition: helpers.py:117
Callable[[bytes, bytes], bytes] setup_encrypt(type[RawEncoder|HexEncoder] key_encoder)
Definition: helpers.py:59
Response empty_okay_response(dict|None headers=None, HTTPStatus status=HTTPStatus.OK)
Definition: helpers.py:124
JsonValueType|None _decrypt_payload_helper(str|bytes key, bytes ciphertext, bytes key_bytes, type[RawEncoder|HexEncoder] key_encoder)
Definition: helpers.py:79
Response webhook_response(Any data, *Mapping[str, Any] registration, HTTPStatus status=HTTPStatus.OK, Mapping[str, str]|None headers=None)
Definition: helpers.py:174
JsonValueType|None decrypt_payload_legacy(str key, bytes ciphertext)
Definition: helpers.py:110
dict savable_state(HomeAssistant hass)
Definition: helpers.py:161