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 276f4059d4..fdd173ed77 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 6524682b60..72eeba4121 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,18 @@ 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 + ) { Encrypt encrypt = commonConfig.getEncrypt(); String saltInHex = Hex.encodeHexString(encrypt.getSalt().getBytes()); this.textEncryptor = Encryptors.text(encrypt.getPassword(), saltInHex); + String saltInHexForNodeServer = Hex.encodeHexString(commonConfig.getJsExecutor().getSalt().getBytes()); + this.textEncryptorForNodeServer = Encryptors.text(commonConfig.getJsExecutor().getPassword(), saltInHexForNodeServer); } @Override @@ -30,6 +36,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 f2aa878bb3..cae767e6f5 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,10 +5,12 @@ 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; 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; @@ -30,6 +32,8 @@ import static org.lowcoder.sdk.constants.GlobalContext.REQUEST; +import com.fasterxml.jackson.databind.ObjectMapper; + @Slf4j @RequiredArgsConstructor @Component @@ -45,13 +49,17 @@ public class DatasourcePluginClient implements NodeServerClient { .build(); private final CommonConfigHelper commonConfigHelper; + private final CommonConfig commonConfig; 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 +127,47 @@ 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); + + boolean encryptionEnabled = !(commonConfig.getJsExecutor().getPassword().isEmpty() || commonConfig.getJsExecutor().getSalt().isEmpty()); + String payload; + WebClient.RequestBodySpec requestSpec = WEB_CLIENT + .post() + .uri(nodeServerHelper.createUri(RUN_PLUGIN_QUERY)) + .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) + .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-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 0000000000..41bd465c82 --- /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 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 b50d069354..697f42fcd6 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 d52888ca7e..80b8ddfff4 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,8 @@ common: cookie-name: LOWCODER_DEBUG_TOKEN js-executor: host: "http://127.0.0.1:6060" + password: ${LOWCODER_NODE_SERVICE_SECRET:} + salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:} workspace: mode: ${LOWCODER_WORKSPACE_MODE:SAAS} plugin-dirs: 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 e5058563cb..fc89e0e746 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,8 @@ common: corsAllowedDomainString: ${LOWCODER_CORS_DOMAINS:*} js-executor: host: ${LOWCODER_NODE_SERVICE_URL:http://127.0.0.1:6060} + password: ${LOWCODER_NODE_SERVICE_SECRET:} + salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:} 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,4 +131,4 @@ management: redis: enabled: true diskspace: - enabled: false + enabled: false \ 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 e20a109b38..d763586ce2 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 124c8d1e5c..793161ef5a 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 0000000000..2240a6571f --- /dev/null +++ b/server/node-service/src/utils/encryption.ts @@ -0,0 +1,42 @@ +import { createDecipheriv, pbkdf2Sync } from "crypto"; +import { badRequest } from "../common/error"; + +// 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"; + +/** + * Derive key from password and salt using PBKDF2WithHmacSHA1 (Spring's default). + */ +function deriveKey(password: string, saltHex: string): Buffer { + const salt = Buffer.from(saltHex, "utf8"); + return pbkdf2Sync(password, salt, ITERATIONS, KEY_LENGTH, DIGEST); +} + +/** + * Decrypt a string encrypted by Spring's Encryptors.text. + */ +export async function decryptString(encrypted: string): Promise { + try { + // Spring's format: hex(salt) + encryptedHex(IV + ciphertext) + const key = deriveKey(PASSWORD, SALT_HEX); + + const encryptedBuf = Buffer.from(encrypted, "hex"); + 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy