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] 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 ###