diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 9538555b2c..33e2ddee01 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -136,6 +136,7 @@ public class ThingsboardInstallService { dataUpdateService.updateData("3.6.4"); entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists(); systemDataLoaderService.updateDefaultNotificationConfigs(false); + systemDataLoaderService.updateJwtSettings(); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 8c86c16edb..bdd0494443 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -19,13 +19,17 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Profile; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @@ -81,6 +85,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; @@ -100,14 +105,11 @@ import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.dao.widget.WidgetTypeService; -import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; -import jakarta.annotation.Nullable; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.TreeMap; @@ -117,79 +119,51 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; +import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault; +import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.validateTokenSigningKeyLength; @Service @Profile("install") @Slf4j +@RequiredArgsConstructor public class DefaultSystemDataLoaderService implements SystemDataLoaderService { public static final String CUSTOMER_CRED = "customer"; public static final String ACTIVITY_STATE = "active"; - @Autowired - private InstallScripts installScripts; + private final InstallScripts installScripts; + private final UserService userService; + private final AdminSettingsService adminSettingsService; + private final TenantService tenantService; + private final TenantProfileService tenantProfileService; + private final CustomerService customerService; + private final DeviceService deviceService; + private final DeviceProfileService deviceProfileService; + private final AttributesService attributesService; + private final DeviceCredentialsService deviceCredentialsService; + private final RuleChainService ruleChainService; + private final TimeseriesService tsService; + private final DeviceConnectivityConfiguration connectivityConfiguration; + private final QueueService queueService; + private final JwtSettingsService jwtSettingsService; + private final NotificationSettingsService notificationSettingsService; + private final NotificationTargetService notificationTargetService; @Autowired private BCryptPasswordEncoder passwordEncoder; - @Autowired - private UserService userService; - - @Autowired - private AdminSettingsService adminSettingsService; - - @Autowired - private WidgetTypeService widgetTypeService; - - @Autowired - private WidgetsBundleService widgetsBundleService; - - @Autowired - private TenantService tenantService; - - @Autowired - private TenantProfileService tenantProfileService; - - @Autowired - private CustomerService customerService; - - @Autowired - private DeviceService deviceService; - - @Autowired - private DeviceProfileService deviceProfileService; - - @Autowired - private AttributesService attributesService; - - @Autowired - private DeviceCredentialsService deviceCredentialsService; - - @Autowired - private RuleChainService ruleChainService; - - @Autowired - private TimeseriesService tsService; - - @Autowired - private DeviceConnectivityConfiguration connectivityConfiguration; - @Value("${state.persistToTelemetry:false}") @Getter private boolean persistActivityToTelemetry; - @Lazy - @Autowired - private QueueService queueService; - - @Autowired - private JwtSettingsService jwtSettingsService; - - @Autowired - private NotificationSettingsService notificationSettingsService; - - @Autowired - private NotificationTargetService notificationTargetService; + @Value("${security.jwt.tokenExpirationTime:9000}") + private Integer tokenExpirationTime; + @Value("${security.jwt.refreshTokenExpTime:604800}") + private Integer refreshTokenExpTime; + @Value("${security.jwt.tokenIssuer:thingsboard.io}") + private String tokenIssuer; + @Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}") + private String tokenSigningKey; @Bean protected BCryptPasswordEncoder passwordEncoder() { @@ -295,7 +269,42 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { @Override public void createRandomJwtSettings() throws Exception { - jwtSettingsService.createRandomJwtSettings(); + if (jwtSettingsService.getJwtSettings() == null) { + log.info("Creating JWT admin settings..."); + var jwtSettings = new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); + if (isSigningKeyDefault(jwtSettings) || !validateTokenSigningKeyLength(jwtSettings)) { + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + } + jwtSettingsService.saveJwtSettings(jwtSettings); + } else { + log.info("Skip creating JWT admin settings because they already exist."); + } + } + + @Override + public void updateJwtSettings() { + JwtSettings jwtSettings = jwtSettingsService.getJwtSettings(); + boolean invalidSignKey = false; + String warningMessage = null; + + if (isSigningKeyDefault(jwtSettings)) { + warningMessage = "The platform is using the default JWT Signing Key, which is a security risk."; + invalidSignKey = true; + } else if (!validateTokenSigningKeyLength(jwtSettings)) { + warningMessage = "The JWT Signing Key is shorter than 512 bits, which is a security risk."; + invalidSignKey = true; + } + + if (invalidSignKey) { + log.warn("WARNING: {}. A new JWT Signing Key has been added automatically. " + + "You can change the JWT Signing Key using the Web UI: " + + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); + + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + jwtSettingsService.saveJwtSettings(jwtSettings); + } } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java index 9058db4172..3ba938bd90 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlAbstractDatabaseSchemaService.java @@ -84,13 +84,17 @@ public abstract class SqlAbstractDatabaseSchemaService implements DatabaseSchema } protected void executeQuery(String query) { + executeQuery(query, null); + } + + protected void executeQuery(String query, String logQuery) { + logQuery = logQuery != null ? logQuery : query; try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { conn.createStatement().execute(query); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script - log.info("Successfully executed query: {}", query); + log.info("Successfully executed query: {}", logQuery); Thread.sleep(5000); } catch (InterruptedException | SQLException e) { - log.error("Failed to execute query: {} due to: {}", query, e.getMessage()); - throw new RuntimeException("Failed to execute query: " + query, e); + throw new RuntimeException("Failed to execute query: " + logQuery, e); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java index b1a41aadcd..5e1357a48e 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java @@ -56,6 +56,7 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer @Override public void createCustomerTitleUniqueConstraintIfNotExists() { executeQuery("DO $$ BEGIN IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'customer_title_unq_key') THEN " + - "ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;"); + "ALTER TABLE customer ADD CONSTRAINT customer_title_unq_key UNIQUE(tenant_id, title); END IF; END; $$;", + "create 'customer_title_unq_key' constraint if it doesn't already exist!"); } } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index 7c05ff620f..71c829ee11 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -25,6 +25,8 @@ public interface SystemDataLoaderService { void createRandomJwtSettings() throws Exception; + void updateJwtSettings() throws Exception; + void createOAuth2Templates() throws Exception; void loadSystemWidgets() throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index cdb3f19601..42c516a8a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -175,7 +175,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService tbClusterService; - private final Optional notificationCenter; private final JwtSettingsValidator jwtSettingsValidator; - - @Value("${security.jwt.tokenExpirationTime:9000}") - private Integer tokenExpirationTime; - @Value("${security.jwt.refreshTokenExpTime:604800}") - private Integer refreshTokenExpTime; - @Value("${security.jwt.tokenIssuer:thingsboard.io}") - private String tokenIssuer; - @Value("${security.jwt.tokenSigningKey:thingsboardDefaultSigningKey}") - private String tokenSigningKey; + private final Optional jwtTokenFactory; private volatile JwtSettings jwtSettings = null; //lazy init - /** - * Create JWT admin settings is intended to be called from Install scripts only - */ - @Override - public void createRandomJwtSettings() { - if (getJwtSettingsFromDb() == null) { - log.info("Creating JWT admin settings..."); - this.jwtSettings = getJwtSettingsFromYml(); - if (isSigningKeyDefault(jwtSettings)) { - this.jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); - } - saveJwtSettings(jwtSettings); - } else { - log.info("Skip creating JWT admin settings because they already exist."); - } - } - - /** - * Create JWT admin settings is intended to be called from Upgrade scripts only - */ - @Override - public void saveLegacyYmlSettings() { - log.info("Saving legacy JWT admin settings from YML..."); - if (getJwtSettingsFromDb() == null) { - saveJwtSettings(getJwtSettingsFromYml()); - } - } - @Override public JwtSettings saveJwtSettings(JwtSettings jwtSettings) { jwtSettingsValidator.validate(jwtSettings); @@ -105,7 +66,9 @@ public class DefaultJwtSettingsService implements JwtSettingsService { @Override public JwtSettings reloadJwtSettings() { log.trace("Executing reloadJwtSettings"); - return getJwtSettings(true); + var settings = getJwtSettings(true); + jwtTokenFactory.ifPresent(JwtTokenFactory::reload); + return settings; } @Override @@ -118,30 +81,13 @@ public class DefaultJwtSettingsService implements JwtSettingsService { if (this.jwtSettings == null || forceReload) { synchronized (this) { if (this.jwtSettings == null || forceReload) { - JwtSettings result = getJwtSettingsFromDb(); - if (result == null) { - result = getJwtSettingsFromYml(); - log.warn("Loading the JWT settings from YML since there are no settings in DB. Looks like the upgrade script was not applied."); - } - if (isSigningKeyDefault(result)) { - log.warn("WARNING: The platform is configured to use default JWT Signing Key. " + - "This is a security issue that needs to be resolved. Please change the JWT Signing Key using the Web UI. " + - "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator."); - notificationCenter.ifPresent(notificationCenter -> { - notificationCenter.sendGeneralWebNotification(TenantId.SYS_TENANT_ID, new SystemAdministratorsFilter(), DefaultNotifications.jwtSigningKeyIssue.toTemplate()); - }); - } - this.jwtSettings = result; + jwtSettings = getJwtSettingsFromDb(); } } } return this.jwtSettings; } - private JwtSettings getJwtSettingsFromYml() { - return new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); - } - private JwtSettings getJwtSettingsFromDb() { AdminSettings adminJwtSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, ADMIN_SETTINGS_JWT_KEY); return adminJwtSettings != null ? mapAdminToJwtSettings(adminJwtSettings) : null; @@ -161,8 +107,12 @@ public class DefaultJwtSettingsService implements JwtSettingsService { return adminJwtSettings; } - private boolean isSigningKeyDefault(JwtSettings settings) { + public static boolean isSigningKeyDefault(JwtSettings settings) { return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey()); } + public static boolean validateTokenSigningKeyLength(JwtSettings settings) { + return Base64.getDecoder().decode(settings.getTokenSigningKey()).length * Byte.SIZE >= KEY_LENGTH; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index b807f65d36..ea8c67e3dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -27,6 +27,9 @@ import java.util.Base64; import java.util.Optional; import java.util.concurrent.TimeUnit; +import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault; +import static org.thingsboard.server.service.security.model.token.JwtTokenFactory.KEY_LENGTH; + @Component @RequiredArgsConstructor public class DefaultJwtSettingsValidator implements JwtSettingsValidator { @@ -59,8 +62,8 @@ public class DefaultJwtSettingsValidator implements JwtSettingsValidator { if (Arrays.isNullOrEmpty(decodedKey)) { throw new DataValidationException("JWT token signing key should be non-empty after Base64 decoding!"); } - if (decodedKey.length * Byte.SIZE < 256 && !JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT.equals(jwtSettings.getTokenSigningKey())) { - throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 256 bits of data!"); + if (decodedKey.length * Byte.SIZE < KEY_LENGTH && !isSigningKeyDefault(jwtSettings)) { + throw new DataValidationException("JWT token signing key should be a Base64 encoded string representing at least 512 bits of data!"); } System.arraycopy(decodedKey, 0, RandomUtils.nextBytes(decodedKey.length), 0, decodedKey.length); //secure memory diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java index 19095ad0b9..d3aa261b00 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/JwtSettingsService.java @@ -26,10 +26,6 @@ public interface JwtSettingsService { JwtSettings reloadJwtSettings(); - void createRandomJwtSettings(); - - void saveLegacyYmlSettings(); - JwtSettings saveJwtSettings(JwtSettings jwtSettings); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java index 08622f578a..f2e276a0ef 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java @@ -16,16 +16,19 @@ package org.thingsboard.server.service.security.model.token; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ClaimsBuilder; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; @@ -41,7 +44,10 @@ import org.thingsboard.server.service.security.exception.JwtExpiredTokenExceptio import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import java.time.ZonedDateTime; +import java.util.Base64; import java.util.Collections; import java.util.Date; import java.util.List; @@ -53,6 +59,8 @@ import java.util.stream.Collectors; @Slf4j public class JwtTokenFactory { + public static int KEY_LENGTH = Jwts.SIG.HS512.getKeyBitLength(); + private static final String SCOPES = "scopes"; private static final String USER_ID = "userId"; private static final String FIRST_NAME = "firstName"; @@ -63,8 +71,12 @@ public class JwtTokenFactory { private static final String CUSTOMER_ID = "customerId"; private static final String SESSION_ID = "sessionId"; + @Lazy private final JwtSettingsService jwtSettingsService; + private volatile JwtParser jwtParser; + private volatile SecretKey secretKey; + /** * Factory method for issuing new JWT Tokens. */ @@ -95,7 +107,7 @@ public class JwtTokenFactory { public SecurityUser parseAccessJwtToken(String token) { Jws jwsClaims = parseTokenClaims(token); - Claims claims = jwsClaims.getBody(); + Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); @SuppressWarnings("unchecked") List scopes = claims.get(SCOPES, List.class); @@ -140,14 +152,14 @@ public class JwtTokenFactory { String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), jwtSettingsService.getJwtSettings().getRefreshTokenExpTime()) .claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID) - .setId(UUID.randomUUID().toString()).compact(); + .id(UUID.randomUUID().toString()).compact(); return new AccessJwtToken(token); } public SecurityUser parseRefreshToken(String token) { Jws jwsClaims = parseTokenClaims(token); - Claims claims = jwsClaims.getBody(); + Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); @SuppressWarnings("unchecked") List scopes = claims.get(SCOPES, List.class); @@ -176,6 +188,11 @@ public class JwtTokenFactory { return new AccessJwtToken(jwtBuilder.compact()); } + public void reload() { + getSecretKey(true); + getJwtParser(true); + } + private JwtBuilder setUpToken(SecurityUser securityUser, List scopes, long expirationTime) { if (StringUtils.isBlank(securityUser.getEmail())) { throw new IllegalArgumentException("Cannot create JWT Token without username/email"); @@ -183,28 +200,27 @@ public class JwtTokenFactory { UserPrincipal principal = securityUser.getUserPrincipal(); - Claims claims = Jwts.claims().setSubject(principal.getValue()); - claims.put(USER_ID, securityUser.getId().getId().toString()); - claims.put(SCOPES, scopes); + ClaimsBuilder claimsBuilder = Jwts.claims() + .subject(principal.getValue()) + .add(USER_ID, securityUser.getId().getId().toString()) + .add(SCOPES, scopes); if (securityUser.getSessionId() != null) { - claims.put(SESSION_ID, securityUser.getSessionId()); + claimsBuilder.add(SESSION_ID, securityUser.getSessionId()); } ZonedDateTime currentTime = ZonedDateTime.now(); return Jwts.builder() - .setClaims(claims) - .setIssuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) - .setIssuedAt(Date.from(currentTime.toInstant())) - .setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) - .signWith(SignatureAlgorithm.HS512, jwtSettingsService.getJwtSettings().getTokenSigningKey()); + .claims(claimsBuilder.build()) + .issuer(jwtSettingsService.getJwtSettings().getTokenIssuer()) + .issuedAt(Date.from(currentTime.toInstant())) + .expiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant())) + .signWith(getSecretKey(false), Jwts.SIG.HS512); } public Jws parseTokenClaims(String token) { try { - return Jwts.parser() - .setSigningKey(jwtSettingsService.getJwtSettings().getTokenSigningKey()) - .parseClaimsJws(token); + return getJwtParser(false).parseSignedClaims(token); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) { log.debug("Invalid JWT Token", ex); throw new BadCredentialsException("Invalid JWT token: ", ex); @@ -220,4 +236,28 @@ public class JwtTokenFactory { return new JwtPair(accessToken.getToken(), refreshToken.getToken()); } + private SecretKey getSecretKey(boolean forceReload) { + if (secretKey == null || forceReload) { + synchronized (this) { + if (secretKey == null || forceReload) { + byte[] decodedToken = Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey()); + secretKey = new SecretKeySpec(decodedToken, "HmacSHA512"); + } + } + } + return secretKey; + } + + private JwtParser getJwtParser(boolean forceReload) { + if (jwtParser == null || forceReload) { + synchronized (this) { + if (jwtParser == null || forceReload) { + jwtParser = Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(jwtSettingsService.getJwtSettings().getTokenSigningKey()))) + .build(); + } + } + } + return jwtParser; + } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java index 7f956f6970..cf01dec208 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java @@ -22,10 +22,12 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; +import java.util.Base64; import java.util.Date; import java.util.concurrent.TimeUnit; @@ -40,14 +42,14 @@ public class OAuth2AppTokenFactory { public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) { Jws jwsClaims; try { - jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken); + jwsClaims = Jwts.parser().verifyWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(appSecret))).build().parseSignedClaims(appToken); } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { throw new IllegalArgumentException("Invalid Application token: ", ex); } catch (ExpiredJwtException expiredEx) { throw new IllegalArgumentException("Application token expired", expiredEx); } - Claims claims = jwsClaims.getBody(); + Claims claims = jwsClaims.getPayload(); Date expiration = claims.getExpiration(); if (expiration == null) { throw new IllegalArgumentException("Application token must have expiration date"); diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 96e52d6c12..b83a1b567c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -21,9 +21,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.Jws; import lombok.extern.slf4j.Slf4j; import org.awaitility.Awaitility; import org.hamcrest.Matcher; @@ -121,6 +119,7 @@ import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; +import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import java.io.IOException; import java.lang.invoke.MethodHandles; @@ -241,6 +240,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired protected ClaimDevicesService claimDevicesService; + @Autowired + private JwtTokenFactory jwtTokenFactory; + @SpyBean protected MailService mailService; @@ -561,13 +563,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected void validateJwtToken(String token, String username) { - Assert.assertNotNull(token); - Assert.assertFalse(token.isEmpty()); - int i = token.lastIndexOf('.'); - Assert.assertTrue(i > 0); - String withoutSignature = token.substring(0, i + 1); - Jwt jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); - Claims claims = jwsClaims.getBody(); + Jws jwsClaims = jwtTokenFactory.parseTokenClaims(token); + Claims claims = jwsClaims.getPayload(); String subject = claims.getSubject(); Assert.assertEquals(username, subject); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java index 430ff2eb09..8876bf8c24 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java @@ -42,7 +42,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @Slf4j @DaoSqlTest public class AdminControllerTest extends AbstractControllerTest { - final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "thingsboardDefaultSigningKey"); + final JwtSettings defaultJwtSettings = new JwtSettings(9000, 604800, "thingsboard.io", "QmlicmJkZk9tSzZPVFozcWY0Sm94UVhybmtBWXZ5YmZMOUZSZzZvcUFiOVhsb3VHUThhUWJGaXp3UHhtcGZ6Tw=="); @Test public void testFindAdminSettingsByKey() throws Exception { @@ -168,7 +168,7 @@ public class AdminControllerTest extends AbstractControllerTest { assertThat(jwtSettings).isEqualTo(defaultJwtSettings); jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(256 / Byte.SIZE).getBytes(StandardCharsets.UTF_8))); + RandomStringUtils.randomAlphanumeric(512 / Byte.SIZE).getBytes(StandardCharsets.UTF_8))); doPost("/api/admin/jwtSettings", jwtSettings).andExpect(status().isOk()); diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 6bd0cd9c0e..8e0b294241 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -640,7 +640,7 @@ public class TbRuleEngineQueueConsumerManagerTest { } private void verifyMsgProcessed(TbMsg tbMsg) { - await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> { + await().atMost(15, TimeUnit.SECONDS).untilAsserted(() -> { verify(actorContext, atLeastOnce()).tell(argThat(msg -> { return ((QueueToRuleEngineMsg) msg).getMsg().getId().equals(tbMsg.getId()); })); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java index 2d39dd9905..e6c6cfbac4 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java @@ -16,16 +16,14 @@ package org.thingsboard.server.service.security.auth; import io.jsonwebtoken.Claims; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.Before; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.api.NotificationCenter; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; -import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; -import org.thingsboard.server.common.data.notification.targets.platform.SystemAdministratorsFilter; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.security.model.JwtToken; @@ -38,6 +36,8 @@ import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.AccessJwtToken; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Calendar; import java.util.Date; import java.util.Optional; @@ -45,19 +45,13 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class JwtTokenFactoryTest { private JwtTokenFactory tokenFactory; private AdminSettingsService adminSettingsService; - private NotificationCenter notificationCenter; private JwtSettingsService jwtSettingsService; private JwtSettings jwtSettings; @@ -66,12 +60,11 @@ public class JwtTokenFactoryTest { public void beforeEach() { jwtSettings = new JwtSettings(); jwtSettings.setTokenIssuer("tb"); - jwtSettings.setTokenSigningKey("abewafaf"); + jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString(RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2)); jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7)); adminSettingsService = mock(AdminSettingsService.class); - notificationCenter = mock(NotificationCenter.class); jwtSettingsService = mockJwtSettingsService(); mockJwtSettings(jwtSettings); @@ -169,21 +162,6 @@ public class JwtTokenFactoryTest { }); } - @Test - public void testJwtSigningKeyIssueNotification() { - JwtSettings badJwtSettings = jwtSettings; - badJwtSettings.setTokenSigningKey(JwtSettingsService.TOKEN_SIGNING_KEY_DEFAULT); - mockJwtSettings(badJwtSettings); - jwtSettingsService = mockJwtSettingsService(); - - for (int i = 0; i < 5; i++) { // to check if notification is not sent twice - jwtSettingsService.getJwtSettings(); - } - verify(notificationCenter, times(1)).sendGeneralWebNotification(eq(TenantId.SYS_TENANT_ID), - isA(SystemAdministratorsFilter.class), argThat(template -> template.getConfiguration().getDeliveryMethodsTemplates().get(NotificationDeliveryMethod.WEB) - .getBody().contains("The platform is configured to use default JWT Signing Key"))); - } - private void mockJwtSettings(JwtSettings settings) { AdminSettings adminJwtSettings = new AdminSettings(); adminJwtSettings.setJsonValue(JacksonUtil.valueToTree(settings)); @@ -192,12 +170,11 @@ public class JwtTokenFactoryTest { } private DefaultJwtSettingsService mockJwtSettingsService() { - return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), - Optional.of(notificationCenter), new DefaultJwtSettingsValidator()); + return new DefaultJwtSettingsService(adminSettingsService, Optional.empty(), new DefaultJwtSettingsValidator(), Optional.empty()); } private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) { - Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getBody(); + Claims claims = tokenFactory.parseTokenClaims(jwtToken.getToken()).getPayload(); assertThat(claims.getExpiration()).matches(actualExpirationTime -> { Calendar expirationTime = Calendar.getInstance(); expirationTime.setTime(new Date()); diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java b/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java index f3530e99d2..9680fd2b96 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/limits/DefaultRateLimitService.java @@ -63,12 +63,21 @@ public class DefaultRateLimitService implements RateLimitService { @Override public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level) { + return checkRateLimit(api, tenantId, level, false); + } + + @Override + public boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level, boolean ignoreTenantNotFound) { if (tenantId.isSysTenantId()) { return true; } TenantProfile tenantProfile = tenantProfileProvider.get(tenantId); if (tenantProfile == null) { - throw new TenantProfileNotFoundException(tenantId); + if (ignoreTenantNotFound) { + return true; + } else { + throw new TenantProfileNotFoundException(tenantId); + } } String rateLimitConfig = tenantProfile.getProfileConfiguration() diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java b/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java index 84c22c514b..3573ee2b8c 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/limits/RateLimitService.java @@ -24,6 +24,8 @@ public interface RateLimitService { boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level); + boolean checkRateLimit(LimitedApi api, TenantId tenantId, Object level, boolean ignoreTenantNotFound); + boolean checkRateLimit(LimitedApi api, Object level, String rateLimitConfig); void cleanUp(LimitedApi api, Object level); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java index d8368f247b..07c158830f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/JwtSettings.java @@ -48,7 +48,7 @@ public class JwtSettings { * Key is used to sign {@link JwtToken}. * Base64 encoded */ - @Schema(description = "The JWT key is used to sing token. Base64 encoded.", example = "cTU4WnNqemI2aU5wbWVjdm1vYXRzanhjNHRUcXliMjE=") + @Schema(description = "The JWT key is used to sing token. Base64 encoded.", example = "dkVTUzU2M2VMWUNwVVltTUhQU2o5SUM0Tkc3M0k2Ykdwcm85QTl6R0RaQ252OFlmVDk2OEptZXBNcndGeExFZg==") private String tokenSigningKey; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java index 4f617c2b9d..1a4c3ffab2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java +++ b/dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java @@ -372,15 +372,6 @@ public class DefaultNotifications { .build()) .build(); - public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder() - .name("JWT Signing Key issue notification") - .type(NotificationType.GENERAL) - .subject("WARNING: security issue") - .text("The platform is configured to use default JWT Signing Key. Please change it on the security settings page") - .icon("warning").color(YELLOW_COLOR) - .button("Go to settings").link("/security-settings/general") - .build(); - private final NotificationTemplateService templateService; private final NotificationRuleService ruleService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java index 48b2471217..34316bc615 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java @@ -27,20 +27,20 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.stats.DefaultCounter; import org.thingsboard.server.common.stats.StatsCounter; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.nosql.CassandraStatementTask; -import org.thingsboard.server.common.data.limit.LimitedApi; -import org.thingsboard.server.cache.limits.RateLimitService; -import jakarta.annotation.Nullable; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -114,7 +114,7 @@ public abstract class AbstractBufferedRateExecutor6.2.4 6.2.4 5.1.2 - 0.9.1 + 0.12.5 2.0.13 2.23.1 1.5.5 diff --git a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json index 104eb18abd..2c552d6ab6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-ar_AE.json +++ b/ui-ngx/src/assets/locale/locale.constant-ar_AE.json @@ -482,9 +482,9 @@ "issuer-name": "اسم الجهة المصدرة", "issuer-name-required": "اسم الجهة المصدرة مطلوب.", "signings-key": "مفتاح التوقيع", - "signings-key-hint": "سلسلة مشفرة بتنسيق Base64 تمثل ما لا يقل عن 256 بت من البيانات.", + "signings-key-hint": "سلسلة مشفرة بتنسيق Base64 تمثل ما لا يقل عن 512 بت من البيانات.", "signings-key-required": "مفتاح التوقيع مطلوب.", - "signings-key-min-length": "يجب أن يكون مفتاح التوقيع ما لا يقل عن 256 بت من البيانات.", + "signings-key-min-length": "يجب أن يكون مفتاح التوقيع ما لا يقل عن 512 بت من البيانات.", "signings-key-base64": "يجب أن يكون مفتاح التوقيع بتنسيق base64.", "expiration-time": "وقت انتهاء صلاحية الرمز (ثانية)", "expiration-time-required": "وقت انتهاء صلاحية الرمز مطلوب.", 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 87f611abe8..e16dcee50b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -457,9 +457,9 @@ "issuer-name": "Issuer name", "issuer-name-required": "Issuer name is required.", "signings-key": "Signing key", - "signings-key-hint": "Base64 encoded string representing at least 256 bits of data.", + "signings-key-hint": "Base64 encoded string representing at least 512 bits of data.", "signings-key-required": "Signing key is required.", - "signings-key-min-length": "Signing key must be at least 256 bits of data.", + "signings-key-min-length": "Signing key must be at least 512 bits of data.", "signings-key-base64": "Signing key must be base64 format.", "expiration-time": "Token expiration time (sec)", "expiration-time-required": "Token expiration time is required.", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_ES.json b/ui-ngx/src/assets/locale/locale.constant-es_ES.json index 4dd93cad1d..314a70e7fa 100644 --- a/ui-ngx/src/assets/locale/locale.constant-es_ES.json +++ b/ui-ngx/src/assets/locale/locale.constant-es_ES.json @@ -431,9 +431,9 @@ "issuer-name": "Nombre del emisor", "issuer-name-required": "Se requiere nombre del emisor.", "signings-key": "Clave de firma", - "signings-key-hint": "Una string codificada en Base64 representando por lo menos 256 bits de datos.", + "signings-key-hint": "Una string codificada en Base64 representando por lo menos 512 bits de datos.", "signings-key-required": "Se requiere clave de firma.", - "signings-key-min-length": "La clave de firma debe tener al menos 256 bits de datos.", + "signings-key-min-length": "La clave de firma debe tener al menos 512 bits de datos.", "signings-key-base64": "La clave de firma debe estar en formato base64.", "expiration-time": "Caducidad del token (en segundos)", "expiration-time-required": "Se requiere caducidad del token.", diff --git a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json index ed74dfe7fb..e8e2cb1f7d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-nl_BE.json +++ b/ui-ngx/src/assets/locale/locale.constant-nl_BE.json @@ -425,9 +425,9 @@ "issuer-name": "Naam van de uitgever", "issuer-name-required": "De naam van de uitgever is vereist.", "signings-key": "Sleutel ondertekenen", - "signings-key-hint": "Base64-gecodeerde tekenreeks die ten minste 256 bits aan gegevens vertegenwoordigt.", + "signings-key-hint": "Base64-gecodeerde tekenreeks die ten minste 512 bits aan gegevens vertegenwoordigt.", "signings-key-required": "Ondertekeningssleutel is vereist.", - "signings-key-min-length": "De ondertekeningssleutel moet ten minste 256 bits aan gegevens bevatten.", + "signings-key-min-length": "De ondertekeningssleutel moet ten minste 512 bits aan gegevens bevatten.", "signings-key-base64": "De ondertekeningssleutel moet de base64-indeling hebben.", "expiration-time": "Vervaltijd token (sec)", "expiration-time-required": "De vervaltijd van het token is vereist.", diff --git a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json index a46c732c6f..b30d4afc38 100644 --- a/ui-ngx/src/assets/locale/locale.constant-pl_PL.json +++ b/ui-ngx/src/assets/locale/locale.constant-pl_PL.json @@ -457,9 +457,9 @@ "issuer-name": "Nazwa emitenta", "issuer-name-required": "Nazwa emitenta jest wymagana.", "signings-key": "Klucz do podpisu", - "signings-key-hint": "Ciąg zakodowany w formacie Base64 reprezentujący co najmniej 256 bitów danych.", + "signings-key-hint": "Ciąg zakodowany w formacie Base64 reprezentujący co najmniej 512 bitów danych.", "signings-key-required": "Klucz do podpisu jest wymagany.", - "signings-key-min-length": "Klucz podpisujący musi mieć co najmniej 256 bitów danych.", + "signings-key-min-length": "Klucz podpisujący musi mieć co najmniej 512 bitów danych.", "signings-key-base64": "Klucz podpisujący musi być w formacie base64.", "expiration-time": "Czas ważności tokena (s)", "expiration-time-required": "Czas ważności tokena jest wymagany.", diff --git a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json index 541d5a0ed6..99b13168c2 100644 --- a/ui-ngx/src/assets/locale/locale.constant-zh_CN.json +++ b/ui-ngx/src/assets/locale/locale.constant-zh_CN.json @@ -451,9 +451,9 @@ "issuer-name": "发行者名称", "issuer-name-required": "发行者名称必填。", "signings-key": "签名密钥", - "signings-key-hint": "Base64编码的字符串,至少256位数据。", + "signings-key-hint": "Base64编码的字符串,至少512位数据。", "signings-key-required": "签名密钥必填。", - "signings-key-min-length": "签名密钥必须至少为256位的数据。", + "signings-key-min-length": "签名密钥必须至少为512位的数据。", "signings-key-base64": "签名密钥必须是Base64格式。", "expiration-time": "令牌过期时间(秒)", "expiration-time-required": "令牌过期时间是必填。",