Skip to content

Commit 2d390f5

Browse files
committed
Encrypt payload to the node-server and add header "X-ENCRYPTED"
1 parent d82fa15 commit 2d390f5

File tree

8 files changed

+158
-24
lines changed

8 files changed

+158
-24
lines changed

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionService.java

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ public interface EncryptionService {
44

55
String encryptString(String plaintext);
66

7+
String encryptStringForNodeServer(String plaintext);
8+
79
String decryptString(String encryptedText);
810

911
String encryptPassword(String plaintext);

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/encryption/EncryptionServiceImpl.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.lowcoder.sdk.config.CommonConfig;
66
import org.lowcoder.sdk.config.CommonConfig.Encrypt;
77
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.beans.factory.annotation.Value;
89
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
910
import org.springframework.security.crypto.encrypt.Encryptors;
1011
import org.springframework.security.crypto.encrypt.TextEncryptor;
@@ -14,13 +15,20 @@
1415
public class EncryptionServiceImpl implements EncryptionService {
1516

1617
private final TextEncryptor textEncryptor;
18+
private final TextEncryptor textEncryptorForNodeServer;
1719
private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
1820

1921
@Autowired
20-
public EncryptionServiceImpl(CommonConfig commonConfig) {
22+
public EncryptionServiceImpl(
23+
CommonConfig commonConfig,
24+
@Value("${lowcoder.node-server.password}") String password,
25+
@Value("${lowcoder.node-server.salt}") String salt
26+
) {
2127
Encrypt encrypt = commonConfig.getEncrypt();
2228
String saltInHex = Hex.encodeHexString(encrypt.getSalt().getBytes());
2329
this.textEncryptor = Encryptors.text(encrypt.getPassword(), saltInHex);
30+
String saltInHexForNodeServer = Hex.encodeHexString(salt.getBytes());
31+
this.textEncryptorForNodeServer = Encryptors.text(password, saltInHexForNodeServer);
2432
}
2533

2634
@Override
@@ -30,6 +38,13 @@ public String encryptString(String plaintext) {
3038
}
3139
return textEncryptor.encrypt(plaintext);
3240
}
41+
@Override
42+
public String encryptStringForNodeServer(String plaintext) {
43+
if (StringUtils.isEmpty(plaintext)) {
44+
return plaintext;
45+
}
46+
return textEncryptorForNodeServer.encrypt(plaintext);
47+
}
3348

3449
@Override
3550
public String decryptString(String encryptedText) {

server/api-service/lowcoder-domain/src/main/java/org/lowcoder/domain/plugin/client/DatasourcePluginClient.java

+37-15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.apache.commons.collections4.CollectionUtils;
66
import org.apache.commons.collections4.MapUtils;
77
import org.apache.commons.lang3.StringUtils;
8+
import org.lowcoder.domain.encryption.EncryptionService;
89
import org.lowcoder.domain.plugin.client.dto.DatasourcePluginDefinition;
910
import org.lowcoder.domain.plugin.client.dto.GetPluginDynamicConfigRequestDTO;
1011
import org.lowcoder.infra.js.NodeServerClient;
@@ -30,6 +31,8 @@
3031

3132
import static org.lowcoder.sdk.constants.GlobalContext.REQUEST;
3233

34+
import com.fasterxml.jackson.databind.ObjectMapper;
35+
3336
@Slf4j
3437
@RequiredArgsConstructor
3538
@Component
@@ -46,12 +49,15 @@ public class DatasourcePluginClient implements NodeServerClient {
4649

4750
private final CommonConfigHelper commonConfigHelper;
4851
private final NodeServerHelper nodeServerHelper;
52+
private final EncryptionService encryptionService;
4953

5054
private static final String PLUGINS_PATH = "plugins";
5155
private static final String RUN_PLUGIN_QUERY = "runPluginQuery";
5256
private static final String VALIDATE_PLUGIN_DATA_SOURCE_CONFIG = "validatePluginDataSourceConfig";
5357
private static final String GET_PLUGIN_DYNAMIC_CONFIG = "getPluginDynamicConfig";
5458

59+
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
60+
5561
public Mono<List<Object>> getPluginDynamicConfigSafely(List<GetPluginDynamicConfigRequestDTO> getPluginDynamicConfigRequestDTOS) {
5662
return getPluginDynamicConfig(getPluginDynamicConfigRequestDTOS)
5763
.onErrorResume(throwable -> {
@@ -119,21 +125,37 @@ public Flux<DatasourcePluginDefinition> getDatasourcePluginDefinitions() {
119125
@SuppressWarnings("unchecked")
120126
public Mono<QueryExecutionResult> executeQuery(String pluginName, Object queryDsl, List<Map<String, Object>> context, Object datasourceConfig) {
121127
return getAcceptLanguage()
122-
.flatMap(language -> WEB_CLIENT
123-
.post()
124-
.uri(nodeServerHelper.createUri(RUN_PLUGIN_QUERY))
125-
.header(HttpHeaders.ACCEPT_LANGUAGE, language)
126-
.bodyValue(Map.of("pluginName", pluginName, "dsl", queryDsl, "context", context, "dataSourceConfig", datasourceConfig))
127-
.exchangeToMono(response -> {
128-
if (response.statusCode().is2xxSuccessful()) {
129-
return response.bodyToMono(Map.class)
130-
.map(map -> map.get("result"))
131-
.map(QueryExecutionResult::success);
132-
}
133-
return response.bodyToMono(Map.class)
134-
.map(map -> MapUtils.getString(map, "message"))
135-
.map(QueryExecutionResult::errorWithMessage);
136-
}));
128+
.flatMap(language -> {
129+
try {
130+
Map<String, Object> body = Map.of(
131+
"pluginName", pluginName,
132+
"dsl", queryDsl,
133+
"context", context,
134+
"dataSourceConfig", datasourceConfig
135+
);
136+
String json = OBJECT_MAPPER.writeValueAsString(body);
137+
String encrypted = encryptionService.encryptStringForNodeServer(json);
138+
return WEB_CLIENT
139+
.post()
140+
.uri(nodeServerHelper.createUri(RUN_PLUGIN_QUERY))
141+
.header(HttpHeaders.ACCEPT_LANGUAGE, language)
142+
.header("X-Encrypted", "true") // Optional: custom header to indicate encryption
143+
.bodyValue(encrypted)
144+
.exchangeToMono(response -> {
145+
if (response.statusCode().is2xxSuccessful()) {
146+
return response.bodyToMono(Map.class)
147+
.map(map -> map.get("result"))
148+
.map(QueryExecutionResult::success);
149+
}
150+
return response.bodyToMono(Map.class)
151+
.map(map -> MapUtils.getString(map, "message"))
152+
.map(QueryExecutionResult::errorWithMessage);
153+
});
154+
} catch (Exception e) {
155+
log.error("Encryption error", e);
156+
return Mono.error(new ServerException("Encryption error"));
157+
}
158+
});
137159
}
138160

139161
@SuppressWarnings("unchecked")

server/api-service/lowcoder-server/src/main/resources/application-debug.yaml

+6-1
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,9 @@ logging:
6161
org.lowcoder: debug
6262

6363
default:
64-
query-timeout: ${LOWCODER_DEFAULT_QUERY_TIMEOUT:10s}
64+
query-timeout: ${LOWCODER_DEFAULT_QUERY_TIMEOUT:10s}
65+
66+
lowcoder:
67+
node-server:
68+
password: ${LOWCODER_NODE_SERVICE_SECRET:lowcoderpwd}
69+
salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:lowcodersalt}

server/api-service/lowcoder-server/src/main/resources/application.yaml

+5
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,8 @@ management:
130130
enabled: true
131131
diskspace:
132132
enabled: false
133+
134+
lowcoder:
135+
node-server:
136+
password: ${LOWCODER_NODE_SERVICE_SECRET:lowcoderpwd}
137+
salt: ${LOWCODER_NODE_SERVICE_SECRET_SALT:lowcodersalt}

server/node-service/src/controllers/plugins.ts

+24-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@ import { Request, Response } from "express";
33
import _ from "lodash";
44
import { Config } from "lowcoder-sdk/dataSource";
55
import * as pluginServices from "../services/plugin";
6+
// Add import for decryption utility
7+
import { decryptString } from "../utils/encryption"; // <-- implement this utility as needed
8+
9+
async function getDecryptedBody(req: Request): Promise<any> {
10+
if (req.headers["x-encrypted"]) {
11+
// Assume body is a raw encrypted string, decrypt and parse as JSON
12+
const encrypted = typeof req.body === "string" ? req.body : req.body?.toString?.();
13+
if (!encrypted) throw badRequest("Missing encrypted body");
14+
const decrypted = await decryptString(encrypted);
15+
try {
16+
return JSON.parse(decrypted);
17+
} catch (e) {
18+
throw badRequest("Failed to parse decrypted body as JSON");
19+
}
20+
}
21+
return req.body;
22+
}
623

724
export async function listPlugins(req: Request, res: Response) {
825
let ids = req.query["id"] || [];
@@ -15,12 +32,10 @@ export async function listPlugins(req: Request, res: Response) {
1532
}
1633

1734
export async function runPluginQuery(req: Request, res: Response) {
18-
const { pluginName, dsl, context, dataSourceConfig } = req.body;
35+
const body = await getDecryptedBody(req);
36+
const { pluginName, dsl, context, dataSourceConfig } = body;
1937
const ctx = pluginServices.getPluginContext(req);
2038

21-
22-
// console.log("pluginName: ", pluginName, "dsl: ", dsl, "context: ", context, "dataSourceConfig: ", dataSourceConfig, "ctx: ", ctx);
23-
2439
const result = await pluginServices.runPluginQuery(
2540
pluginName,
2641
dsl,
@@ -32,7 +47,8 @@ export async function runPluginQuery(req: Request, res: Response) {
3247
}
3348

3449
export async function validatePluginDataSourceConfig(req: Request, res: Response) {
35-
const { pluginName, dataSourceConfig } = req.body;
50+
const body = await getDecryptedBody(req);
51+
const { pluginName, dataSourceConfig } = body;
3652
const ctx = pluginServices.getPluginContext(req);
3753
const result = await pluginServices.validatePluginDataSourceConfig(
3854
pluginName,
@@ -50,10 +66,11 @@ type GetDynamicDefReqBody = {
5066

5167
export async function getDynamicDef(req: Request, res: Response) {
5268
const ctx = pluginServices.getPluginContext(req);
53-
if (!Array.isArray(req.body)) {
69+
const body = await getDecryptedBody(req);
70+
if (!Array.isArray(body)) {
5471
throw badRequest("request body is not a valid array");
5572
}
56-
const fields = req.body as GetDynamicDefReqBody;
73+
const fields = body as GetDynamicDefReqBody;
5774
const result: Config[] = [];
5875
for (const item of fields) {
5976
const def = await pluginServices.getDynamicConfigDef(

server/node-service/src/server.ts

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { collectDefaultMetrics } from "prom-client";
99
import apiRouter from "./routes/apiRouter";
1010
import systemRouter from "./routes/systemRouter";
1111
import cors, { CorsOptions } from "cors";
12+
import bodyParser from "body-parser";
1213
collectDefaultMetrics();
1314

1415
const prefix = "/node-service";
@@ -32,6 +33,15 @@ router.use(morgan("dev"));
3233
/** Parse the request */
3334
router.use(express.urlencoded({ extended: false }));
3435

36+
/** Custom middleware: use raw body for encrypted requests */
37+
router.use((req, res, next) => {
38+
if (req.headers["x-encrypted"]) {
39+
bodyParser.text({ type: "*/*" })(req, res, next);
40+
} else {
41+
bodyParser.json()(req, res, next);
42+
}
43+
});
44+
3545
/** Takes care of JSON data */
3646
router.use(
3747
express.json({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createDecipheriv, createHash } from "crypto";
2+
import { badRequest } from "../common/error";
3+
4+
// Spring's Encryptors.text uses AES-256-CBC with a key derived from password and salt (hex).
5+
// The encrypted string format is: hex(salt) + encryptedBase64
6+
// See: https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/encrypt/Encryptors.html
7+
8+
const ALGORITHM = "aes-256-cbc";
9+
const KEY_LENGTH = 32; // 256 bits
10+
const IV_LENGTH = 16; // 128 bits
11+
12+
// You must set these to match your Java config:
13+
const PASSWORD = process.env.LOWCODER_NODE_SERVICE_SECRET || "lowcoderpwd";
14+
const SALT_HEX = process.env.LOWCODER_NODE_SERVICE_SECRET_SALT || "lowcodersalt";
15+
16+
/**
17+
* Convert a string to its binary representation, then to a hex string.
18+
*/
19+
function stringToHexFromBinary(str: string): string {
20+
// Convert string to binary (Buffer), then to hex string
21+
return Buffer.from(str, "utf8").toString("hex");
22+
}
23+
24+
/**
25+
* Derive key from password and salt using SHA-256 (Spring's default).
26+
*/
27+
function deriveKey(password: string, saltHex: string): Buffer {
28+
// Convert salt string to binary, then to hex string
29+
const saltHexFromBinary = stringToHexFromBinary(saltHex);
30+
const salt = Buffer.from(saltHexFromBinary, "hex");
31+
const hash = createHash("sha256");
32+
hash.update(password);
33+
hash.update(salt);
34+
return hash.digest();
35+
}
36+
37+
/**
38+
* Decrypt a string encrypted by Spring's Encryptors.text.
39+
*/
40+
export async function decryptString(encrypted: string): Promise<string> {
41+
try {
42+
// Spring's format: hex(salt) + encryptedBase64
43+
// But if you know salt, encrypted is just Base64(IV + ciphertext)
44+
const key = deriveKey(PASSWORD, SALT_HEX);
45+
46+
// Spring's Encryptors.text prepends a random IV (16 bytes) to the ciphertext, all base64 encoded.
47+
const encryptedBuf = Buffer.from(encrypted, "base64");
48+
const iv = encryptedBuf.slice(0, IV_LENGTH);
49+
const ciphertext = encryptedBuf.slice(IV_LENGTH);
50+
51+
const decipher = createDecipheriv(ALGORITHM, key, iv);
52+
let decrypted = decipher.update(ciphertext, undefined, "utf8");
53+
decrypted += decipher.final("utf8");
54+
return decrypted;
55+
} catch (e) {
56+
throw badRequest("Failed to decrypt string");
57+
}
58+
}

0 commit comments

Comments
 (0)