Skip to content

Commit

Permalink
Removed support for encrypted password
Browse files Browse the repository at this point in the history
  • Loading branch information
elad-bar committed Jul 23, 2020
1 parent 1417d8f commit 6f243fc
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 94 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## 2020-07-23

**Implemented enhancements:**

- Moved encryption key of component to .storage directory
- Removed support for non encrypted password (**Breaking Change**)

**Fixed bugs:**

- Better handling of password parsing

## 2020-07-21

**Fixed bugs:**
Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,15 @@ Component will try to login to the BlueIris server to verify new settings, follo
- Invalid administrator credentials - credentials are invalid or user is not an admin
- Invalid server details - Cannot reach the server

###### Password protection
Password is being saved in integration settings to `.storage` encrypted,

In the past password saved in clear text, to use the encryption, please remove the integration, restart HA and re-add integration,

As long as the password will remain in clear text saved in integration setting, the following warning log message will appear during restart:
###### Encryption key got corrupted
If a persistent notification popped up with the following message:
```
BlueIris password is not encrypted, please remove integration and reintegrate
Encryption key got corrupted, please remove the integration and re-add it
```

It means that encryption key was modified from outside the code,
Please remove the integration and re-add it to make it work again.

## Components

###### Binary Sensor - Alerts
Expand Down
2 changes: 1 addition & 1 deletion custom_components/blueiris/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
ha = get_ha(hass, entry.entry_id)

if ha is not None:
await ha.async_remove()
await ha.async_remove(entry)

clear_ha(hass, entry.entry_id)

Expand Down
2 changes: 1 addition & 1 deletion custom_components/blueiris/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async def async_step_user(self, user_input=None):

errors = {"base": "already_configured"}

schema = self._config_flow.get_default_data(new_user_input)
schema = await self._config_flow.get_default_data(new_user_input)

return self.async_show_form(
step_id="user",
Expand Down
52 changes: 37 additions & 15 deletions custom_components/blueiris/managers/config_flow_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
from typing import List, Optional

from cryptography.fernet import InvalidToken

from homeassistant.components.mqtt import DATA_MQTT
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers import config_validation as cv
Expand Down Expand Up @@ -64,9 +66,10 @@ def config_data(self) -> ConfigData:
return self._config_manager.data

async def update_options(self, options: dict, flow: str):
_LOGGER.debug("Update options")
validate_login = False

new_options = self._clone_items(options, flow)
new_options = await self._clone_items(options, flow)

if flow == CONFIG_FLOW_OPTIONS:
validate_login = self._should_validate_login(new_options)
Expand All @@ -77,17 +80,19 @@ async def update_options(self, options: dict, flow: str):

self._options = new_options

self._update_entry()
await self._update_entry()

if validate_login:
await self._handle_data(flow)

return new_options

async def update_data(self, data: dict, flow: str):
self._data = self._clone_items(data, flow)
_LOGGER.debug("Update data")

self._data = await self._clone_items(data, flow)

self._update_entry()
await self._update_entry()

await self._handle_data(flow)

Expand All @@ -107,8 +112,8 @@ def _get_default_fields(self, flow, config_data: Optional[ConfigData] = None):

return fields

def get_default_data(self, user_input):
config_data = self._config_manager.get_basic_data(user_input)
async def get_default_data(self, user_input):
config_data = await self._config_manager.get_basic_data(user_input)

fields = self._get_default_fields(CONFIG_FLOW_DATA, config_data)

Expand All @@ -121,6 +126,7 @@ def get_default_options(self):
ha = self._get_ha(self._config_entry.entry_id)

camera_list: List[CameraData] = ha.api.camera_list

is_admin = ha.api.data.get("admin", False)

profiles_list = ha.api.data.get("profiles", [])
Expand Down Expand Up @@ -211,12 +217,25 @@ def get_default_options(self):

return data_schema

def _update_entry(self):
entry = ConfigEntry(0, "", "", self._data, "", "", {}, options=self._options)
async def _update_entry(self):
try:
entry = ConfigEntry(
0, "", "", self._data, "", "", {}, options=self._options
)

await self._config_manager.update(entry)
except InvalidToken:
_LOGGER.info("Reset password")

self._config_manager.update(entry)
del self._data[CONF_PASSWORD]

def _handle_password(self, user_input):
entry = ConfigEntry(
0, "", "", self._data, "", "", {}, options=self._options
)

await self._config_manager.update(entry)

async def _handle_password(self, user_input):
if CONF_CLEAR_CREDENTIALS in user_input:
clear_credentials = user_input.get(CONF_CLEAR_CREDENTIALS)

Expand All @@ -228,11 +247,11 @@ def _handle_password(self, user_input):

if CONF_PASSWORD in user_input:
password_clear_text = user_input[CONF_PASSWORD]
password = self._password_manager.encrypt(password_clear_text)
password = await self._password_manager.encrypt(password_clear_text)

user_input[CONF_PASSWORD] = password

def _clone_items(self, user_input, flow: str):
async def _clone_items(self, user_input, flow: str):
new_user_input = {}

if user_input is not None:
Expand All @@ -242,7 +261,7 @@ def _clone_items(self, user_input, flow: str):
new_user_input[key] = user_input_data

if flow != CONFIG_FLOW_INIT:
self._handle_password(new_user_input)
await self._handle_password(new_user_input)

return new_user_input

Expand Down Expand Up @@ -286,9 +305,12 @@ async def _set_actions(self, options):

del options[action]

storage_manager = StorageManager(self._hass, self._config_manager)
generate_configuration_files = CONF_GENERATE_CONFIG_FILES in actions

storage_manager = StorageManager(self._hass)
data = await storage_manager.async_load_from_store()
data.actions = actions
integration_data = data.integrations.get(self.title, {})
integration_data[generate_configuration_files] = generate_configuration_files

await storage_manager.async_save_to_store(data)

Expand Down
12 changes: 8 additions & 4 deletions custom_components/blueiris/managers/configuration_manager.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import logging

from homeassistant.config_entries import ConfigEntry

from ..helpers.const import *
from ..models.camera_data import CameraData
from ..models.config_data import ConfigData
from .password_manager import PasswordManager

_LOGGER = logging.getLogger(__name__)


class ConfigManager:
data: ConfigData
Expand All @@ -14,11 +18,11 @@ class ConfigManager:
def __init__(self, password_manager: PasswordManager):
self.password_manager = password_manager

def update(self, config_entry: ConfigEntry):
async def update(self, config_entry: ConfigEntry):
data = config_entry.data
options = config_entry.options

result: ConfigData = self.get_basic_data(data)
result: ConfigData = await self.get_basic_data(data)

result.log_level = options.get(CONF_LOG_LEVEL, LOG_LEVEL_DEFAULT)

Expand All @@ -45,7 +49,7 @@ def update(self, config_entry: ConfigEntry):
self.config_entry = config_entry
self.data = result

def get_basic_data(self, data):
async def get_basic_data(self, data):
result = ConfigData()

if data is not None:
Expand All @@ -57,7 +61,7 @@ def get_basic_data(self, data):
result.password = data.get(CONF_PASSWORD)

if result.password is not None and len(result.password) > 0:
result.password_clear_text = self.password_manager.decrypt(
result.password_clear_text = await self.password_manager.decrypt(
result.password
)
else:
Expand Down
3 changes: 2 additions & 1 deletion custom_components/blueiris/managers/entity_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,10 @@ def create_components(self):
config_data = self.config_data
available_camera = self.api.camera_list
available_profiles = self.api.data.get("profiles", [])
is_admin = self.api.data.get("admin", False)
allowed_profile = config_data.allowed_profile

if allowed_profile is None or len(allowed_profile) > 0:
if is_admin and (allowed_profile is None or len(allowed_profile) > 0):
for profile_name in available_profiles:
profile_id = available_profiles.index(profile_name)

Expand Down
42 changes: 30 additions & 12 deletions custom_components/blueiris/managers/home_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import sys
from typing import Optional

from cryptography.fernet import InvalidToken

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
Expand Down Expand Up @@ -82,17 +84,33 @@ def config_data(self) -> Optional[ConfigData]:

async def async_init(self, entry: ConfigEntry):
try:
self._config_manager.update(entry)
self._storage_manager = StorageManager(self._hass)

await self._config_manager.update(entry)

self._api = BlueIrisApi(self._hass, self._config_manager)
self._entity_manager = EntityManager(self._hass, self)
self._device_manager = DeviceManager(self._hass, self)
self._storage_manager = StorageManager(self._hass, self.config_manager)
self._config_generator = AdvancedConfigurationGenerator(self._hass, self)

self._entity_registry = await er_async_get_registry(self._hass)

self._hass.loop.create_task(self._async_init())
except InvalidToken:
error_message = "Encryption key got corrupted, please remove the integration and re-add it"

_LOGGER.error(error_message)

data = await self._storage_manager.async_load_from_store()
data.key = None
await self._storage_manager.async_save_to_store(data)

await self._hass.services.async_call(
"persistent_notification",
"create",
{"title": DEFAULT_NAME, "message": error_message},
)

except Exception as ex:
exc_type, exc_obj, tb = sys.exc_info()
line_number = tb.tb_lineno
Expand Down Expand Up @@ -131,25 +149,25 @@ async def async_update_entry(self, entry: ConfigEntry = None):
_LOGGER.info(f"Handling ConfigEntry change: {entry.as_dict()}")

if update_config_manager:
self._config_manager.update(entry)
await self._config_manager.update(entry)

await self._api.initialize()

await self.async_update(datetime.now())

data = await self.storage_manager.async_load_from_store()
integration_data = data.integrations.get(entry.title)

if update_config_manager and CONF_GENERATE_CONFIG_FILES in data.actions:
async_call_later(self._hass, 5, self.generate_config_files)
if update_config_manager and integration_data is not None:
if integration_data.generate_configuration_files:
async_call_later(self._hass, 5, self.generate_config_files)

data.actions = []
integration_data.generate_configuration_files = False

await self.storage_manager.async_save_to_store(data)

async def async_remove(self):
config_entry = self._config_manager.config_entry

_LOGGER.info(f"Removing current integration - {config_entry.title}")
async def async_remove(self, entry: ConfigEntry):
_LOGGER.info(f"Removing current integration - {entry.title}")

if self._remove_async_track_time is not None:
self._remove_async_track_time()
Expand All @@ -158,11 +176,11 @@ async def async_remove(self):
unload = self._hass.config_entries.async_forward_entry_unload

for domain in SUPPORTED_DOMAINS:
await unload(config_entry, domain)
await unload(entry, domain)

await self._device_manager.async_remove()

_LOGGER.info(f"Current integration ({config_entry.title}) removed")
_LOGGER.info(f"Current integration ({entry.title}) removed")

async def async_update(self, event_time):
if not self._is_initialized:
Expand Down
Loading

0 comments on commit 6f243fc

Please sign in to comment.