From 7104ba20658818a378d9c97b31b4f916821be3a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Mon, 29 Oct 2018 22:47:03 +0100 Subject: [PATCH 1/4] Implement hmac for incoming messages --- app/api/blueprint.py | 5 +- app/api/resources/device.py | 12 +++++ .../swagger/get_device_secret_spec.yaml | 20 ++++++++ app/devices/api.py | 23 ++++++++- app/devices/models.py | 5 ++ app/devices/tasks.py | 19 ++++--- app/swagger/template.yaml | 21 ++++++++ migrations/versions/dad1f9b4eec2_.py | 51 +++++++++++++++++++ 8 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 app/api/resources/swagger/get_device_secret_spec.yaml create mode 100644 migrations/versions/dad1f9b4eec2_.py diff --git a/app/api/blueprint.py b/app/api/blueprint.py index 3038b47..eabe425 100644 --- a/app/api/blueprint.py +++ b/app/api/blueprint.py @@ -21,7 +21,8 @@ def add_resources(): DeviceListResource, DeviceTypeResource, DeviceTypeListResource, - DeviceConfigurationResource) + DeviceConfigurationResource, + DeviceSecretResource) from .resources.dashboard import (DashboardResource, DashboardListResource, DashboardWidgetResource, @@ -47,6 +48,8 @@ def add_resources(): api.add_resource(DeviceTypeListResource, '/v1/devices/types') api.add_resource(DeviceConfigurationResource, '/v1/devices//configuration') + api.add_resource(DeviceSecretResource, + '/v1/devices//secret') api.add_resource(DashboardResource, '/v1/dashboards/') api.add_resource(DashboardListResource, '/v1/dashboards') diff --git a/app/api/resources/device.py b/app/api/resources/device.py index 8f6e120..5a29376 100644 --- a/app/api/resources/device.py +++ b/app/api/resources/device.py @@ -34,6 +34,11 @@ class RecordingsSchema(BaseResourceSchema): record_value = fields.String() +class DeviceSecretSchema(BaseResourceSchema): + device_secret = fields.String(dump_only=True) + secret_algorithm = fields.String() + + def validate_device_ownership(device_id): if not devices.can_user_access_device(g.current_account.id, device_id): abort(403, message='You are not allowed to access this device', @@ -127,3 +132,10 @@ class DeviceConfigurationResource(ProtectedResource): def get(self, device_id): validate_device_ownership(device_id) return devices.get_device_configuration(device_id), 200 + + +class DeviceSecretResource(ProtectedResource): + @swag_from('swagger/get_device_secret_spec.yaml') + def get(self, device_id): + validate_device_ownership(device_id) + return DeviceSecretSchema().dump(devices.get_device(device_id)), 200 diff --git a/app/api/resources/swagger/get_device_secret_spec.yaml b/app/api/resources/swagger/get_device_secret_spec.yaml new file mode 100644 index 0000000..fa757a6 --- /dev/null +++ b/app/api/resources/swagger/get_device_secret_spec.yaml @@ -0,0 +1,20 @@ +Gets a device secret info +--- +tags: + - Device +parameters: + - in: path + name: device_id + required: true + type: integer + description: Id of the device +responses: + 200: + description: Success + schema: + type: object + required: + - content + properties: + content: + $ref: '#/definitions/DeviceWithConfig' diff --git a/app/devices/api.py b/app/devices/api.py index 23b3540..6680f7d 100644 --- a/app/devices/api.py +++ b/app/devices/api.py @@ -1,4 +1,6 @@ import sys +import hmac +import urllib.parse from .models import Device, Recording, DeviceAssociation, DeviceType from app.core import app @@ -178,7 +180,7 @@ def get_device_types(): def parse_raw_json_recording(device_id, json_msg): """ - Parses raw json recording and creates Recrding object + Parses raw json recording and creates Recording object :param device_id: Id of device :type device_id: int @@ -198,7 +200,20 @@ def parse_raw_json_recording(device_id, json_msg): + str(error_instance)) -def create_recording_and_return(device_id, raw_json): +def validate_hmac_in_message(device_id, raw_json): + device = Device.get(id=device_id) + hmac_value = raw_json.pop('hmac', None) + raw_json_bytes = urllib.parse.urlencode(raw_json).encode('utf-8') + calculated_hmac = hmac.new( + bytes(device.device_secret, 'utf-8'), + raw_json_bytes, + device.secret_algorithm).hexdigest() + if not hmac.compare_digest(hmac_value, calculated_hmac): + raise ValueError("Bad hmac") + + +def create_recording_and_return(device_id, raw_json, + authenticated=False): """ Tries to create recording with given parameters and returns it. Raises error on failure @@ -212,6 +227,9 @@ def create_recording_and_return(device_id, raw_json): if not Device.exists(id=device_id): raise ValueError("Device does not exist!") + if not authenticated: + validate_hmac_in_message(device_id, raw_json) + recording = parse_raw_json_recording(device_id, raw_json) recording.save() return recording @@ -230,6 +248,7 @@ def create_recording(device_id, raw_json): if not Device.exists(id=device_id): raise ValueError("Device does not exist!") + validate_hmac_in_message(device_id, raw_json) recording = parse_raw_json_recording(device_id, raw_json) with app.app_context(): recording.save() diff --git a/app/devices/models.py b/app/devices/models.py index 341f070..13c5b15 100644 --- a/app/devices/models.py +++ b/app/devices/models.py @@ -3,6 +3,7 @@ from datetime import datetime import datetime as datetime_module from app.core import db from sqlalchemy.dialects.postgresql import JSON +from secrets import token_urlsafe class Recording(db.Model): @@ -126,6 +127,8 @@ class Device(db.Model): name = db.Column(db.String, nullable=False) device_type_id = db.Column(db.Integer, db.ForeignKey('device_types.id')) device_type = db.relationship("DeviceType", foreign_keys=[device_type_id]) + device_secret = db.Column(db.String, nullable=False) + secret_algorithm = db.Column(db.String, nullable=False) configuration = db.Column(JSON, nullable=True) users = db.relationship("DeviceAssociation", @@ -137,6 +140,8 @@ class Device(db.Model): self.name = name self.configuration = configuration self.device_type_id = device_type + self.secret_algorithm = 'sha512' + self.device_secret = token_urlsafe(32) def save(self): """ diff --git a/app/devices/tasks.py b/app/devices/tasks.py index 5538157..4aa6f7d 100644 --- a/app/devices/tasks.py +++ b/app/devices/tasks.py @@ -3,8 +3,7 @@ from app.celery_builder import task_builder from flask import current_app as app -@task_builder.task() -def send_config(device_id, config): +def connect_and_send_mqtt_message(topic, message): from flask_mqtt import Mqtt, MQTT_ERR_SUCCESS mqtt = Mqtt(app) @@ -15,14 +14,12 @@ def send_config(device_id, config): @mqtt.on_connect() def handle_connect(client, userdata, flags, rc): print('MQTT worker client connected') - print("Sending configuration to device: " + str(device_id)) - print("Configuration: " + str(config)) - topic = 'device/' + str(device_id) + '/config' print("Targeting topic: " + topic) + print("Sending message: " + message) try: - (result, mid) = mqtt.publish(topic, config, 2) + (result, mid) = mqtt.publish(topic, message, 2) if (result == MQTT_ERR_SUCCESS): - print("Success!!!") + print("Successfully sent a message") print("Result: " + str(result)) print("Message id: " + str(mid)) mqtt.client.disconnect() @@ -33,3 +30,11 @@ def send_config(device_id, config): print("Instance: " + str(error_instance)) mqtt.client.disconnect() return + + +@task_builder.task() +def send_config(device_id, config): + print("Sending configuration to device: " + str(device_id)) + print("Configuration: " + str(config)) + topic = 'device/' + str(device_id) + '/config' + connect_and_send_mqtt_message(topic, config) diff --git a/app/swagger/template.yaml b/app/swagger/template.yaml index cd62c37..7d5a143 100644 --- a/app/swagger/template.yaml +++ b/app/swagger/template.yaml @@ -94,6 +94,16 @@ definitions: description: Type of chart example: line + secret: + type: string + description: Secret key + example: Ranom-Key123 + + hashalgorithm: + type: string + description: Hashing algorithm used + example: sha512 + Credentials: type: object required: @@ -209,6 +219,17 @@ definitions: configuration: $ref: '#/definitions/configuration' + DeviceSecretInfo: + type: object + required: + - device_secret + - secret_algorithm + properties: + device_secret: + $ref: '#/definitions/secret' + secret_algorithm: + $ref: '#/definitions/hashalgorithm' + DeviceCreation: type: object required: diff --git a/migrations/versions/dad1f9b4eec2_.py b/migrations/versions/dad1f9b4eec2_.py new file mode 100644 index 0000000..fb0eb54 --- /dev/null +++ b/migrations/versions/dad1f9b4eec2_.py @@ -0,0 +1,51 @@ +"""empty message + +Revision ID: dad1f9b4eec2 +Revises: b5eb4a04c77e +Create Date: 2018-10-29 20:48:49.588509 + +""" +from alembic import op +from secrets import token_urlsafe +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'dad1f9b4eec2' +down_revision = 'b5eb4a04c77e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('devices', sa.Column('device_secret', sa.String(), + nullable=True)) + op.add_column('devices', sa.Column('secret_algorithm', sa.String(), + nullable=True)) + + devices = sa.sql.table('devices', sa.column('id', sa.Integer), + sa.sql.column('device_secret'), + sa.sql.column('secret_algorithm')) + + conn = op.get_bind() + res = conn.execute("select id from devices") + results = res.fetchall() + for result in results: + op.execute(devices.update().where(devices.c.id == + op.inline_literal(result[0])). + values({'device_secret': token_urlsafe(32), + 'secret_algorithm': 'sha512'}) + ) + + # Forbid nulls + op.alter_column('devices', 'device_secret', nullable=False) + op.alter_column('devices', 'secret_algorithm', nullable=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('devices', 'secret_algorithm') + op.drop_column('devices', 'device_secret') + # ### end Alembic commands ### From 550f4e4efba7774574affccf143617bbf0e18d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Mon, 29 Oct 2018 22:56:58 +0100 Subject: [PATCH 2/4] Add hmac for outbound messages --- app/devices/api.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/app/devices/api.py b/app/devices/api.py index 6680f7d..18e6536 100644 --- a/app/devices/api.py +++ b/app/devices/api.py @@ -5,6 +5,23 @@ from .models import Device, Recording, DeviceAssociation, DeviceType from app.core import app +# Private helpers +def generate_hmac_for_message(device_id, raw_json): + device = Device.get(id=device_id) + raw_json_bytes = urllib.parse.urlencode(raw_json).encode('utf-8') + return hmac.new( + bytes(device.device_secret, 'utf-8'), + raw_json_bytes, + device.secret_algorithm).hexdigest() + + +def validate_hmac_in_message(device_id, raw_json): + hmac_value = raw_json.pop('hmac', None) + calculated_hmac = generate_hmac_for_message(device_id, raw_json) + if not hmac.compare_digest(hmac_value, calculated_hmac): + raise ValueError("Bad hmac") + + # Public interface def create_device(name, account_id, device_type=1): """ @@ -53,6 +70,9 @@ def set_device_configuration(device_id, configuration_json): device = Device.get(id=device_id) device.configuration = configuration_json device.save() + configuration_json['hmac'] = generate_hmac_for_message( + device_id, + configuration_json) send_config.delay(device_id, str(configuration_json)) return device @@ -200,18 +220,6 @@ def parse_raw_json_recording(device_id, json_msg): + str(error_instance)) -def validate_hmac_in_message(device_id, raw_json): - device = Device.get(id=device_id) - hmac_value = raw_json.pop('hmac', None) - raw_json_bytes = urllib.parse.urlencode(raw_json).encode('utf-8') - calculated_hmac = hmac.new( - bytes(device.device_secret, 'utf-8'), - raw_json_bytes, - device.secret_algorithm).hexdigest() - if not hmac.compare_digest(hmac_value, calculated_hmac): - raise ValueError("Bad hmac") - - def create_recording_and_return(device_id, raw_json, authenticated=False): """ From a9ec5dd741d4dc1008d01943a536407a66440140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Mon, 29 Oct 2018 22:59:23 +0100 Subject: [PATCH 3/4] Fix swagger docs for get device secret info --- app/api/resources/swagger/get_device_secret_spec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/resources/swagger/get_device_secret_spec.yaml b/app/api/resources/swagger/get_device_secret_spec.yaml index fa757a6..7642b1d 100644 --- a/app/api/resources/swagger/get_device_secret_spec.yaml +++ b/app/api/resources/swagger/get_device_secret_spec.yaml @@ -17,4 +17,4 @@ responses: - content properties: content: - $ref: '#/definitions/DeviceWithConfig' + $ref: '#/definitions/DeviceSecretInfo' From e7e75fc658cb3f83cf9ffbce09979dbbb0674319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ensar=20Saraj=C4=8Di=C4=87?= Date: Mon, 29 Oct 2018 23:25:47 +0100 Subject: [PATCH 4/4] Version 0.3.3 --- config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.py b/config.py index 0f2a672..f286029 100644 --- a/config.py +++ b/config.py @@ -2,7 +2,7 @@ import os # App configuration DEBUG = os.environ['DEBUG'] -APP_VERSION = '0.3.2' +APP_VERSION = '0.3.3' # Define the application directory BASE_DIR = os.path.abspath(os.path.dirname(__file__))