From 2d390f5665b0dc4e8691e034d9c4ef2b76f934d5 Mon Sep 17 00:00:00 2001 From: Thomasr Date: Thu, 24 Apr 2025 14:43:36 -0400 Subject: [PATCH 1/4] Encrypt payload to the node-server and add header "X-ENCRYPTED" --- .../domain/encryption/EncryptionService.java | 2 + .../encryption/EncryptionServiceImpl.java | 17 +++++- .../plugin/client/DatasourcePluginClient.java | 52 ++++++++++++----- .../src/main/resources/application-debug.yaml | 7 ++- .../src/main/resources/application.yaml | 5 ++ .../node-service/src/controllers/plugins.ts | 31 +++++++--- server/node-service/src/server.ts | 10 ++++ server/node-service/src/utils/encryption.ts | 58 +++++++++++++++++++ 8 files changed, 158 insertions(+), 24 deletions(-) create mode 100644 server/node-service/src/utils/encryption.ts diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionService.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionService.java index 276f4059d..fdd173ed7 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionService.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionService.java @@ -4,6 +4,8 @@ public interface EncryptionService { String encryptString(String plaintext); + String encryptStringForNodeServer(String plaintext); + String decryptString(String encryptedText); String encryptPassword(String plaintext); diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java index 6524682b6..d1dafd8af 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java @@ -5,6 +5,7 @@ import org.lowcoder.sdk.config.CommonConfig; import org.lowcoder.sdk.config.CommonConfig.Encrypt; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.encrypt.Encryptors; import org.springframework.security.crypto.encrypt.TextEncryptor; @@ -14,13 +15,20 @@ public class EncryptionServiceImpl implements EncryptionService { private final TextEncryptor textEncryptor; + private final TextEncryptor textEncryptorForNodeServer; private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); @Autowired - public EncryptionServiceImpl(CommonConfig commonConfig) { + public EncryptionServiceImpl( + CommonConfig commonConfig, + @Value("${lowcoder.node-server.password}") String password, + @Value("${lowcoder.node-server.salt}") String salt + ) { Encrypt encrypt = commonConfig.getEncrypt(); String saltInHex = Hex.encodeHexString(encrypt.getSalt().getBytes()); this.textEncryptor = Encryptors.text(encrypt.getPassword(), saltInHex); + String saltInHexForNodeServer = Hex.encodeHexString(salt.getBytes()); + this.textEncryptorForNodeServer = Encryptors.text(password, saltInHexForNodeServer); } @Override @@ -30,6 +38,13 @@ public String encryptString(String plaintext) { } return textEncryptor.encrypt(plaintext); } + @Override + public String encryptStringForNodeServer(String plaintext) { + if (StringUtils.isEmpty(plaintext)) { + return plaintext; + } + return textEncryptorForNodeServer.encrypt(plaintext); + } @Override public String decryptString(String encryptedText) { diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java index f2aa878bb..812978193 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java @@ -5,6 +5,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; +import org.lowcoder.domain.encryption.EncryptionService; import org.lowcoder.domain.plugin.client.dto.DatasourcePluginDefinition; import org.lowcoder.domain.plugin.client.dto.GetPluginDynamicConfigRequestDTO; import org.lowcoder.infra.js.NodeServerClient; @@ -30,6 +31,8 @@ import static org.lowcoder.sdk.constants.GlobalContext.REQUEST; +import com.fasterxml.jackson.databind.ObjectMapper; + @Slf4j @RequiredArgsConstructor @Component @@ -46,12 +49,15 @@ public class DatasourcePluginClient implements NodeServerClient { private final CommonConfigHelper commonConfigHelper; private final NodeServerHelper nodeServerHelper; + private final EncryptionService encryptionService; private static final String PLUGINS_PATH = "plugins"; private static final String RUN_PLUGIN_QUERY = "runPluginQuery"; private static final String VALIDATE_PLUGIN_DATA_SOURCE_CONFIG = "validatePluginDataSourceConfig"; private static final String GET_PLUGIN_DYNAMIC_CONFIG = "getPluginDynamicConfig"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public Mono> getPluginDynamicConfigSafely(List getPluginDynamicConfigRequestDTOS) { return getPluginDynamicConfig(getPluginDynamicConfigRequestDTOS) .onErrorResume(throwable -> { @@ -119,21 +125,37 @@ public Flux getDatasourcePluginDefinitions() { @SuppressWarnings("unchecked") public Mono executeQuery(String pluginName, Object queryDsl, List> context, Object datasourceConfig) { return getAcceptLanguage() - .flatMap(language -> WEB_CLIENT - .post() - .uri(nodeServerHelper.createUri(RUN_PLUGIN_QUERY)) - .header(HttpHeaders.ACCEPT_LANGUAGE, language) - .bodyValue(Map.of("pluginName", pluginName, "dsl", queryDsl, "context", context, "dataSourceConfig", datasourceConfig)) - .exchangeToMono(response -> { - if (response.statusCode().is2xxSuccessful()) { - return response.bodyToMono(Map.class) - .map(map -> map.get("result")) - .map(QueryExecutionResult::success); - } - return response.bodyToMono(Map.class) - .map(map -> MapUtils.getString(map, "message")) - .map(QueryExecutionResult::errorWithMessage); - })); + .flatMap(language -> { + try { + Map body = Map.of( + "pluginName", pluginName, + "dsl", queryDsl, + "context", context, + "dataSourceConfig", datasourceConfig + ); + String json = OBJECT_MAPPER.writeValueAsString(body); + String encrypted = encryptionService.encryptStringForNodeServer(json); + return WEB_CLIENT + .post() + .uri(nodeServerHelper.createUri(RUN_PLUGIN_QUERY)) + .header(HttpHeaders.ACCEPT_LANGUAGE, language) + .header("X-Encrypted", "true") // Optional: custom header to indicate encryption + .bodyValue(encrypted) + .exchangeToMono(response -> { + if (response.statusCode().is2xxSuccessful()) { + return response.bodyToMono(Map.class) + .map(map -> map.get("result")) + .map(QueryExecutionResult::success); + } + return response.bodyToMono(Map.class) + .map(map -> MapUtils.getString(map, "message")) + .map(QueryExecutionResult::errorWithMessage); + }); + } catch (Exception e) { + log.error("Encryption error", e); + return Mono.error(new ServerException("Encryption error")); + } + }); } @SuppressWarnings("unchecked") diff --git a/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml b/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml index d52888ca7..4c85ab858 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml +++ b/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml @@ -61,4 +61,9 @@ logging: org.lowcoder: debug default: - query-timeout: ${LOWCODER_DEFAULT_QUERY_TIMEOUT:10s} \ No newline at end of file + query-timeout: ${LOWCODER_DEFAULT_QUERY_TIMEOUT:10s} + +lowcoder: + node-server: + password: ${LOWCODER_NODE_SERVICE_SECRET:lowcoderpwd} + salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:lowcodersalt} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/resources/application.yaml b/server/api-service/lowcoder-server/src/main/resources/application.yaml index e5058563c..783391083 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application.yaml +++ b/server/api-service/lowcoder-server/src/main/resources/application.yaml @@ -130,3 +130,8 @@ management: enabled: true diskspace: enabled: false + +lowcoder: + node-server: + password: ${LOWCODER_NODE_SERVICE_SECRET:lowcoderpwd} + salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:lowcodersalt} \ No newline at end of file diff --git a/server/node-service/src/controllers/plugins.ts b/server/node-service/src/controllers/plugins.ts index e20a109b3..d763586ce 100644 --- a/server/node-service/src/controllers/plugins.ts +++ b/server/node-service/src/controllers/plugins.ts @@ -3,6 +3,23 @@ import { Request, Response } from "express"; import _ from "lodash"; import { Config } from "lowcoder-sdk/dataSource"; import * as pluginServices from "../services/plugin"; +// Add import for decryption utility +import { decryptString } from "../utils/encryption"; // <-- implement this utility as needed + +async function getDecryptedBody(req: Request): Promise { + if (req.headers["x-encrypted"]) { + // Assume body is a raw encrypted string, decrypt and parse as JSON + const encrypted = typeof req.body === "string" ? req.body : req.body?.toString?.(); + if (!encrypted) throw badRequest("Missing encrypted body"); + const decrypted = await decryptString(encrypted); + try { + return JSON.parse(decrypted); + } catch (e) { + throw badRequest("Failed to parse decrypted body as JSON"); + } + } + return req.body; +} export async function listPlugins(req: Request, res: Response) { let ids = req.query["id"] || []; @@ -15,12 +32,10 @@ export async function listPlugins(req: Request, res: Response) { } export async function runPluginQuery(req: Request, res: Response) { - const { pluginName, dsl, context, dataSourceConfig } = req.body; + const body = await getDecryptedBody(req); + const { pluginName, dsl, context, dataSourceConfig } = body; const ctx = pluginServices.getPluginContext(req); - - // console.log("pluginName: ", pluginName, "dsl: ", dsl, "context: ", context, "dataSourceConfig: ", dataSourceConfig, "ctx: ", ctx); - const result = await pluginServices.runPluginQuery( pluginName, dsl, @@ -32,7 +47,8 @@ export async function runPluginQuery(req: Request, res: Response) { } export async function validatePluginDataSourceConfig(req: Request, res: Response) { - const { pluginName, dataSourceConfig } = req.body; + const body = await getDecryptedBody(req); + const { pluginName, dataSourceConfig } = body; const ctx = pluginServices.getPluginContext(req); const result = await pluginServices.validatePluginDataSourceConfig( pluginName, @@ -50,10 +66,11 @@ type GetDynamicDefReqBody = { export async function getDynamicDef(req: Request, res: Response) { const ctx = pluginServices.getPluginContext(req); - if (!Array.isArray(req.body)) { + const body = await getDecryptedBody(req); + if (!Array.isArray(body)) { throw badRequest("request body is not a valid array"); } - const fields = req.body as GetDynamicDefReqBody; + const fields = body as GetDynamicDefReqBody; const result: Config[] = []; for (const item of fields) { const def = await pluginServices.getDynamicConfigDef( diff --git a/server/node-service/src/server.ts b/server/node-service/src/server.ts index 124c8d1e5..793161ef5 100644 --- a/server/node-service/src/server.ts +++ b/server/node-service/src/server.ts @@ -9,6 +9,7 @@ import { collectDefaultMetrics } from "prom-client"; import apiRouter from "./routes/apiRouter"; import systemRouter from "./routes/systemRouter"; import cors, { CorsOptions } from "cors"; +import bodyParser from "body-parser"; collectDefaultMetrics(); const prefix = "/node-service"; @@ -32,6 +33,15 @@ router.use(morgan("dev")); /** Parse the request */ router.use(express.urlencoded({ extended: false })); +/** Custom middleware: use raw body for encrypted requests */ +router.use((req, res, next) => { + if (req.headers["x-encrypted"]) { + bodyParser.text({ type: "*/*" })(req, res, next); + } else { + bodyParser.json()(req, res, next); + } +}); + /** Takes care of JSON data */ router.use( express.json({ diff --git a/server/node-service/src/utils/encryption.ts b/server/node-service/src/utils/encryption.ts new file mode 100644 index 000000000..38b10474b --- /dev/null +++ b/server/node-service/src/utils/encryption.ts @@ -0,0 +1,58 @@ +import { createDecipheriv, createHash } from "crypto"; +import { badRequest } from "../common/error"; + +// Spring's Encryptors.text uses AES-256-CBC with a key derived from password and salt (hex). +// The encrypted string format is: hex(salt) + encryptedBase64 +// See: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/encrypt/Encryptors.html + +const ALGORITHM = "aes-256-cbc"; +const KEY_LENGTH = 32; // 256 bits +const IV_LENGTH = 16; // 128 bits + +// You must set these to match your Java config: +const PASSWORD = process.env.LOWCODER_NODE_SERVICE_SECRET || "lowcoderpwd"; +const SALT_HEX = process.env.LOWCODER_NODE_SERVICE_SECRET_SALT || "lowcodersalt"; + +/** + * Convert a string to its binary representation, then to a hex string. + */ +function stringToHexFromBinary(str: string): string { + // Convert string to binary (Buffer), then to hex string + return Buffer.from(str, "utf8").toString("hex"); +} + +/** + * Derive key from password and salt using SHA-256 (Spring's default). + */ +function deriveKey(password: string, saltHex: string): Buffer { + // Convert salt string to binary, then to hex string + const saltHexFromBinary = stringToHexFromBinary(saltHex); + const salt = Buffer.from(saltHexFromBinary, "hex"); + const hash = createHash("sha256"); + hash.update(password); + hash.update(salt); + return hash.digest(); +} + +/** + * Decrypt a string encrypted by Spring's Encryptors.text. + */ +export async function decryptString(encrypted: string): Promise { + try { + // Spring's format: hex(salt) + encryptedBase64 + // But if you know salt, encrypted is just Base64(IV + ciphertext) + const key = deriveKey(PASSWORD, SALT_HEX); + + // Spring's Encryptors.text prepends a random IV (16 bytes) to the ciphertext, all base64 encoded. + const encryptedBuf = Buffer.from(encrypted, "base64"); + const iv = encryptedBuf.slice(0, IV_LENGTH); + const ciphertext = encryptedBuf.slice(IV_LENGTH); + + const decipher = createDecipheriv(ALGORITHM, key, iv); + let decrypted = decipher.update(ciphertext, undefined, "utf8"); + decrypted += decipher.final("utf8"); + return decrypted; + } catch (e) { + throw badRequest("Failed to decrypt string"); + } +} \ No newline at end of file From cdd86d4a7aefb19df0aa5bb1706f5d8e81cfac33 Mon Sep 17 00:00:00 2001 From: Thomasr Date: Fri, 25 Apr 2025 04:41:01 -0400 Subject: [PATCH 2/4] 1024 iteration for deriveKey method --- server/node-service/src/utils/encryption.ts | 34 ++++++--------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/server/node-service/src/utils/encryption.ts b/server/node-service/src/utils/encryption.ts index 38b10474b..2240a6571 100644 --- a/server/node-service/src/utils/encryption.ts +++ b/server/node-service/src/utils/encryption.ts @@ -1,37 +1,23 @@ -import { createDecipheriv, createHash } from "crypto"; +import { createDecipheriv, pbkdf2Sync } from "crypto"; import { badRequest } from "../common/error"; -// Spring's Encryptors.text uses AES-256-CBC with a key derived from password and salt (hex). -// The encrypted string format is: hex(salt) + encryptedBase64 -// See: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/encrypt/Encryptors.html - +// Spring's Encryptors.text uses AES-256-CBC with PBKDF2 (HmacSHA1, 1024 iterations). const ALGORITHM = "aes-256-cbc"; const KEY_LENGTH = 32; // 256 bits const IV_LENGTH = 16; // 128 bits +const ITERATIONS = 1024; +const DIGEST = "sha1"; // You must set these to match your Java config: const PASSWORD = process.env.LOWCODER_NODE_SERVICE_SECRET || "lowcoderpwd"; const SALT_HEX = process.env.LOWCODER_NODE_SERVICE_SECRET_SALT || "lowcodersalt"; /** - * Convert a string to its binary representation, then to a hex string. - */ -function stringToHexFromBinary(str: string): string { - // Convert string to binary (Buffer), then to hex string - return Buffer.from(str, "utf8").toString("hex"); -} - -/** - * Derive key from password and salt using SHA-256 (Spring's default). + * Derive key from password and salt using PBKDF2WithHmacSHA1 (Spring's default). */ function deriveKey(password: string, saltHex: string): Buffer { - // Convert salt string to binary, then to hex string - const saltHexFromBinary = stringToHexFromBinary(saltHex); - const salt = Buffer.from(saltHexFromBinary, "hex"); - const hash = createHash("sha256"); - hash.update(password); - hash.update(salt); - return hash.digest(); + const salt = Buffer.from(saltHex, "utf8"); + return pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, DIGEST); } /** @@ -39,12 +25,10 @@ function deriveKey(password: string, saltHex: string): Buffer { */ export async function decryptString(encrypted: string): Promise { try { - // Spring's format: hex(salt) + encryptedBase64 - // But if you know salt, encrypted is just Base64(IV + ciphertext) + // Spring's format: hex(salt) + encryptedHex(IV + ciphertext) const key = deriveKey(PASSWORD, SALT_HEX); - // Spring's Encryptors.text prepends a random IV (16 bytes) to the ciphertext, all base64 encoded. - const encryptedBuf = Buffer.from(encrypted, "base64"); + const encryptedBuf = Buffer.from(encrypted, "hex"); const iv = encryptedBuf.slice(0, IV_LENGTH); const ciphertext = encryptedBuf.slice(IV_LENGTH); From 210f2926ae934941754fcdb2d4115ecb923243fa Mon Sep 17 00:00:00 2001 From: Thomasr Date: Fri, 25 Apr 2025 15:02:12 -0400 Subject: [PATCH 3/4] backward compatible encryption option --- .../encryption/EncryptionServiceImpl.java | 8 +++---- .../plugin/client/DatasourcePluginClient.java | 22 ++++++++++++++----- .../org/lowcoder/sdk/config/CommonConfig.java | 3 +++ .../src/main/resources/application-debug.yaml | 10 ++++----- .../src/main/resources/application.yaml | 10 ++++----- 5 files changed, 31 insertions(+), 22 deletions(-) diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java index d1dafd8af..72eeba412 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java @@ -20,15 +20,13 @@ public class EncryptionServiceImpl implements EncryptionService { @Autowired public EncryptionServiceImpl( - CommonConfig commonConfig, - @Value("${lowcoder.node-server.password}") String password, - @Value("${lowcoder.node-server.salt}") String salt + CommonConfig commonConfig ) { Encrypt encrypt = commonConfig.getEncrypt(); String saltInHex = Hex.encodeHexString(encrypt.getSalt().getBytes()); this.textEncryptor = Encryptors.text(encrypt.getPassword(), saltInHex); - String saltInHexForNodeServer = Hex.encodeHexString(salt.getBytes()); - this.textEncryptorForNodeServer = Encryptors.text(password, saltInHexForNodeServer); + String saltInHexForNodeServer = Hex.encodeHexString(commonConfig.getJsExecutor().getSalt().getBytes()); + this.textEncryptorForNodeServer = Encryptors.text(commonConfig.getJsExecutor().getPassword(), saltInHexForNodeServer); } @Override diff --git a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java index 812978193..5f0da78c4 100644 --- a/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java +++ b/server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java @@ -10,6 +10,7 @@ import org.lowcoder.domain.plugin.client.dto.GetPluginDynamicConfigRequestDTO; import org.lowcoder.infra.js.NodeServerClient; import org.lowcoder.infra.js.NodeServerHelper; +import org.lowcoder.sdk.config.CommonConfig; import org.lowcoder.sdk.config.CommonConfigHelper; import org.lowcoder.sdk.exception.ServerException; import org.lowcoder.sdk.models.DatasourceTestResult; @@ -48,6 +49,7 @@ public class DatasourcePluginClient implements NodeServerClient { .build(); private final CommonConfigHelper commonConfigHelper; + private final CommonConfig commonConfig; private final NodeServerHelper nodeServerHelper; private final EncryptionService encryptionService; @@ -134,13 +136,23 @@ public Mono executeQuery(String pluginName, Object queryDs "dataSourceConfig", datasourceConfig ); String json = OBJECT_MAPPER.writeValueAsString(body); - String encrypted = encryptionService.encryptStringForNodeServer(json); - return WEB_CLIENT + + boolean encryptionEnabled = commonConfig.getJsExecutor().isEncrypted(); + String payload; + WebClient.RequestBodySpec requestSpec = WEB_CLIENT .post() .uri(nodeServerHelper.createUri(RUN_PLUGIN_QUERY)) - .header(HttpHeaders.ACCEPT_LANGUAGE, language) - .header("X-Encrypted", "true") // Optional: custom header to indicate encryption - .bodyValue(encrypted) + .header(HttpHeaders.ACCEPT_LANGUAGE, language); + + if (encryptionEnabled) { + payload = encryptionService.encryptStringForNodeServer(json); + requestSpec = requestSpec.header("X-Encrypted", "true"); + } else { + payload = json; + } + + return requestSpec + .bodyValue(payload) .exchangeToMono(response -> { if (response.statusCode().is2xxSuccessful()) { return response.bodyToMono(Map.class) diff --git a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java index b50d06935..697f42fcd 100644 --- a/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java +++ b/server/api-service/lowcoder-sdk/src/main/java/org/lowcoder/sdk/config/CommonConfig.java @@ -147,6 +147,9 @@ public long getMaxAgeInSeconds() { @Data public static class JsExecutor { private String host; + private String password; + private String salt; + private boolean isEncrypted; } @Data diff --git a/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml b/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml index 4c85ab858..050e8077b 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml +++ b/server/api-service/lowcoder-server/src/main/resources/application-debug.yaml @@ -37,6 +37,9 @@ common: cookie-name: LOWCODER_DEBUG_TOKEN js-executor: host: "http://127.0.0.1:6060" + password: ${LOWCODER_NODE_SERVICE_SECRET:lowcoderpwd} + salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:lowcodersalt} + is-encrypted: ${LOWCODER_NODE_SERVICE_ENCRYPTED:false} workspace: mode: ${LOWCODER_WORKSPACE_MODE:SAAS} plugin-dirs: @@ -61,9 +64,4 @@ logging: org.lowcoder: debug default: - query-timeout: ${LOWCODER_DEFAULT_QUERY_TIMEOUT:10s} - -lowcoder: - node-server: - password: ${LOWCODER_NODE_SERVICE_SECRET:lowcoderpwd} - salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:lowcodersalt} \ No newline at end of file + query-timeout: ${LOWCODER_DEFAULT_QUERY_TIMEOUT:10s} \ No newline at end of file diff --git a/server/api-service/lowcoder-server/src/main/resources/application.yaml b/server/api-service/lowcoder-server/src/main/resources/application.yaml index 783391083..00bce2fc1 100644 --- a/server/api-service/lowcoder-server/src/main/resources/application.yaml +++ b/server/api-service/lowcoder-server/src/main/resources/application.yaml @@ -74,6 +74,9 @@ common: corsAllowedDomainString: ${LOWCODER_CORS_DOMAINS:*} js-executor: host: ${LOWCODER_NODE_SERVICE_URL:http://127.0.0.1:6060} + password: ${LOWCODER_NODE_SERVICE_SECRET:lowcoderpwd} + salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:lowcodersalt} + is-encrypted: ${LOWCODER_NODE_SERVICE_ENCRYPTED:false} max-query-request-size: ${LOWCODER_MAX_REQUEST_SIZE:20m} max-query-response-size: ${LOWCODER_MAX_REQUEST_SIZE:20m} max-upload-size: ${LOWCODER_MAX_REQUEST_SIZE:20m} @@ -129,9 +132,4 @@ management: redis: enabled: true diskspace: - enabled: false - -lowcoder: - node-server: - password: ${LOWCODER_NODE_SERVICE_SECRET:lowcoderpwd} - salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:lowcodersalt} \ No newline at end of file + enabled: false \ No newline at end of file From 998e37314f0e0684f7a4779cf896d2ea57ff60de Mon Sep 17 00:00:00 2001 From: Thomasr Date: Mon, 28 Apr 2025 02:25:53 -0400 Subject: [PATCH 4/4] testcase for payload encryption --- .../encryption/EncryptionServiceImplTest.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 server/api-service/lowcoder-domain/src/test/java/org/lowcoder/domain/encryption/EncryptionServiceImplTest.java diff --git a/server/api-service/lowcoder-domain/src/test/java/org/lowcoder/domain/encryption/EncryptionServiceImplTest.java b/server/api-service/lowcoder-domain/src/test/java/org/lowcoder/domain/encryption/EncryptionServiceImplTest.java new file mode 100644 index 000000000..41bd465c8 --- /dev/null +++ b/server/api-service/lowcoder-domain/src/test/java/org/lowcoder/domain/encryption/EncryptionServiceImplTest.java @@ -0,0 +1,80 @@ +package org.lowcoder.domain.encryption; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lowcoder.sdk.config.CommonConfig; +import org.lowcoder.sdk.config.CommonConfig.Encrypt; +import org.lowcoder.sdk.config.CommonConfig.JsExecutor; +import org.springframework.security.crypto.encrypt.Encryptors; +import org.springframework.security.crypto.encrypt.TextEncryptor; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class EncryptionServiceImplTest { + + private EncryptionServiceImpl encryptionService; + private TextEncryptor nodeServerEncryptor; + private String nodePassword = "nodePassword"; + private String nodeSalt = "nodeSalt"; + + @BeforeEach + void setUp() { + // Mock CommonConfig and its nested classes + Encrypt encrypt = mock(Encrypt.class); + when(encrypt.getPassword()).thenReturn("testPassword"); + when(encrypt.getSalt()).thenReturn("testSalt"); + + JsExecutor jsExecutor = mock(JsExecutor.class); + when(jsExecutor.getPassword()).thenReturn(nodePassword); + when(jsExecutor.getSalt()).thenReturn(nodeSalt); + + CommonConfig commonConfig = mock(CommonConfig.class); + when(commonConfig.getEncrypt()).thenReturn(encrypt); + when(commonConfig.getJsExecutor()).thenReturn(jsExecutor); + + encryptionService = new EncryptionServiceImpl(commonConfig); + + // For direct comparison in test + String saltInHexForNodeServer = org.apache.commons.codec.binary.Hex.encodeHexString(nodeSalt.getBytes()); + nodeServerEncryptor = Encryptors.text(nodePassword, saltInHexForNodeServer); + } + + @Test + void testEncryptStringForNodeServer_NullInput() { + assertNull(encryptionService.encryptStringForNodeServer(null)); + } + + @Test + void testEncryptStringForNodeServer_EmptyInput() { + assertEquals("", encryptionService.encryptStringForNodeServer("")); + } + + @Test + void testEncryptStringForNodeServer_EncryptsAndDecryptsCorrectly() { + String plain = "node secret"; + String encrypted = encryptionService.encryptStringForNodeServer(plain); + assertNotNull(encrypted); + assertNotEquals(plain, encrypted); + + // Decrypt using the same encryptor to verify correctness + String decrypted = nodeServerEncryptor.decrypt(encrypted); + assertEquals(plain, decrypted); + } + + @Test + void testEncryptStringForNodeServer_DifferentInputsProduceDifferentOutputs() { + String encrypted1 = encryptionService.encryptStringForNodeServer("abc"); + String encrypted2 = encryptionService.encryptStringForNodeServer("def"); + assertNotEquals(encrypted1, encrypted2); + } + + @Test + void testEncryptStringForNodeServer_SameInputProducesDifferentOutputs() { + String input = "repeat"; + String encrypted1 = encryptionService.encryptStringForNodeServer(input); + String encrypted2 = encryptionService.encryptStringForNodeServer(input); + // Spring's Encryptors.text uses random IV, so outputs should differ + assertNotEquals(encrypted1, encrypted2); + } +} \ No newline at end of file