Commit fe2427e99f3706a673b9a23260ffd20cc32cd2ee

Authored by YevhenBondarenko
1 parent 3f5e77ab

added validation for lwm2m device credentials

... ... @@ -185,9 +185,9 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
185 185 .filter(Objects::nonNull)
186 186 .forEach(securityMode -> {
187 187 try {
188   - LwM2MSecurityMode.valueOf(securityMode);
  188 + LwM2MSecurityMode.valueOf(securityMode.toUpperCase());
189 189 } catch (IllegalArgumentException e) {
190   - throw new DeviceCredentialsValidationException("Unknown LwM2M security mode: " + securityMode);
  190 + throw new DeviceCredentialsValidationException("Unknown LwM2M security mode: " + securityMode + ", (the mode should be: NO_SEC, PSK, RPK, X509)!");
191 191 }
192 192 });
193 193
... ...
  1 +/**
  2 + * Copyright © 2016-2021 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.device.credentials.lwm2m;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonIgnore;
  19 +import lombok.Getter;
  20 +import lombok.Setter;
  21 +import lombok.SneakyThrows;
  22 +import org.apache.commons.codec.binary.Hex;
  23 +
  24 +@Getter
  25 +@Setter
  26 +public abstract class AbstractLwM2MServerCredentialsWithKeys implements LwM2MServerCredentials {
  27 +
  28 + private String clientPublicKeyOrId;
  29 + private String clientSecretKey;
  30 +
  31 + @JsonIgnore
  32 + public byte[] getDecodedClientPublicKeyOrId() {
  33 + return getDecoded(clientPublicKeyOrId);
  34 + }
  35 +
  36 + @JsonIgnore
  37 + public byte[] getDecodedClientSecretKey() {
  38 + return getDecoded(clientSecretKey);
  39 + }
  40 +
  41 + @SneakyThrows
  42 + private static byte[] getDecoded(String key) {
  43 + return Hex.decodeHex(key.toLowerCase().toCharArray());
  44 + }
  45 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 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.device.credentials.lwm2m;
  17 +
  18 +import lombok.Getter;
  19 +import lombok.Setter;
  20 +
  21 +@Getter
  22 +@Setter
  23 +public class LwM2MBootstrapCredentials {
  24 + private LwM2MServerCredentials bootstrapServer;
  25 + private LwM2MServerCredentials lwm2mServer;
  26 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 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.device.credentials.lwm2m;
  17 +
  18 +import lombok.Getter;
  19 +import lombok.Setter;
  20 +
  21 +@Getter
  22 +@Setter
  23 +public class LwM2MDeviceCredentials {
  24 + private LwM2MClientCredentials client;
  25 + private LwM2MBootstrapCredentials bootstrap;
  26 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 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.device.credentials.lwm2m;
  17 +
  18 +import com.fasterxml.jackson.annotation.JsonIgnore;
  19 +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
  20 +import com.fasterxml.jackson.annotation.JsonSubTypes;
  21 +import com.fasterxml.jackson.annotation.JsonTypeInfo;
  22 +
  23 +@JsonTypeInfo(
  24 + use = JsonTypeInfo.Id.NAME,
  25 + property = "securityMode")
  26 +@JsonSubTypes({
  27 + @JsonSubTypes.Type(value = NoSecServerCredentials.class, name = "NO_SEC"),
  28 + @JsonSubTypes.Type(value = PSKServerCredentials.class, name = "PSK"),
  29 + @JsonSubTypes.Type(value = RPKServerCredentials.class, name = "RPK"),
  30 + @JsonSubTypes.Type(value = X509ServerCredentials.class, name = "X509")
  31 +})
  32 +@JsonIgnoreProperties(ignoreUnknown = true)
  33 +public interface LwM2MServerCredentials {
  34 +
  35 + @JsonIgnore
  36 + LwM2MSecurityMode getSecurityMode();
  37 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 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.device.credentials.lwm2m;
  17 +
  18 +public class NoSecServerCredentials implements LwM2MServerCredentials {
  19 +
  20 + @Override
  21 + public LwM2MSecurityMode getSecurityMode() {
  22 + return LwM2MSecurityMode.NO_SEC;
  23 + }
  24 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 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.device.credentials.lwm2m;
  17 +
  18 +public class PSKServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
  19 +
  20 + @Override
  21 + public LwM2MSecurityMode getSecurityMode() {
  22 + return LwM2MSecurityMode.PSK;
  23 + }
  24 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 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.device.credentials.lwm2m;
  17 +
  18 +public class RPKServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
  19 +
  20 + @Override
  21 + public LwM2MSecurityMode getSecurityMode() {
  22 + return LwM2MSecurityMode.RPK;
  23 + }
  24 +}
... ...
  1 +/**
  2 + * Copyright © 2016-2021 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.device.credentials.lwm2m;
  17 +
  18 +public class X509ServerCredentials extends AbstractLwM2MServerCredentialsWithKeys {
  19 +
  20 + @Override
  21 + public LwM2MSecurityMode getSecurityMode() {
  22 + return LwM2MSecurityMode.X509;
  23 + }
  24 +}
... ...
... ... @@ -227,6 +227,10 @@
227 227 <groupId>org.elasticsearch.client</groupId>
228 228 <artifactId>rest</artifactId>
229 229 </dependency>
  230 + <dependency>
  231 + <groupId>org.eclipse.leshan</groupId>
  232 + <artifactId>leshan-core</artifactId>
  233 + </dependency>
230 234 </dependencies>
231 235 <build>
232 236 <plugins>
... ...
... ... @@ -16,9 +16,9 @@
16 16 package org.thingsboard.server.dao.device;
17 17
18 18
19   -import com.fasterxml.jackson.databind.node.ObjectNode;
20 19 import lombok.extern.slf4j.Slf4j;
21   -import org.apache.commons.lang3.StringUtils;
  20 +import org.apache.commons.codec.binary.Hex;
  21 +import org.eclipse.leshan.core.util.SecurityUtil;
22 22 import org.hibernate.exception.ConstraintViolationException;
23 23 import org.springframework.beans.factory.annotation.Autowired;
24 24 import org.springframework.cache.annotation.CacheEvict;
... ... @@ -26,10 +26,18 @@ import org.springframework.cache.annotation.Cacheable;
26 26 import org.springframework.stereotype.Service;
27 27 import org.thingsboard.common.util.JacksonUtil;
28 28 import org.thingsboard.server.common.data.Device;
  29 +import org.thingsboard.server.common.data.StringUtils;
29 30 import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
  31 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapCredentials;
30 32 import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredentials;
  33 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials;
  34 +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MServerCredentials;
31 35 import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKClientCredentials;
  36 +import org.thingsboard.server.common.data.device.credentials.lwm2m.PSKServerCredentials;
  37 +import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKClientCredentials;
  38 +import org.thingsboard.server.common.data.device.credentials.lwm2m.RPKServerCredentials;
32 39 import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ClientCredentials;
  40 +import org.thingsboard.server.common.data.device.credentials.lwm2m.X509ServerCredentials;
33 41 import org.thingsboard.server.common.data.id.DeviceId;
34 42 import org.thingsboard.server.common.data.id.EntityId;
35 43 import org.thingsboard.server.common.data.id.TenantId;
... ... @@ -129,7 +137,7 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
129 137 if (StringUtils.isEmpty(mqttCredentials.getClientId()) && StringUtils.isEmpty(mqttCredentials.getUserName())) {
130 138 throw new DeviceCredentialsValidationException("Both mqtt client id and user name are empty!");
131 139 }
132   - if (StringUtils.isNotEmpty(mqttCredentials.getClientId()) && StringUtils.isNotEmpty(mqttCredentials.getPassword())) {
  140 + if (StringUtils.isNotEmpty(mqttCredentials.getClientId()) && StringUtils.isNotEmpty(mqttCredentials.getPassword()) && StringUtils.isEmpty(mqttCredentials.getUserName())) {
133 141 throw new DeviceCredentialsValidationException("Password cannot be specified along with client id");
134 142 }
135 143
... ... @@ -154,22 +162,16 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
154 162 }
155 163
156 164 private void formatSimpleLwm2mCredentials(DeviceCredentials deviceCredentials) {
157   - LwM2MClientCredentials clientCredentials;
158   - ObjectNode json;
  165 + LwM2MDeviceCredentials lwM2MCredentials;
159 166 try {
160   - json = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), ObjectNode.class);
161   - if (json == null) {
162   - throw new IllegalArgumentException();
163   - }
164   - clientCredentials = JacksonUtil.convertValue(json.get("client"), LwM2MClientCredentials.class);
165   - if (clientCredentials == null) {
166   - throw new IllegalArgumentException();
167   - }
  167 + lwM2MCredentials = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), LwM2MDeviceCredentials.class);
  168 + validateLwM2MDeviceCredentials(lwM2MCredentials);
168 169 } catch (IllegalArgumentException e) {
169 170 throw new DeviceCredentialsValidationException("Invalid credentials body for LwM2M credentials!");
170 171 }
171 172
172 173 String credentialsId = null;
  174 + LwM2MClientCredentials clientCredentials = lwM2MCredentials.getClient();
173 175
174 176 switch (clientCredentials.getSecurityConfigClientMode()) {
175 177 case NO_SEC:
... ... @@ -185,8 +187,8 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
185 187 String cert = EncryptionUtil.trimNewLines(x509Config.getCert());
186 188 String sha3Hash = EncryptionUtil.getSha3Hash(cert);
187 189 x509Config.setCert(cert);
188   - ((ObjectNode) json.get("client")).put("cert", cert);
189   - deviceCredentials.setCredentialsValue(JacksonUtil.toString(json));
  190 + ((X509ClientCredentials) clientCredentials).setCert(cert);
  191 + deviceCredentials.setCredentialsValue(JacksonUtil.toString(lwM2MCredentials));
190 192 credentialsId = sha3Hash;
191 193 } else {
192 194 credentialsId = x509Config.getEndpoint();
... ... @@ -199,6 +201,158 @@ public class DeviceCredentialsServiceImpl extends AbstractEntityService implemen
199 201 deviceCredentials.setCredentialsId(credentialsId);
200 202 }
201 203
  204 + private void validateLwM2MDeviceCredentials(LwM2MDeviceCredentials lwM2MCredentials) {
  205 + if (lwM2MCredentials == null) {
  206 + throw new DeviceCredentialsValidationException("LwM2M credentials should be specified!");
  207 + }
  208 +
  209 + LwM2MClientCredentials clientCredentials = lwM2MCredentials.getClient();
  210 + if (clientCredentials == null) {
  211 + throw new DeviceCredentialsValidationException("LwM2M client credentials should be specified!");
  212 + }
  213 + validateLwM2MClientCredentials(clientCredentials);
  214 +
  215 + LwM2MBootstrapCredentials bootstrapCredentials = lwM2MCredentials.getBootstrap();
  216 + if (bootstrapCredentials == null) {
  217 + throw new DeviceCredentialsValidationException("LwM2M bootstrap credentials should be specified!");
  218 + }
  219 +
  220 + LwM2MServerCredentials bootstrapServerCredentials = bootstrapCredentials.getBootstrapServer();
  221 + if (bootstrapServerCredentials == null) {
  222 + throw new DeviceCredentialsValidationException("LwM2M bootstrap server credentials should be specified!");
  223 + }
  224 + validateServerCredentials(bootstrapServerCredentials, "Bootstrap server");
  225 +
  226 + LwM2MServerCredentials lwm2mServerCredentials = bootstrapCredentials.getLwm2mServer();
  227 + if (lwm2mServerCredentials == null) {
  228 + throw new DeviceCredentialsValidationException("LwM2M lwm2m server credentials should be specified!");
  229 + }
  230 + validateServerCredentials(lwm2mServerCredentials, "LwM2M server");
  231 + }
  232 +
  233 + private void validateLwM2MClientCredentials(LwM2MClientCredentials clientCredentials) {
  234 + if (StringUtils.isEmpty(clientCredentials.getEndpoint())) {
  235 + throw new DeviceCredentialsValidationException("LwM2M client endpoint should be specified!");
  236 + }
  237 +
  238 + switch (clientCredentials.getSecurityConfigClientMode()) {
  239 + case NO_SEC:
  240 + break;
  241 + case PSK:
  242 + PSKClientCredentials pskCredentials = (PSKClientCredentials) clientCredentials;
  243 + if (StringUtils.isEmpty(pskCredentials.getIdentity())) {
  244 + throw new DeviceCredentialsValidationException("LwM2M client PSK identity should be specified!");
  245 + }
  246 +
  247 + String pskKey = pskCredentials.getKey();
  248 + if (StringUtils.isEmpty(pskKey)) {
  249 + throw new DeviceCredentialsValidationException("LwM2M client PSK key should be specified!");
  250 + }
  251 +
  252 + if (!pskKey.matches("-?[0-9a-fA-F]+")) {
  253 + throw new DeviceCredentialsValidationException("LwM2M client PSK key should be HexDecimal format!");
  254 + }
  255 +
  256 + if (pskKey.length() % 32 != 0 || pskKey.length() > 128) {
  257 + throw new DeviceCredentialsValidationException("LwM2M client PSK key must be 32, 64, 128 characters!");
  258 + }
  259 + break;
  260 + case RPK:
  261 + RPKClientCredentials rpkCredentials = (RPKClientCredentials) clientCredentials;
  262 +
  263 + if (StringUtils.isEmpty(rpkCredentials.getKey())) {
  264 + throw new DeviceCredentialsValidationException("LwM2M client RPK key should be specified!");
  265 + }
  266 +
  267 + try {
  268 + SecurityUtil.publicKey.decode(rpkCredentials.getDecodedKey());
  269 + } catch (Exception e) {
  270 + throw new DeviceCredentialsValidationException("LwM2M client RPK key should be in RFC7250 standard!");
  271 + }
  272 + break;
  273 + case X509:
  274 + X509ClientCredentials x509CCredentials = (X509ClientCredentials) clientCredentials;
  275 + if (x509CCredentials.getCert() != null) {
  276 + try {
  277 + SecurityUtil.certificate.decode(Hex.decodeHex(x509CCredentials.getCert().toLowerCase().toCharArray()));
  278 + } catch (Exception e) {
  279 + throw new DeviceCredentialsValidationException("LwM2M client X509 certificate should be in DER-encoded X.509 format!");
  280 + }
  281 + }
  282 + break;
  283 + }
  284 + }
  285 +
  286 + private void validateServerCredentials(LwM2MServerCredentials serverCredentials, String server) {
  287 + switch (serverCredentials.getSecurityMode()) {
  288 + case NO_SEC:
  289 + break;
  290 + case PSK:
  291 + PSKServerCredentials pskCredentials = (PSKServerCredentials) serverCredentials;
  292 + if (StringUtils.isEmpty(pskCredentials.getClientPublicKeyOrId())) {
  293 + throw new DeviceCredentialsValidationException(server + " client PSK public key or id should be specified!");
  294 + }
  295 +
  296 + String pskKey = pskCredentials.getClientSecretKey();
  297 + if (StringUtils.isEmpty(pskKey)) {
  298 + throw new DeviceCredentialsValidationException(server + " client PSK key should be specified!");
  299 + }
  300 +
  301 + if (!pskKey.matches("-?[0-9a-fA-F]+")) {
  302 + throw new DeviceCredentialsValidationException(server + " client PSK key should be HexDecimal format!");
  303 + }
  304 +
  305 + if (pskKey.length() % 32 != 0 || pskKey.length() > 128) {
  306 + throw new DeviceCredentialsValidationException(server + " client PSK key must be 32, 64, 128 characters!");
  307 + }
  308 + break;
  309 + case RPK:
  310 + RPKServerCredentials rpkCredentials = (RPKServerCredentials) serverCredentials;
  311 +
  312 + if (StringUtils.isEmpty(rpkCredentials.getClientPublicKeyOrId())) {
  313 + throw new DeviceCredentialsValidationException(server + " client RPK public key or id should be specified!");
  314 + }
  315 +
  316 + try {
  317 + SecurityUtil.publicKey.decode(rpkCredentials.getDecodedClientPublicKeyOrId());
  318 + } catch (Exception e) {
  319 + throw new DeviceCredentialsValidationException(server + " client RPK public key or id should be in RFC7250 standard!");
  320 + }
  321 +
  322 + if (StringUtils.isEmpty(rpkCredentials.getClientSecretKey())) {
  323 + throw new DeviceCredentialsValidationException(server + " client RPK secret key should be specified!");
  324 + }
  325 +
  326 + try {
  327 + SecurityUtil.privateKey.decode(rpkCredentials.getDecodedClientSecretKey());
  328 + } catch (Exception e) {
  329 + throw new DeviceCredentialsValidationException(server + " client RPK secret key should be in RFC5958 standard!");
  330 + }
  331 + break;
  332 + case X509:
  333 + X509ServerCredentials x509CCredentials = (X509ServerCredentials) serverCredentials;
  334 + if (StringUtils.isEmpty(x509CCredentials.getClientPublicKeyOrId())) {
  335 + throw new DeviceCredentialsValidationException(server + " client X509 public key or id should be specified!");
  336 + }
  337 +
  338 + try {
  339 + SecurityUtil.certificate.decode(x509CCredentials.getDecodedClientPublicKeyOrId());
  340 + } catch (Exception e) {
  341 + throw new DeviceCredentialsValidationException(server + " client X509 public key or id should be in DER-encoded X.509 format!");
  342 + }
  343 + if (StringUtils.isEmpty(x509CCredentials.getClientSecretKey())) {
  344 + throw new DeviceCredentialsValidationException(server + " client X509 secret key should be specified!");
  345 + }
  346 +
  347 + try {
  348 + SecurityUtil.privateKey.decode(x509CCredentials.getDecodedClientSecretKey());
  349 + } catch (Exception e) {
  350 + throw new DeviceCredentialsValidationException(server + " client X509 secret key should be in RFC5958 standard!");
  351 + }
  352 + break;
  353 + }
  354 + }
  355 +
202 356 @Override
203 357 @CacheEvict(cacheNames = DEVICE_CREDENTIALS_CACHE, key = "'deviceCredentials_' + #deviceCredentials.credentialsId")
204 358 public void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials) {
... ...