From 44b0a1100f19225fef098785db92d78af5ef75e1 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 10 Sep 2020 15:14:37 +0300 Subject: [PATCH 1/3] Add new device credentials MQTT_BASIC --- .../device-credentials-dialog.component.html | 31 +++++++ .../device-credentials-dialog.component.ts | 85 +++++++++++++++++-- ui-ngx/src/app/shared/models/device.models.ts | 14 ++- .../assets/locale/locale.constant-en_US.json | 6 ++ 4 files changed, 126 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html index 1cf9911aa8..f1b5b442ce 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html @@ -58,6 +58,37 @@ {{ 'device.rsa-key-required' | translate }} +
+ + device.client-id + + + {{ 'device.client-id-pattern' | translate }} + + + + device.user-name + + + {{ 'device.user-name-required' | translate }} + + + + device.password + + + + + {{ 'device.client-id-or-user-name-necessary' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts index ccbe1affe8..4cfcc90a2a 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts @@ -19,9 +19,23 @@ import { ErrorStateMatcher } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { + FormBuilder, + FormControl, + FormGroup, + FormGroupDirective, + NgForm, + ValidationErrors, + ValidatorFn, + Validators +} from '@angular/forms'; import { DeviceService } from '@core/http/device.service'; -import { credentialTypeNames, DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models'; +import { + credentialTypeNames, + DeviceCredentialMQTTBasic, + DeviceCredentials, + DeviceCredentialsType +} from '@shared/models/device.models'; import { DialogComponent } from '@shared/components/dialog.component'; import { Router } from '@angular/router'; @@ -53,6 +67,8 @@ export class DeviceCredentialsDialogComponent extends credentialTypeNamesMap = credentialTypeNames; + hidePassword = true; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogData, @@ -69,7 +85,12 @@ export class DeviceCredentialsDialogComponent extends this.deviceCredentialsFormGroup = this.fb.group({ credentialsType: [DeviceCredentialsType.ACCESS_TOKEN], credentialsId: [''], - credentialsValue: [''] + credentialsValue: [''], + credentialsBasic: this.fb.group({ + clientId: ['', [Validators.pattern(/^[A-Za-z0-9]+$/)]], + userName: [''], + password: [''] + }, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])}) }); if (this.isReadOnly) { this.deviceCredentialsFormGroup.disable({emitEvent: false}); @@ -89,10 +110,17 @@ export class DeviceCredentialsDialogComponent extends this.deviceService.getDeviceCredentials(this.data.deviceId).subscribe( (deviceCredentials) => { this.deviceCredentials = deviceCredentials; + let credentialsValue = deviceCredentials.credentialsValue; + let credentialsBasic = {clientId: null, userName: null, password: null}; + if (deviceCredentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) { + credentialsValue = null; + credentialsBasic = deviceCredentials.credentialsValue as DeviceCredentialMQTTBasic; + } this.deviceCredentialsFormGroup.patchValue({ credentialsType: deviceCredentials.credentialsType, credentialsId: deviceCredentials.credentialsId, - credentialsValue: deviceCredentials.credentialsValue + credentialsValue, + credentialsBasic }); this.updateValidators(); } @@ -100,12 +128,16 @@ export class DeviceCredentialsDialogComponent extends } credentialsTypeChanged(): void { - this.deviceCredentialsFormGroup.patchValue( - {credentialsId: null, credentialsValue: null}, {emitEvent: true}); + this.deviceCredentialsFormGroup.patchValue({ + credentialsId: null, + credentialsValue: null, + credentialsBasic: {clientId: '', userName: '', password: ''} + }, {emitEvent: true}); this.updateValidators(); } updateValidators(): void { + this.hidePassword = true; const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType; switch (crendetialsType) { case DeviceCredentialsType.ACCESS_TOKEN: @@ -113,27 +145,66 @@ export class DeviceCredentialsDialogComponent extends this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity(); this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsBasic').disable(); break; case DeviceCredentialsType.X509_CERTIFICATE: this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]); this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity(); this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsBasic').disable(); break; + case DeviceCredentialsType.MQTT_BASIC: + this.deviceCredentialsFormGroup.get('credentialsBasic').enable(); + this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity(); } } + private atLeastOne(validator: ValidatorFn, controls: string[] = null) { + return (group: FormGroup): ValidationErrors | null => { + if (!controls) { + controls = Object.keys(group.controls); + } + const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k])); + + return hasAtLeastOne ? null : {atLeastOne: true}; + }; + } + cancel(): void { this.dialogRef.close(null); } save(): void { this.submitted = true; - this.deviceCredentials = {...this.deviceCredentials, ...this.deviceCredentialsFormGroup.value}; + const deviceCredentialsValue = this.deviceCredentialsFormGroup.value; + if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) { + deviceCredentialsValue.credentialsValue = deviceCredentialsValue.credentialsBasic; + } + delete deviceCredentialsValue.credentialsBasic; + this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue}; this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe( (deviceCredentials) => { this.dialogRef.close(deviceCredentials); } ); } + + passwordChanged() { + const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value; + if (value !== '') { + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]); + if (this.deviceCredentialsFormGroup.get('credentialsBasic.userName').untouched) { + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').markAsTouched({onlySelf: true}); + } + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity(); + } else { + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity(); + } + } } diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index 264ec6c70d..a71c5db36c 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -292,13 +292,15 @@ export interface DeviceInfo extends Device { export enum DeviceCredentialsType { ACCESS_TOKEN = 'ACCESS_TOKEN', - X509_CERTIFICATE = 'X509_CERTIFICATE' + X509_CERTIFICATE = 'X509_CERTIFICATE', + MQTT_BASIC = 'MQTT_BASIC' } export const credentialTypeNames = new Map( [ [DeviceCredentialsType.ACCESS_TOKEN, 'Access token'], - [DeviceCredentialsType.X509_CERTIFICATE, 'X.509 Certificate'], + [DeviceCredentialsType.X509_CERTIFICATE, 'MQTT X.509'], + [DeviceCredentialsType.MQTT_BASIC, 'MQTT Basic'] ] ); @@ -306,7 +308,13 @@ export interface DeviceCredentials extends BaseData { deviceId: DeviceId; credentialsType: DeviceCredentialsType; credentialsId: string; - credentialsValue: string; + credentialsValue: string | DeviceCredentialMQTTBasic; +} + +export interface DeviceCredentialMQTTBasic { + clientId: string; + userName: string; + password: string; } export interface DeviceSearchQuery extends EntitySearchQuery { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 9a41d1bf2a..91d78a4147 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -718,6 +718,12 @@ "access-token-invalid": "Access token length must be from 1 to 20 characters.", "rsa-key": "RSA public key", "rsa-key-required": "RSA public key is required.", + "client-id": "Client ID", + "client-id-pattern": "Contains invalid character.", + "user-name": "User Name", + "user-name-required": "User Name is required.", + "client-id-or-user-name-necessary": "Client ID and/or User Name are necessary", + "password": "Password", "secret": "Secret", "secret-required": "Secret is required.", "device-type": "Device type", From 6e492c78d1a16dfdb6bd3d569f4c130fcf815702 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Thu, 10 Sep 2020 15:37:25 +0300 Subject: [PATCH 2/3] Change device credentials value type --- .../home/pages/device/device-credentials-dialog.component.ts | 4 ++-- ui-ngx/src/app/shared/models/device.models.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts index 4cfcc90a2a..73e5ae040f 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts @@ -114,7 +114,7 @@ export class DeviceCredentialsDialogComponent extends let credentialsBasic = {clientId: null, userName: null, password: null}; if (deviceCredentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) { credentialsValue = null; - credentialsBasic = deviceCredentials.credentialsValue as DeviceCredentialMQTTBasic; + credentialsBasic = JSON.parse(deviceCredentials.credentialsValue) as DeviceCredentialMQTTBasic; } this.deviceCredentialsFormGroup.patchValue({ credentialsType: deviceCredentials.credentialsType, @@ -183,7 +183,7 @@ export class DeviceCredentialsDialogComponent extends this.submitted = true; const deviceCredentialsValue = this.deviceCredentialsFormGroup.value; if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) { - deviceCredentialsValue.credentialsValue = deviceCredentialsValue.credentialsBasic; + deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic); } delete deviceCredentialsValue.credentialsBasic; this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue}; diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index a71c5db36c..fe92268b80 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -308,7 +308,7 @@ export interface DeviceCredentials extends BaseData { deviceId: DeviceId; credentialsType: DeviceCredentialsType; credentialsId: string; - credentialsValue: string | DeviceCredentialMQTTBasic; + credentialsValue: string; } export interface DeviceCredentialMQTTBasic { From a3326b4464705b125fab923040d8564c98777b3c Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Thu, 10 Sep 2020 17:17:42 +0300 Subject: [PATCH 3/3] New type of credentials: MQTT --- .../transport/DefaultTransportApiService.java | 70 ++++++++++++++++++- .../credentials/BasicMqttCredentials.java | 27 +++++++ .../server/common/msg/EncryptionUtil.java | 21 +++++- common/queue/src/main/proto/queue.proto | 9 ++- .../transport/mqtt/MqttTransportHandler.java | 39 ++++++----- .../common/transport/TransportService.java | 6 +- .../service/DefaultTransportService.java | 20 ++++-- .../device/DeviceCredentialsServiceImpl.java | 42 ++++++++++- 8 files changed, 201 insertions(+), 33 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index dcab1114fd..16c7c894da 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -23,12 +23,14 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -36,6 +38,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -46,6 +49,7 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; @@ -91,6 +95,7 @@ public class DefaultTransportApiService implements TransportApiService { private final TbClusterService tbClusterService; private final DataDecodingEncodingService dataDecodingEncodingService; + private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>(); public DefaultTransportApiService(DeviceProfileService deviceProfileService, TenantService tenantService, @@ -117,6 +122,10 @@ public class DefaultTransportApiService implements TransportApiService { ValidateDeviceTokenRequestMsg msg = transportApiRequestMsg.getValidateTokenRequestMsg(); return Futures.transform(validateCredentials(msg.getToken(), DeviceCredentialsType.ACCESS_TOKEN), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); + } else if (transportApiRequestMsg.hasValidateBasicMqttCredRequestMsg()) { + TransportProtos.ValidateBasicMqttCredRequestMsg msg = transportApiRequestMsg.getValidateBasicMqttCredRequestMsg(); + return Futures.transform(validateCredentials(msg), + value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); } else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) { ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg(); return Futures.transform(validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE), @@ -130,7 +139,6 @@ public class DefaultTransportApiService implements TransportApiService { } else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) { return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); - } return Futures.transform(getEmptyTransportApiResponseFuture(), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor()); @@ -146,6 +154,62 @@ public class DefaultTransportApiService implements TransportApiService { } } + private ListenableFuture validateCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) { + DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName()); + if (credentials != null) { + if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) { + return getDeviceInfo(credentials.getDeviceId(), credentials); + } else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) { + if (!checkMqttCredentials(mqtt, credentials)) { + credentials = null; + } + } + } + if (credentials == null) { + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName())); + if (credentials == null) { + credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId())); + } + } + if (credentials != null) { + return getDeviceInfo(credentials.getDeviceId(), credentials); + } else { + return getEmptyTransportApiResponseFuture(); + } + } + + private DeviceCredentials checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, String credId) { + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credId); + if (deviceCredentials != null && deviceCredentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) { + if (!checkMqttCredentials(clientCred, deviceCredentials)) { + return null; + } else { + return deviceCredentials; + } + } + return null; + } + + private boolean checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, DeviceCredentials deviceCredentials) { + BasicMqttCredentials dbCred = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); + if (!StringUtils.isEmpty(dbCred.getClientId()) && !dbCred.getClientId().equals(clientCred.getClientId())) { + return false; + } + if (!StringUtils.isEmpty(dbCred.getUserName()) && !dbCred.getUserName().equals(clientCred.getUserName())) { + return false; + } + if (!StringUtils.isEmpty(dbCred.getPassword())) { + if (StringUtils.isEmpty(clientCred.getPassword())) { + return false; + } else { + if (!dbCred.getPassword().equals(clientCred.getPassword())) { + return false; + } + } + } + return true; + } + private ListenableFuture handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) { DeviceId gatewayId = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB())); ListenableFuture gatewayFuture = deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, gatewayId); @@ -237,7 +301,7 @@ public class DefaultTransportApiService implements TransportApiService { builder.setCredentialsBody(credentials.getCredentialsValue()); } return TransportApiResponseMsg.newBuilder() - .setValidateTokenResponseMsg(builder.build()).build(); + .setValidateCredResponseMsg(builder.build()).build(); } catch (JsonProcessingException e) { log.warn("[{}] Failed to lookup device by id", deviceId, e); return getEmptyTransportApiResponse(); @@ -265,6 +329,6 @@ public class DefaultTransportApiService implements TransportApiService { private TransportApiResponseMsg getEmptyTransportApiResponse() { return TransportApiResponseMsg.newBuilder() - .setValidateTokenResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build(); + .setValidateCredResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build(); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java new file mode 100644 index 0000000000..eeef8617b5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/credentials/BasicMqttCredentials.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.device.credentials; + +import lombok.Data; + +@Data +public class BasicMqttCredentials { + + private String clientId; + private String userName; + private String password; + +} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java index a309c5b830..f60d18fbd4 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/EncryptionUtil.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.msg; import lombok.extern.slf4j.Slf4j; import org.bouncycastle.crypto.digests.SHA3Digest; import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; + /** * @author Valerii Sosliuk */ @@ -30,8 +31,8 @@ public class EncryptionUtil { public static String trimNewLines(String input) { return input.replaceAll("-----BEGIN CERTIFICATE-----", "") .replaceAll("-----END CERTIFICATE-----", "") - .replaceAll("\n","") - .replaceAll("\r",""); + .replaceAll("\n", "") + .replaceAll("\r", ""); } public static String getSha3Hash(String data) { @@ -45,4 +46,20 @@ public class EncryptionUtil { String sha3Hash = ByteUtils.toHexString(hashedBytes); return sha3Hash; } + + public static String getSha3Hash(String delim, String... tokens) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (String token : tokens) { + if (token != null && !token.isEmpty()) { + if (first) { + first = false; + } else { + sb.append(delim); + } + sb.append(token); + } + } + return getSha3Hash(sb.toString()); + } } diff --git a/common/queue/src/main/proto/queue.proto b/common/queue/src/main/proto/queue.proto index 46bda34458..b833ef1b7a 100644 --- a/common/queue/src/main/proto/queue.proto +++ b/common/queue/src/main/proto/queue.proto @@ -147,6 +147,12 @@ message ValidateDeviceX509CertRequestMsg { string hash = 1; } +message ValidateBasicMqttCredRequestMsg { + string clientId = 1; + string userName = 2; + string password = 3; +} + message ValidateDeviceCredentialsResponseMsg { DeviceInfoProto deviceInfo = 1; string credentialsBody = 2; @@ -429,11 +435,12 @@ message TransportApiRequestMsg { GetOrCreateDeviceFromGatewayRequestMsg getOrCreateDeviceRequestMsg = 3; GetTenantRoutingInfoRequestMsg getTenantRoutingInfoRequestMsg = 4; GetDeviceProfileRequestMsg getDeviceProfileRequestMsg = 5; + ValidateBasicMqttCredRequestMsg validateBasicMqttCredRequestMsg = 6; } /* Response from ThingsBoard Core Service to Transport Service */ message TransportApiResponseMsg { - ValidateDeviceCredentialsResponseMsg validateTokenResponseMsg = 1; + ValidateDeviceCredentialsResponseMsg validateCredResponseMsg = 1; GetOrCreateDeviceFromGatewayResponseMsg getOrCreateDeviceResponseMsg = 2; GetTenantRoutingInfoResponseMsg getTenantRoutingInfoResponseMsg = 4; GetDeviceProfileResponseMsg getDeviceProfileResponseMsg = 5; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 57ed954a9c..81809d9c67 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -66,6 +66,7 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.security.cert.X509Certificate; import java.io.IOException; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -365,25 +366,27 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { String userName = msg.payload().userName(); log.info("[{}] Processing connect msg for client with user name: {}!", sessionId, userName); - if (StringUtils.isEmpty(userName)) { - ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)); - ctx.close(); - } else { - transportService.process(DeviceTransportType.MQTT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(userName).build(), - new TransportServiceCallback() { - @Override - public void onSuccess(ValidateDeviceCredentialsResponse msg) { - onValidateDeviceResponse(msg, ctx); - } - - @Override - public void onError(Throwable e) { - log.trace("[{}] Failed to process credentials: {}", address, userName, e); - ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); - ctx.close(); - } - }); + TransportProtos.ValidateBasicMqttCredRequestMsg.Builder request = TransportProtos.ValidateBasicMqttCredRequestMsg.newBuilder() + .setClientId(msg.payload().clientIdentifier()) + .setUserName(userName); + String password = msg.payload().password(); + if (password != null) { + request.setPassword(password); } + transportService.process(DeviceTransportType.MQTT, request.build(), + new TransportServiceCallback() { + @Override + public void onSuccess(ValidateDeviceCredentialsResponse msg) { + onValidateDeviceResponse(msg, ctx); + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to process credentials: {}", address, userName, e); + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE)); + ctx.close(); + } + }); } private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert) { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java index f6c3a12aca..3fc8ed96d1 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsRes import org.thingsboard.server.gen.transport.TransportProtos.ClaimDeviceMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.GetTenantRoutingInfoResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.PostAttributeMsg; @@ -35,7 +34,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.SubscribeToRPCMsg; import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionInfoProto; import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceCredentialsResponseMsg; +import org.thingsboard.server.gen.transport.TransportProtos.ValidateBasicMqttCredRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; @@ -49,6 +48,9 @@ public interface TransportService { void process(DeviceTransportType transportType, ValidateDeviceTokenRequestMsg msg, TransportServiceCallback callback); + void process(DeviceTransportType transportType, ValidateBasicMqttCredRequestMsg msg, + TransportServiceCallback callback); + void process(DeviceTransportType transportType, ValidateDeviceX509CertRequestMsg msg, TransportServiceCallback callback); diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index b572835971..8a5966f6af 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -252,9 +252,20 @@ public class DefaultTransportService implements TransportService { } @Override - public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceTokenRequestMsg msg, TransportServiceCallback callback) { + public void process(DeviceTransportType transportType, TransportProtos.ValidateDeviceTokenRequestMsg msg, + TransportServiceCallback callback) { log.trace("Processing msg: {}", msg); - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setValidateTokenRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), + TransportApiRequestMsg.newBuilder().setValidateTokenRequestMsg(msg).build()); + doProcess(transportType, protoMsg, callback); + } + + @Override + public void process(DeviceTransportType transportType, TransportProtos.ValidateBasicMqttCredRequestMsg msg, + TransportServiceCallback callback) { + log.trace("Processing msg: {}", msg); + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), + TransportApiRequestMsg.newBuilder().setValidateBasicMqttCredRequestMsg(msg).build()); doProcess(transportType, protoMsg, callback); } @@ -265,9 +276,10 @@ public class DefaultTransportService implements TransportService { doProcess(transportType, protoMsg, callback); } - private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg protoMsg, TransportServiceCallback callback) { + private void doProcess(DeviceTransportType transportType, TbProtoQueueMsg protoMsg, + TransportServiceCallback callback) { ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { - TransportProtos.ValidateDeviceCredentialsResponseMsg msg = tmp.getValue().getValidateTokenResponseMsg(); + TransportProtos.ValidateDeviceCredentialsResponseMsg msg = tmp.getValue().getValidateCredResponseMsg(); ValidateDeviceCredentialsResponse.ValidateDeviceCredentialsResponseBuilder result = ValidateDeviceCredentialsResponse.builder(); if (msg.hasDeviceInfo()) { result.credentials(msg.getCredentialsBody()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index ac247667bc..ff1763f662 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -21,18 +21,20 @@ import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.util.mapping.JacksonUtil; import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CREDENTIALS_CACHE; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -75,8 +77,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen } private DeviceCredentials saveOrUpdate(TenantId tenantId, DeviceCredentials deviceCredentials) { - if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { - formatCertData(deviceCredentials); + if(deviceCredentials.getCredentialsType() == null){ + throw new DataValidationException("Device credentials type should be specified"); + } + switch (deviceCredentials.getCredentialsType()) { + case X509_CERTIFICATE: + formatCertData(deviceCredentials); + break; + case MQTT_BASIC: + formatSimpleMqttCredentials(deviceCredentials); + break; } log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); credentialsValidator.validate(deviceCredentials, id -> tenantId); @@ -93,6 +103,32 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen } } + private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) { + BasicMqttCredentials mqttCredentials; + try { + mqttCredentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class); + if (mqttCredentials == null) { + throw new IllegalArgumentException(); + } + } catch (IllegalArgumentException e) { + throw new DataValidationException("Invalid credentials body for simple mqtt credentials!"); + } + if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) { + throw new DataValidationException("Both mqtt client id and user name are empty!"); + } + if (StringUtils.isEmpty(mqttCredentials.getClientId())) { + deviceCredentials.setCredentialsId(mqttCredentials.getUserName()); + } else if (StringUtils.isEmpty(mqttCredentials.getUserName())) { + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash(mqttCredentials.getClientId())); + } else { + deviceCredentials.setCredentialsId(EncryptionUtil.getSha3Hash("|", mqttCredentials.getClientId(), mqttCredentials.getUserName())); + } + if (!StringUtils.isEmpty(mqttCredentials.getPassword())) { + mqttCredentials.setPassword(mqttCredentials.getPassword()); + } + deviceCredentials.setCredentialsValue(JacksonUtil.toString(mqttCredentials)); + } + private void formatCertData(DeviceCredentials deviceCredentials) { String cert = EncryptionUtil.trimNewLines(deviceCredentials.getCredentialsValue()); String sha3Hash = EncryptionUtil.getSha3Hash(cert);