1 """Helpers for mobile_app."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Mapping
6 from http
import HTTPStatus
10 from aiohttp.web
import Response, json_response
11 from nacl.encoding
import Base64Encoder, HexEncoder, RawEncoder
12 from nacl.secret
import SecretBox
28 ATTR_NO_LEGACY_ENCRYPTION,
30 ATTR_SUPPORTS_ENCRYPTION,
37 _LOGGER = logging.getLogger(__name__)
41 key_encoder: type[RawEncoder | HexEncoder],
42 ) -> Callable[[bytes, bytes], bytes]:
43 """Return decryption function and length of key.
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
58 key_encoder: type[RawEncoder | HexEncoder],
59 ) -> Callable[[bytes, bytes], bytes]:
60 """Return encryption function and length of key.
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
78 key_encoder: type[RawEncoder | HexEncoder],
79 ) -> JsonValueType |
None:
80 """Decrypt encrypted payload."""
84 _LOGGER.warning(
"Ignoring encrypted payload because libsodium not installed")
88 _LOGGER.warning(
"Ignoring encrypted payload because no decryption key known")
91 msg_bytes = decrypt(ciphertext, key_bytes)
93 _LOGGER.debug(
"Successfully decrypted mobile_app payload")
98 """Decrypt encrypted payload."""
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")
111 """Decrypt encrypted payload."""
118 """Generate a context from a request."""
119 return Context(user_id=registration[CONF_USER_ID])
123 headers: dict |
None =
None, status: HTTPStatus = HTTPStatus.OK
125 """Return a Response with empty JSON object and a 200."""
127 text=
"{}", status=status, content_type=CONTENT_TYPE_JSON, headers=headers
134 status: HTTPStatus = HTTPStatus.BAD_REQUEST,
135 headers: dict |
None =
None,
137 """Return an error Response."""
138 return json_response(
139 {
"success":
False,
"error": {
"code": code,
"message": message}},
146 """Return a registration without sensitive values."""
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],
162 """Return a clean object containing things that should be saved."""
164 DATA_DELETED_IDS: hass.data[DOMAIN][DATA_DELETED_IDS],
171 registration: Mapping[str, Any],
172 status: HTTPStatus = HTTPStatus.OK,
173 headers: Mapping[str, str] |
None =
None,
175 """Return a encrypted response if registration supports it."""
178 if registration[ATTR_SUPPORTS_ENCRYPTION]:
180 HexEncoder
if ATTR_NO_LEGACY_ENCRYPTION
in registration
else RawEncoder
183 if ATTR_NO_LEGACY_ENCRYPTION
in registration:
184 key: bytes = registration[CONF_SECRET]
188 enc_data = encrypt(json_data, key).decode(
"utf-8")
189 json_data =
json_bytes({
"encrypted":
True,
"encrypted_data": enc_data})
192 body=json_data, status=status, content_type=CONTENT_TYPE_JSON, headers=headers
197 """Return the device info for this registration."""
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],
bytes _convert_legacy_encryption_key(str key)
JsonValueType|None decrypt_payload(str key, bytes ciphertext)
Callable[[bytes, bytes], bytes] setup_decrypt(type[RawEncoder|HexEncoder] key_encoder)
DeviceInfo device_info(dict registration)
Response error_response(str code, str message, HTTPStatus status=HTTPStatus.BAD_REQUEST, dict|None headers=None)
dict safe_registration(dict registration)
Context registration_context(Mapping[str, Any] registration)
Callable[[bytes, bytes], bytes] setup_encrypt(type[RawEncoder|HexEncoder] key_encoder)
Response empty_okay_response(dict|None headers=None, HTTPStatus status=HTTPStatus.OK)
JsonValueType|None _decrypt_payload_helper(str|bytes key, bytes ciphertext, bytes key_bytes, type[RawEncoder|HexEncoder] key_encoder)
Response webhook_response(Any data, *Mapping[str, Any] registration, HTTPStatus status=HTTPStatus.OK, Mapping[str, str]|None headers=None)
JsonValueType|None decrypt_payload_legacy(str key, bytes ciphertext)
dict savable_state(HomeAssistant hass)