Commit 4aabf936f1ee79324c25693f5f311030e51e9841
1 parent
e25bd4f0
X509 certificate support implemented
Showing
22 changed files
with
437 additions
and
15 deletions
@@ -51,6 +51,8 @@ public class DefaultDeviceAuthService implements DeviceAuthService { | @@ -51,6 +51,8 @@ public class DefaultDeviceAuthService implements DeviceAuthService { | ||
51 | // Credentials ID matches Credentials value in this | 51 | // Credentials ID matches Credentials value in this |
52 | // primitive case; | 52 | // primitive case; |
53 | return DeviceAuthResult.of(credentials.getDeviceId()); | 53 | return DeviceAuthResult.of(credentials.getDeviceId()); |
54 | + case X509_CERTIFICATE: | ||
55 | + return DeviceAuthResult.of(credentials.getDeviceId()); | ||
54 | default: | 56 | default: |
55 | return DeviceAuthResult.of("Credentials Type is not supported yet!"); | 57 | return DeviceAuthResult.of("Credentials Type is not supported yet!"); |
56 | } | 58 | } |
@@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.security; | @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.security; | ||
17 | 17 | ||
18 | public enum DeviceCredentialsType { | 18 | public enum DeviceCredentialsType { |
19 | 19 | ||
20 | - ACCESS_TOKEN | 20 | + ACCESS_TOKEN, |
21 | + X509_CERTIFICATE | ||
21 | 22 | ||
22 | } | 23 | } |
@@ -20,7 +20,6 @@ public class DeviceTokenCredentials implements DeviceCredentialsFilter { | @@ -20,7 +20,6 @@ public class DeviceTokenCredentials implements DeviceCredentialsFilter { | ||
20 | private final String token; | 20 | private final String token; |
21 | 21 | ||
22 | public DeviceTokenCredentials(String token) { | 22 | public DeviceTokenCredentials(String token) { |
23 | - super(); | ||
24 | this.token = token; | 23 | this.token = token; |
25 | } | 24 | } |
26 | 25 |
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceX509Credentials.java
0 → 100644
1 | +/** | ||
2 | + * Copyright © 2016 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.common.data.security; | ||
17 | + | ||
18 | +/** | ||
19 | + * @author Valerii Sosliuk | ||
20 | + */ | ||
21 | +public class DeviceX509Credentials implements DeviceCredentialsFilter { | ||
22 | + | ||
23 | + private final String sha3Hash; | ||
24 | + | ||
25 | + public DeviceX509Credentials(String sha3Hash) { | ||
26 | + this.sha3Hash = sha3Hash; | ||
27 | + } | ||
28 | + | ||
29 | + @Override | ||
30 | + public String getCredentialsId() { return sha3Hash; } | ||
31 | + | ||
32 | + @Override | ||
33 | + public DeviceCredentialsType getCredentialsType() { return DeviceCredentialsType.X509_CERTIFICATE; } | ||
34 | + | ||
35 | + @Override | ||
36 | + public String toString() { | ||
37 | + return "DeviceX509Credentials [SHA3=" + sha3Hash + "]"; | ||
38 | + } | ||
39 | +} |
@@ -146,6 +146,10 @@ | @@ -146,6 +146,10 @@ | ||
146 | <groupId>org.springframework.boot</groupId> | 146 | <groupId>org.springframework.boot</groupId> |
147 | <artifactId>spring-boot-autoconfigure</artifactId> | 147 | <artifactId>spring-boot-autoconfigure</artifactId> |
148 | </dependency> | 148 | </dependency> |
149 | + <dependency> | ||
150 | + <groupId>org.bouncycastle</groupId> | ||
151 | + <artifactId>bcprov-jdk15on</artifactId> | ||
152 | + </dependency> | ||
149 | </dependencies> | 153 | </dependencies> |
150 | <build> | 154 | <build> |
151 | <plugins> | 155 | <plugins> |
@@ -18,9 +18,7 @@ package org.thingsboard.server.dao; | @@ -18,9 +18,7 @@ package org.thingsboard.server.dao; | ||
18 | import java.util.ArrayList; | 18 | import java.util.ArrayList; |
19 | import java.util.Collection; | 19 | import java.util.Collection; |
20 | import java.util.Collections; | 20 | import java.util.Collections; |
21 | -import java.util.HashSet; | ||
22 | import java.util.List; | 21 | import java.util.List; |
23 | -import java.util.Set; | ||
24 | import java.util.UUID; | 22 | import java.util.UUID; |
25 | 23 | ||
26 | import org.thingsboard.server.common.data.id.UUIDBased; | 24 | import org.thingsboard.server.common.data.id.UUIDBased; |
1 | +/** | ||
2 | + * Copyright © 2016 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.dao; | ||
17 | + | ||
18 | +import lombok.extern.slf4j.Slf4j; | ||
19 | +import org.bouncycastle.crypto.digests.SHA3Digest; | ||
20 | +import org.bouncycastle.pqc.math.linearalgebra.ByteUtils; | ||
21 | +/** | ||
22 | + * @author Valerii Sosliuk | ||
23 | + */ | ||
24 | +@Slf4j | ||
25 | +public class EncryptionUtil { | ||
26 | + | ||
27 | + private EncryptionUtil() { | ||
28 | + } | ||
29 | + | ||
30 | + public static String getSha3Hash(String data) { | ||
31 | + String trimmedData = data.replaceAll("\n","").replaceAll("\r",""); | ||
32 | + byte[] dataBytes = trimmedData.getBytes(); | ||
33 | + SHA3Digest md = new SHA3Digest(256); | ||
34 | + md.reset(); | ||
35 | + md.update(dataBytes, 0, dataBytes.length); | ||
36 | + byte[] hashedBytes = new byte[256 / 8]; | ||
37 | + md.doFinal(hashedBytes, 0); | ||
38 | + String sha3Hash = ByteUtils.toHexString(hashedBytes); | ||
39 | + return sha3Hash; | ||
40 | + } | ||
41 | +} |
@@ -23,6 +23,8 @@ import org.springframework.util.StringUtils; | @@ -23,6 +23,8 @@ import org.springframework.util.StringUtils; | ||
23 | import org.thingsboard.server.common.data.Device; | 23 | import org.thingsboard.server.common.data.Device; |
24 | import org.thingsboard.server.common.data.id.DeviceId; | 24 | import org.thingsboard.server.common.data.id.DeviceId; |
25 | import org.thingsboard.server.common.data.security.DeviceCredentials; | 25 | import org.thingsboard.server.common.data.security.DeviceCredentials; |
26 | +import org.thingsboard.server.common.data.security.DeviceCredentialsType; | ||
27 | +import org.thingsboard.server.dao.EncryptionUtil; | ||
26 | import org.thingsboard.server.dao.exception.DataValidationException; | 28 | import org.thingsboard.server.dao.exception.DataValidationException; |
27 | import org.thingsboard.server.dao.model.DeviceCredentialsEntity; | 29 | import org.thingsboard.server.dao.model.DeviceCredentialsEntity; |
28 | import org.thingsboard.server.dao.service.DataValidator; | 30 | import org.thingsboard.server.dao.service.DataValidator; |
@@ -70,11 +72,19 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { | @@ -70,11 +72,19 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { | ||
70 | } | 72 | } |
71 | 73 | ||
72 | private DeviceCredentials saveOrUpdare(DeviceCredentials deviceCredentials) { | 74 | private DeviceCredentials saveOrUpdare(DeviceCredentials deviceCredentials) { |
75 | + if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.X509_CERTIFICATE) { | ||
76 | + encryptDeviceId(deviceCredentials); | ||
77 | + } | ||
73 | log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); | 78 | log.trace("Executing updateDeviceCredentials [{}]", deviceCredentials); |
74 | credentialsValidator.validate(deviceCredentials); | 79 | credentialsValidator.validate(deviceCredentials); |
75 | return getData(deviceCredentialsDao.save(deviceCredentials)); | 80 | return getData(deviceCredentialsDao.save(deviceCredentials)); |
76 | } | 81 | } |
77 | 82 | ||
83 | + private void encryptDeviceId(DeviceCredentials deviceCredentials) { | ||
84 | + String sha3Hash = EncryptionUtil.getSha3Hash(deviceCredentials.getCredentialsId()); | ||
85 | + deviceCredentials.setCredentialsId(sha3Hash); | ||
86 | + } | ||
87 | + | ||
78 | @Override | 88 | @Override |
79 | public void deleteDeviceCredentials(DeviceCredentials deviceCredentials) { | 89 | public void deleteDeviceCredentials(DeviceCredentials deviceCredentials) { |
80 | log.trace("Executing deleteDeviceCredentials [{}]", deviceCredentials); | 90 | log.trace("Executing deleteDeviceCredentials [{}]", deviceCredentials); |
@@ -121,6 +131,10 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { | @@ -121,6 +131,10 @@ public class DeviceCredentialsServiceImpl implements DeviceCredentialsService { | ||
121 | throw new DataValidationException("Incorrect access token length [" + deviceCredentials.getCredentialsId().length() + "]!"); | 131 | throw new DataValidationException("Incorrect access token length [" + deviceCredentials.getCredentialsId().length() + "]!"); |
122 | } | 132 | } |
123 | break; | 133 | break; |
134 | + case X509_CERTIFICATE: | ||
135 | + if (deviceCredentials.getCredentialsId().length() == 0) { | ||
136 | + throw new DataValidationException("X509 Certificate Cannot be empty!"); | ||
137 | + } | ||
124 | default: | 138 | default: |
125 | break; | 139 | break; |
126 | } | 140 | } |
@@ -69,6 +69,7 @@ | @@ -69,6 +69,7 @@ | ||
69 | <surfire.version>2.19.1</surfire.version> | 69 | <surfire.version>2.19.1</surfire.version> |
70 | <jar-plugin.version>3.0.2</jar-plugin.version> | 70 | <jar-plugin.version>3.0.2</jar-plugin.version> |
71 | <springfox-swagger.version>2.6.1</springfox-swagger.version> | 71 | <springfox-swagger.version>2.6.1</springfox-swagger.version> |
72 | + <bouncycastle.version>1.56</bouncycastle.version> | ||
72 | </properties> | 73 | </properties> |
73 | 74 | ||
74 | <modules> | 75 | <modules> |
@@ -689,6 +690,16 @@ | @@ -689,6 +690,16 @@ | ||
689 | <artifactId>springfox-swagger2</artifactId> | 690 | <artifactId>springfox-swagger2</artifactId> |
690 | <version>${springfox-swagger.version}</version> | 691 | <version>${springfox-swagger.version}</version> |
691 | </dependency> | 692 | </dependency> |
693 | + <dependency> | ||
694 | + <groupId>org.bouncycastle</groupId> | ||
695 | + <artifactId>bcprov-jdk15on</artifactId> | ||
696 | + <version>${bouncycastle.version}</version> | ||
697 | + </dependency> | ||
698 | + <dependency> | ||
699 | + <groupId>org.bouncycastle</groupId> | ||
700 | + <artifactId>bcpkix-jdk15on</artifactId> | ||
701 | + <version>${bouncycastle.version}</version> | ||
702 | + </dependency> | ||
692 | </dependencies> | 703 | </dependencies> |
693 | </dependencyManagement> | 704 | </dependencyManagement> |
694 | 705 |
1 | HOSTNAME="$(hostname)" | 1 | HOSTNAME="$(hostname)" |
2 | PASSWORD="password" | 2 | PASSWORD="password" |
3 | 3 | ||
4 | -CLIENT_TRUSTSTORE="client_truststore.crt" | 4 | +CLIENT_TRUSTSTORE="client_truststore.pem" |
5 | +CLIENT_KEY_ALIAS="clientalias" | ||
6 | +CLIENT_FILE_PREFIX="mqttclient" | ||
5 | 7 | ||
6 | SERVER_KEY_ALIAS="serveralias" | 8 | SERVER_KEY_ALIAS="serveralias" |
7 | SERVER_FILE_PREFIX="mqttserver" | 9 | SERVER_FILE_PREFIX="mqttserver" |
@@ -45,6 +45,7 @@ read -p "Do you want to copy $SERVER_FILE_PREFIX.jks to server directory? " yn | @@ -45,6 +45,7 @@ read -p "Do you want to copy $SERVER_FILE_PREFIX.jks to server directory? " yn | ||
45 | else | 45 | else |
46 | DESTINATION=$SERVER_KEYSTORE_DIR | 46 | DESTINATION=$SERVER_KEYSTORE_DIR |
47 | fi; | 47 | fi; |
48 | + mkdir -p $SERVER_KEYSTORE_DIR | ||
48 | cp $SERVER_FILE_PREFIX.jks $DESTINATION | 49 | cp $SERVER_FILE_PREFIX.jks $DESTINATION |
49 | if [ $? -ne 0 ]; then | 50 | if [ $? -ne 0 ]; then |
50 | echo "Failed to copy keystore file." | 51 | echo "Failed to copy keystore file." |
tools/src/main/shell/onewaysslmqttclient.py
0 → 100644
1 | +# -*- coding: utf-8 -*- | ||
2 | +# | ||
3 | +# Copyright © 2016 The Thingsboard Authors | ||
4 | +# | ||
5 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | +# you may not use this file except in compliance with the License. | ||
7 | +# You may obtain a copy of the License at | ||
8 | +# | ||
9 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | +# | ||
11 | +# Unless required by applicable law or agreed to in writing, software | ||
12 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | +# See the License for the specific language governing permissions and | ||
15 | +# limitations under the License. | ||
16 | +# | ||
17 | + | ||
18 | +import paho.mqtt.client as mqtt | ||
19 | +import ssl, socket | ||
20 | + | ||
21 | +# The callback for when the client receives a CONNACK response from the server. | ||
22 | +def on_connect(client, userdata, rc): | ||
23 | + print('Connected with result code '+str(rc)) | ||
24 | + # Subscribing in on_connect() means that if we lose the connection and | ||
25 | + # reconnect then subscriptions will be renewed. | ||
26 | + client.subscribe('v1/devices/me/attributes') | ||
27 | + client.subscribe('v1/devices/me/attributes/response/+') | ||
28 | + client.subscribe('v1/devices/me/rpc/request/+') | ||
29 | + | ||
30 | + | ||
31 | +# The callback for when a PUBLISH message is received from the server. | ||
32 | +def on_message(client, userdata, msg): | ||
33 | + print 'Topic: ' + msg.topic + '\nMessage: ' + str(msg.payload) | ||
34 | + if msg.topic.startswith( 'v1/devices/me/rpc/request/'): | ||
35 | + requestId = msg.topic[len('v1/devices/me/rpc/request/'):len(msg.topic)] | ||
36 | + print 'This is a RPC call. RequestID: ' + requestId + '. Going to reply now!' | ||
37 | + client.publish('v1/devices/me/rpc/response/' + requestId, "{\"value1\":\"A\", \"value2\":\"B\"}", 1) | ||
38 | + | ||
39 | + | ||
40 | +client = mqtt.Client() | ||
41 | +client.on_connect = on_connect | ||
42 | +client.on_message = on_message | ||
43 | +client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1) | ||
44 | + | ||
45 | +#client.tls_set(ca_certs="client_truststore.pem", certfile="mqttclient.nopass.pem", keyfile=None, cert_reqs=ssl.CERT_REQUIRED, | ||
46 | +# tls_version=ssl.PROTOCOL_TLSv1, ciphers=None); | ||
47 | +client.tls_set(ca_certs="client_truststore.pem", certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED, | ||
48 | + tls_version=ssl.PROTOCOL_TLSv1, ciphers=None); | ||
49 | + | ||
50 | +client.username_pw_set("B1_TEST_TOKEN") | ||
51 | +client.tls_insecure_set(False) | ||
52 | +client.connect(socket.gethostname(), 1883, 1) | ||
53 | + | ||
54 | + | ||
55 | +# Blocking call that processes network traffic, dispatches callbacks and | ||
56 | +# handles reconnecting. | ||
57 | +# Other loop*() functions are available that give a threaded interface and a | ||
58 | +# manual interface. | ||
59 | +client.loop_forever() |
1 | +#!/bin/sh | ||
2 | +# | ||
3 | +# Copyright © 2016 The Thingsboard Authors | ||
4 | +# | ||
5 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
6 | +# you may not use this file except in compliance with the License. | ||
7 | +# You may obtain a copy of the License at | ||
8 | +# | ||
9 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
10 | +# | ||
11 | +# Unless required by applicable law or agreed to in writing, software | ||
12 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
13 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
14 | +# See the License for the specific language governing permissions and | ||
15 | +# limitations under the License. | ||
16 | +# | ||
17 | + | ||
18 | + | ||
19 | +. keygen.properties | ||
20 | + | ||
21 | +echo "Generating SSL Key Pair..." | ||
22 | + | ||
23 | +keytool -genkeypair -v \ | ||
24 | + -alias $CLIENT_KEY_ALIAS \ | ||
25 | + -dname "CN=$HOSTNAME, OU=Thingsboard, O=Thingsboard, L=Piscataway, ST=NJ, C=US" \ | ||
26 | + -keystore $CLIENT_FILE_PREFIX.jks \ | ||
27 | + -keypass $PASSWORD \ | ||
28 | + -storepass $PASSWORD \ | ||
29 | + -keyalg RSA \ | ||
30 | + -keysize 2048 \ | ||
31 | + -validity 9999 | ||
32 | +echo "Converting keystore to pkcs12" | ||
33 | +keytool -importkeystore \ | ||
34 | + -srckeystore $CLIENT_FILE_PREFIX.jks \ | ||
35 | + -destkeystore $CLIENT_FILE_PREFIX.p12 \ | ||
36 | + -srcalias $CLIENT_KEY_ALIAS \ | ||
37 | + -srcstoretype jks \ | ||
38 | + -deststoretype pkcs12 \ | ||
39 | + -keypass $PASSWORD \ | ||
40 | + -srcstorepass $PASSWORD \ | ||
41 | + -deststorepass $PASSWORD \ | ||
42 | + -srckeypass $PASSWORD \ | ||
43 | + -destkeypass $PASSWORD | ||
44 | + | ||
45 | +echo "Converting pkcs12 to pem" | ||
46 | +openssl pkcs12 -in $CLIENT_FILE_PREFIX.p12 \ | ||
47 | + -out $CLIENT_FILE_PREFIX.pem \ | ||
48 | + -passin pass:$PASSWORD \ | ||
49 | + -passout pass:$PASSWORD \ | ||
50 | + | ||
51 | +echo "Importing server public key..." | ||
52 | +keytool -export \ | ||
53 | + -alias $SERVER_KEY_ALIAS \ | ||
54 | + -keystore $SERVER_KEYSTORE_DIR/$SERVER_FILE_PREFIX.jks \ | ||
55 | + -file $CLIENT_TRUSTSTORE -rfc \ | ||
56 | + -storepass $PASSWORD | ||
57 | + | ||
58 | +echo "Exporting no-password pem certificate" | ||
59 | +openssl rsa -in $CLIENT_FILE_PREFIX.pem -out $CLIENT_FILE_PREFIX.nopass.pem -passin pass:$PASSWORD | ||
60 | +tail -n +$(($(grep -m1 -n -e '-----BEGIN CERTIFICATE' $CLIENT_FILE_PREFIX.pem | cut -d: -f1) )) \ | ||
61 | + $CLIENT_FILE_PREFIX.pem >> $CLIENT_FILE_PREFIX.nopass.pem | ||
62 | + | ||
63 | +echo "Done." |
tools/src/main/shell/twowaysslmqttclient.py
renamed from
tools/src/main/shell/securemqttclient.py
1 | +# -*- coding: utf-8 -*- | ||
1 | # | 2 | # |
2 | # Copyright © 2016-2017 The Thingsboard Authors | 3 | # Copyright © 2016-2017 The Thingsboard Authors |
3 | # | 4 | # |
@@ -41,8 +42,9 @@ client.on_connect = on_connect | @@ -41,8 +42,9 @@ client.on_connect = on_connect | ||
41 | client.on_message = on_message | 42 | client.on_message = on_message |
42 | client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1) | 43 | client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1) |
43 | 44 | ||
44 | -client.tls_set(ca_certs="client_truststore.crt", certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED, | ||
45 | - tls_version=ssl.PROTOCOL_TLSv1, ciphers=None); | 45 | +client.tls_set(ca_certs="client_truststore.pem", certfile="mqttclient.nopass.pem", keyfile=None, cert_reqs=ssl.CERT_REQUIRED, |
46 | + tls_version=ssl.PROTOCOL_TLSv1, ciphers=None); | ||
47 | + | ||
46 | client.username_pw_set("TEST_TOKEN") | 48 | client.username_pw_set("TEST_TOKEN") |
47 | client.tls_insecure_set(False) | 49 | client.tls_insecure_set(False) |
48 | client.connect(socket.gethostname(), 1883, 1) | 50 | client.connect(socket.gethostname(), 1883, 1) |
@@ -18,15 +18,23 @@ package org.thingsboard.server.transport.mqtt; | @@ -18,15 +18,23 @@ package org.thingsboard.server.transport.mqtt; | ||
18 | import com.google.common.io.Resources; | 18 | import com.google.common.io.Resources; |
19 | import io.netty.handler.ssl.SslHandler; | 19 | import io.netty.handler.ssl.SslHandler; |
20 | import lombok.extern.slf4j.Slf4j; | 20 | import lombok.extern.slf4j.Slf4j; |
21 | +import org.springframework.beans.factory.annotation.Autowired; | ||
21 | import org.springframework.beans.factory.annotation.Value; | 22 | import org.springframework.beans.factory.annotation.Value; |
22 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | 23 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
23 | import org.springframework.stereotype.Component; | 24 | import org.springframework.stereotype.Component; |
25 | +import org.thingsboard.server.common.data.security.DeviceCredentials; | ||
26 | +import org.thingsboard.server.dao.EncryptionUtil; | ||
27 | +import org.thingsboard.server.dao.device.DeviceCredentialsService; | ||
28 | +import org.thingsboard.server.transport.mqtt.util.SslUtil; | ||
24 | 29 | ||
25 | import javax.net.ssl.*; | 30 | import javax.net.ssl.*; |
26 | import java.io.File; | 31 | import java.io.File; |
27 | import java.io.FileInputStream; | 32 | import java.io.FileInputStream; |
33 | +import java.io.IOException; | ||
28 | import java.net.URL; | 34 | import java.net.URL; |
29 | import java.security.KeyStore; | 35 | import java.security.KeyStore; |
36 | +import java.security.cert.CertificateException; | ||
37 | +import java.security.cert.X509Certificate; | ||
30 | 38 | ||
31 | /** | 39 | /** |
32 | * Created by valerii.sosliuk on 11/6/16. | 40 | * Created by valerii.sosliuk on 11/6/16. |
@@ -51,6 +59,9 @@ public class MqttSslHandlerProvider { | @@ -51,6 +59,9 @@ public class MqttSslHandlerProvider { | ||
51 | @Value("${mqtt.ssl.trustStoreType}") | 59 | @Value("${mqtt.ssl.trustStoreType}") |
52 | private String trustStoreType; | 60 | private String trustStoreType; |
53 | 61 | ||
62 | + @Autowired | ||
63 | + private DeviceCredentialsService deviceCredentialsService; | ||
64 | + | ||
54 | 65 | ||
55 | public SslHandler getSslHandler() { | 66 | public SslHandler getSslHandler() { |
56 | try { | 67 | try { |
@@ -71,13 +82,14 @@ public class MqttSslHandlerProvider { | @@ -71,13 +82,14 @@ public class MqttSslHandlerProvider { | ||
71 | kmf.init(ks, keyStorePassword.toCharArray()); | 82 | kmf.init(ks, keyStorePassword.toCharArray()); |
72 | 83 | ||
73 | KeyManager[] km = kmf.getKeyManagers(); | 84 | KeyManager[] km = kmf.getKeyManagers(); |
74 | - TrustManager[] tm = tmFactory.getTrustManagers(); | 85 | + TrustManager x509wrapped = getX509TrustManager(tmFactory); |
86 | + TrustManager[] tm = {x509wrapped}; | ||
75 | SSLContext sslContext = SSLContext.getInstance(TLS); | 87 | SSLContext sslContext = SSLContext.getInstance(TLS); |
76 | sslContext.init(km, tm, null); | 88 | sslContext.init(km, tm, null); |
77 | SSLEngine sslEngine = sslContext.createSSLEngine(); | 89 | SSLEngine sslEngine = sslContext.createSSLEngine(); |
78 | sslEngine.setUseClientMode(false); | 90 | sslEngine.setUseClientMode(false); |
79 | sslEngine.setNeedClientAuth(false); | 91 | sslEngine.setNeedClientAuth(false); |
80 | - sslEngine.setWantClientAuth(false); | 92 | + sslEngine.setWantClientAuth(true); |
81 | sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols()); | 93 | sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols()); |
82 | sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites()); | 94 | sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites()); |
83 | sslEngine.setEnableSessionCreation(true); | 95 | sslEngine.setEnableSessionCreation(true); |
@@ -88,4 +100,54 @@ public class MqttSslHandlerProvider { | @@ -88,4 +100,54 @@ public class MqttSslHandlerProvider { | ||
88 | } | 100 | } |
89 | } | 101 | } |
90 | 102 | ||
103 | + private TrustManager getX509TrustManager(TrustManagerFactory tmf) throws Exception { | ||
104 | + X509TrustManager x509Tm = null; | ||
105 | + for (TrustManager tm : tmf.getTrustManagers()) { | ||
106 | + if (tm instanceof X509TrustManager) { | ||
107 | + x509Tm = (X509TrustManager) tm; | ||
108 | + break; | ||
109 | + } | ||
110 | + } | ||
111 | + X509TrustManager x509TmWrapper = new ThingsboardMqttX509TrustManager(x509Tm, deviceCredentialsService); | ||
112 | + return x509TmWrapper; | ||
113 | + } | ||
114 | + | ||
115 | + static class ThingsboardMqttX509TrustManager implements X509TrustManager { | ||
116 | + | ||
117 | + private final X509TrustManager trustManager; | ||
118 | + private DeviceCredentialsService deviceCredentialsService; | ||
119 | + | ||
120 | + ThingsboardMqttX509TrustManager(X509TrustManager trustManager, DeviceCredentialsService deviceCredentialsService) { | ||
121 | + this.trustManager = trustManager; | ||
122 | + this.deviceCredentialsService = deviceCredentialsService; | ||
123 | + } | ||
124 | + | ||
125 | + @Override | ||
126 | + public X509Certificate[] getAcceptedIssuers() { | ||
127 | + return trustManager.getAcceptedIssuers(); | ||
128 | + } | ||
129 | + | ||
130 | + @Override | ||
131 | + public void checkServerTrusted(X509Certificate[] chain, | ||
132 | + String authType) throws CertificateException { | ||
133 | + trustManager.checkServerTrusted(chain, authType); | ||
134 | + } | ||
135 | + | ||
136 | + @Override | ||
137 | + public void checkClientTrusted(X509Certificate[] chain, | ||
138 | + String authType) throws CertificateException { | ||
139 | + for (X509Certificate cert : chain) { | ||
140 | + try { | ||
141 | + String strCert = SslUtil.getX509CertificateString(cert); | ||
142 | + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); | ||
143 | + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(sha3Hash); | ||
144 | + if (deviceCredentials == null) { | ||
145 | + throw new CertificateException("Invalid Device Certificate"); | ||
146 | + } | ||
147 | + } catch (IOException e) { | ||
148 | + e.printStackTrace(); | ||
149 | + } | ||
150 | + } | ||
151 | + } | ||
152 | + } | ||
91 | } | 153 | } |
@@ -18,11 +18,13 @@ package org.thingsboard.server.transport.mqtt; | @@ -18,11 +18,13 @@ package org.thingsboard.server.transport.mqtt; | ||
18 | import io.netty.channel.ChannelHandlerContext; | 18 | import io.netty.channel.ChannelHandlerContext; |
19 | import io.netty.channel.ChannelInboundHandlerAdapter; | 19 | import io.netty.channel.ChannelInboundHandlerAdapter; |
20 | import io.netty.handler.codec.mqtt.*; | 20 | import io.netty.handler.codec.mqtt.*; |
21 | +import io.netty.handler.ssl.SslHandler; | ||
21 | import io.netty.util.concurrent.Future; | 22 | import io.netty.util.concurrent.Future; |
22 | import io.netty.util.concurrent.GenericFutureListener; | 23 | import io.netty.util.concurrent.GenericFutureListener; |
23 | import lombok.extern.slf4j.Slf4j; | 24 | import lombok.extern.slf4j.Slf4j; |
24 | import org.springframework.util.StringUtils; | 25 | import org.springframework.util.StringUtils; |
25 | import org.thingsboard.server.common.data.security.DeviceTokenCredentials; | 26 | import org.thingsboard.server.common.data.security.DeviceTokenCredentials; |
27 | +import org.thingsboard.server.common.data.security.DeviceX509Credentials; | ||
26 | import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg; | 28 | import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg; |
27 | import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg; | 29 | import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg; |
28 | import org.thingsboard.server.common.msg.session.MsgType; | 30 | import org.thingsboard.server.common.msg.session.MsgType; |
@@ -30,9 +32,13 @@ import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg; | @@ -30,9 +32,13 @@ import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg; | ||
30 | import org.thingsboard.server.common.transport.SessionMsgProcessor; | 32 | import org.thingsboard.server.common.transport.SessionMsgProcessor; |
31 | import org.thingsboard.server.common.transport.adaptor.AdaptorException; | 33 | import org.thingsboard.server.common.transport.adaptor.AdaptorException; |
32 | import org.thingsboard.server.common.transport.auth.DeviceAuthService; | 34 | import org.thingsboard.server.common.transport.auth.DeviceAuthService; |
35 | +import org.thingsboard.server.dao.EncryptionUtil; | ||
33 | import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; | 36 | import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; |
34 | import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx; | 37 | import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx; |
38 | +import org.thingsboard.server.transport.mqtt.util.SslUtil; | ||
35 | 39 | ||
40 | +import javax.net.ssl.SSLPeerUnverifiedException; | ||
41 | +import javax.security.cert.X509Certificate; | ||
36 | import java.util.ArrayList; | 42 | import java.util.ArrayList; |
37 | import java.util.List; | 43 | import java.util.List; |
38 | 44 | ||
@@ -57,12 +63,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -57,12 +63,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
57 | private final String sessionId; | 63 | private final String sessionId; |
58 | private final MqttTransportAdaptor adaptor; | 64 | private final MqttTransportAdaptor adaptor; |
59 | private final SessionMsgProcessor processor; | 65 | private final SessionMsgProcessor processor; |
66 | + private final SslHandler sslHandler; | ||
60 | 67 | ||
61 | - public MqttTransportHandler(SessionMsgProcessor processor, DeviceAuthService authService, MqttTransportAdaptor adaptor) { | 68 | + public MqttTransportHandler(SessionMsgProcessor processor, DeviceAuthService authService, |
69 | + MqttTransportAdaptor adaptor, SslHandler sslHandler) { | ||
62 | this.processor = processor; | 70 | this.processor = processor; |
63 | this.adaptor = adaptor; | 71 | this.adaptor = adaptor; |
64 | this.sessionCtx = new MqttSessionCtx(processor, authService, adaptor); | 72 | this.sessionCtx = new MqttSessionCtx(processor, authService, adaptor); |
65 | this.sessionId = sessionCtx.getSessionId().toUidStr(); | 73 | this.sessionId = sessionCtx.getSessionId().toUidStr(); |
74 | + this.sslHandler = sslHandler; | ||
66 | } | 75 | } |
67 | 76 | ||
68 | @Override | 77 | @Override |
@@ -197,6 +206,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -197,6 +206,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
197 | 206 | ||
198 | private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { | 207 | private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { |
199 | log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); | 208 | log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); |
209 | + X509Certificate cert; | ||
210 | + if (sslHandler != null && (cert = getX509Certificate()) != null) { | ||
211 | + processX509CertConnect(ctx, cert); | ||
212 | + } else { | ||
213 | + processAuthTokenConnect(ctx, msg); | ||
214 | + } | ||
215 | + } | ||
216 | + | ||
217 | + private void processAuthTokenConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { | ||
200 | String userName = msg.payload().userName(); | 218 | String userName = msg.payload().userName(); |
201 | if (StringUtils.isEmpty(userName)) { | 219 | if (StringUtils.isEmpty(userName)) { |
202 | ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)); | 220 | ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)); |
@@ -209,6 +227,35 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -209,6 +227,35 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
209 | } | 227 | } |
210 | } | 228 | } |
211 | 229 | ||
230 | + private void processX509CertConnect(ChannelHandlerContext ctx, X509Certificate cert) { | ||
231 | + try { | ||
232 | + String strCert = SslUtil.getX509CertificateString(cert); | ||
233 | + String sha3Hash = EncryptionUtil.getSha3Hash(strCert); | ||
234 | + if (sessionCtx.login(new DeviceX509Credentials(sha3Hash))) { | ||
235 | + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_ACCEPTED)); | ||
236 | + } else { | ||
237 | + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED)); | ||
238 | + ctx.close(); | ||
239 | + } | ||
240 | + } catch (Exception e) { | ||
241 | + ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED)); | ||
242 | + ctx.close(); | ||
243 | + } | ||
244 | + } | ||
245 | + | ||
246 | + private X509Certificate getX509Certificate() { | ||
247 | + try { | ||
248 | + X509Certificate[] certChain = sslHandler.engine().getSession().getPeerCertificateChain(); | ||
249 | + if (certChain.length > 0) { | ||
250 | + return certChain[0]; | ||
251 | + } | ||
252 | + } catch (SSLPeerUnverifiedException e) { | ||
253 | + log.warn(e.getMessage()); | ||
254 | + return null; | ||
255 | + } | ||
256 | + return null; | ||
257 | + } | ||
258 | + | ||
212 | private void processDisconnect(ChannelHandlerContext ctx) { | 259 | private void processDisconnect(ChannelHandlerContext ctx) { |
213 | ctx.close(); | 260 | ctx.close(); |
214 | } | 261 | } |
@@ -55,13 +55,15 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha | @@ -55,13 +55,15 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha | ||
55 | @Override | 55 | @Override |
56 | public void initChannel(SocketChannel ch) { | 56 | public void initChannel(SocketChannel ch) { |
57 | ChannelPipeline pipeline = ch.pipeline(); | 57 | ChannelPipeline pipeline = ch.pipeline(); |
58 | + SslHandler sslHandler = null; | ||
58 | if (sslHandlerProvider != null) { | 59 | if (sslHandlerProvider != null) { |
59 | - pipeline.addLast(sslHandlerProvider.getSslHandler()); | 60 | + sslHandler = sslHandlerProvider.getSslHandler(); |
61 | + pipeline.addLast(sslHandler); | ||
60 | } | 62 | } |
61 | pipeline.addLast("decoder", new MqttDecoder()); | 63 | pipeline.addLast("decoder", new MqttDecoder()); |
62 | pipeline.addLast("encoder", MqttEncoder.INSTANCE); | 64 | pipeline.addLast("encoder", MqttEncoder.INSTANCE); |
63 | 65 | ||
64 | - MqttTransportHandler handler = new MqttTransportHandler(processor, authService, adaptor); | 66 | + MqttTransportHandler handler = new MqttTransportHandler(processor, authService, adaptor, sslHandler); |
65 | pipeline.addLast(handler); | 67 | pipeline.addLast(handler); |
66 | ch.closeFuture().addListener(handler); | 68 | ch.closeFuture().addListener(handler); |
67 | } | 69 | } |
1 | +/** | ||
2 | + * Copyright © 2016 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.mqtt.util; | ||
17 | + | ||
18 | +import lombok.extern.slf4j.Slf4j; | ||
19 | +import sun.misc.BASE64Encoder; | ||
20 | + | ||
21 | +import java.io.ByteArrayOutputStream; | ||
22 | +import java.io.IOException; | ||
23 | +import java.security.cert.CertificateEncodingException; | ||
24 | +import java.security.cert.X509Certificate; | ||
25 | + | ||
26 | +/** | ||
27 | + * @author Valerii Sosliuk | ||
28 | + */ | ||
29 | +@Slf4j | ||
30 | +public class SslUtil { | ||
31 | + | ||
32 | + private SslUtil() { | ||
33 | + } | ||
34 | + | ||
35 | + public static String getX509CertificateString(X509Certificate cert) throws CertificateEncodingException, IOException { | ||
36 | + ByteArrayOutputStream out = new ByteArrayOutputStream(); | ||
37 | + BASE64Encoder encoder = new BASE64Encoder(); | ||
38 | + encoder.encodeBuffer(cert.getEncoded(), out); | ||
39 | + return new String(out.toByteArray(), "UTF-8").trim(); | ||
40 | + } | ||
41 | + | ||
42 | + public static String getX509CertificateString(javax.security.cert.X509Certificate cert) | ||
43 | + throws javax.security.cert.CertificateEncodingException, IOException { | ||
44 | + ByteArrayOutputStream out = new ByteArrayOutputStream(); | ||
45 | + BASE64Encoder encoder = new BASE64Encoder(); | ||
46 | + encoder.encodeBuffer(cert.getEncoded(), out); | ||
47 | + return new String(out.toByteArray(), "UTF-8").trim(); | ||
48 | + } | ||
49 | +} |
@@ -42,9 +42,17 @@ | @@ -42,9 +42,17 @@ | ||
42 | 42 | ||
43 | <dependencies> | 43 | <dependencies> |
44 | <dependency> | 44 | <dependency> |
45 | + <groupId>org.thingsboard</groupId> | ||
46 | + <artifactId>dao</artifactId> | ||
47 | + </dependency> | ||
48 | + <dependency> | ||
45 | <groupId>org.springframework.boot</groupId> | 49 | <groupId>org.springframework.boot</groupId> |
46 | <artifactId>spring-boot-autoconfigure</artifactId> | 50 | <artifactId>spring-boot-autoconfigure</artifactId> |
47 | </dependency> | 51 | </dependency> |
52 | + <dependency> | ||
53 | + <groupId>org.bouncycastle</groupId> | ||
54 | + <artifactId>bcprov-jdk15on</artifactId> | ||
55 | + </dependency> | ||
48 | </dependencies> | 56 | </dependencies> |
49 | 57 | ||
50 | </project> | 58 | </project> |
@@ -24,7 +24,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, | @@ -24,7 +24,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, | ||
24 | value: 'ACCESS_TOKEN' | 24 | value: 'ACCESS_TOKEN' |
25 | }, | 25 | }, |
26 | { | 26 | { |
27 | - name: 'X.509 Certificate (Coming soon)', | 27 | + name: 'X.509 Certificate', |
28 | value: 'X509_CERTIFICATE' | 28 | value: 'X509_CERTIFICATE' |
29 | } | 29 | } |
30 | ]; | 30 | ]; |
@@ -35,6 +35,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, | @@ -35,6 +35,7 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, | ||
35 | vm.valid = valid; | 35 | vm.valid = valid; |
36 | vm.cancel = cancel; | 36 | vm.cancel = cancel; |
37 | vm.save = save; | 37 | vm.save = save; |
38 | + vm.clear = clear; | ||
38 | 39 | ||
39 | loadDeviceCredentials(); | 40 | loadDeviceCredentials(); |
40 | 41 | ||
@@ -50,10 +51,16 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, | @@ -50,10 +51,16 @@ export default function ManageDeviceCredentialsController(deviceService, $scope, | ||
50 | 51 | ||
51 | function valid() { | 52 | function valid() { |
52 | return vm.deviceCredentials && | 53 | return vm.deviceCredentials && |
53 | - vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN' && | 54 | + (vm.deviceCredentials.credentialsType === 'ACCESS_TOKEN' |
55 | + || vm.deviceCredentials.credentialsType === 'X509_CERTIFICATE') | ||
56 | + && | ||
54 | vm.deviceCredentials.credentialsId && vm.deviceCredentials.credentialsId.length > 0; | 57 | vm.deviceCredentials.credentialsId && vm.deviceCredentials.credentialsId.length > 0; |
55 | } | 58 | } |
56 | 59 | ||
60 | + function clear() { | ||
61 | + vm.deviceCredentials.credentialsId = null; | ||
62 | + } | ||
63 | + | ||
57 | function save() { | 64 | function save() { |
58 | deviceService.saveDeviceCredentials(vm.deviceCredentials).then(function success(deviceCredentials) { | 65 | deviceService.saveDeviceCredentials(vm.deviceCredentials).then(function success(deviceCredentials) { |
59 | vm.deviceCredentials = deviceCredentials; | 66 | vm.deviceCredentials = deviceCredentials; |
@@ -33,7 +33,8 @@ | @@ -33,7 +33,8 @@ | ||
33 | <fieldset ng-disabled="loading || vm.isReadOnly"> | 33 | <fieldset ng-disabled="loading || vm.isReadOnly"> |
34 | <md-input-container class="md-block"> | 34 | <md-input-container class="md-block"> |
35 | <label translate>device.credentials-type</label> | 35 | <label translate>device.credentials-type</label> |
36 | - <md-select ng-disabled="loading || vm.isReadOnly" ng-model="vm.deviceCredentials.credentialsType"> | 36 | + <md-select ng-disabled="loading || vm.isReadOnly" ng-model="vm.deviceCredentials.credentialsType" |
37 | + ng-change="vm.clear()"> | ||
37 | <md-option ng-repeat="credentialsType in vm.credentialsTypes" value="{{credentialsType.value}}"> | 38 | <md-option ng-repeat="credentialsType in vm.credentialsTypes" value="{{credentialsType.value}}"> |
38 | {{credentialsType.name}} | 39 | {{credentialsType.name}} |
39 | </md-option> | 40 | </md-option> |
@@ -48,6 +49,14 @@ | @@ -48,6 +49,14 @@ | ||
48 | <div translate ng-message="pattern">device.access-token-invalid</div> | 49 | <div translate ng-message="pattern">device.access-token-invalid</div> |
49 | </div> | 50 | </div> |
50 | </md-input-container> | 51 | </md-input-container> |
52 | + <md-input-container class="md-block" ng-if="vm.deviceCredentials.credentialsType === 'X509_CERTIFICATE'"> | ||
53 | + <label translate>device.rsa-key</label> | ||
54 | + <textarea required name="rsaKey" ng-model="vm.deviceCredentials.credentialsId" | ||
55 | + cols="15" rows="5" /> | ||
56 | + <div ng-messages="theForm.rsaKey.$error"> | ||
57 | + <div translate ng-message="required">device.rsa-key-required</div> | ||
58 | + </div> | ||
59 | + </md-input-container> | ||
51 | </fieldset> | 60 | </fieldset> |
52 | </div> | 61 | </div> |
53 | </md-dialog-content> | 62 | </md-dialog-content> |
@@ -308,6 +308,8 @@ | @@ -308,6 +308,8 @@ | ||
308 | "access-token": "Access token", | 308 | "access-token": "Access token", |
309 | "access-token-required": "Access token is required.", | 309 | "access-token-required": "Access token is required.", |
310 | "access-token-invalid": "Access token length must be from 1 to 20 characters.", | 310 | "access-token-invalid": "Access token length must be from 1 to 20 characters.", |
311 | + "rsa-key": "RSA public key", | ||
312 | + "access-token-required": "RSA public key is required.", | ||
311 | "secret": "Secret", | 313 | "secret": "Secret", |
312 | "secret-required": "Secret is required.", | 314 | "secret-required": "Secret is required.", |
313 | "name": "Name", | 315 | "name": "Name", |