create-entity-server 0.0.15 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create.js +15 -7
- package/package.json +1 -1
- package/template/.env.example +8 -7
- package/template/configs/database.json +173 -10
- package/template/entities/Account/account_audit.json +4 -5
- package/template/entities/System/system_audit_log.json +14 -8
- package/template/samples/README.md +28 -22
- package/template/samples/browser/entity-server-client.js +453 -0
- package/template/samples/browser/example.html +498 -0
- package/template/samples/entities/02_types_and_defaults.json +15 -16
- package/template/samples/entities/04_fk_and_composite_unique.json +0 -2
- package/template/samples/entities/05_cache.json +9 -8
- package/template/samples/entities/06_history_and_hard_delete.json +27 -9
- package/template/samples/entities/07_license_scope.json +40 -31
- package/template/samples/entities/09_hook_entity.json +0 -6
- package/template/samples/entities/10_hook_submit_delete.json +5 -2
- package/template/samples/entities/11_hook_webhook.json +9 -7
- package/template/samples/entities/12_hook_push.json +3 -3
- package/template/samples/entities/13_read_only.json +13 -10
- package/template/samples/entities/15_reset_defaults.json +0 -1
- package/template/samples/entities/16_isolated_license.json +62 -0
- package/template/samples/entities/README.md +36 -39
- package/template/samples/flutter/lib/entity_server_client.dart +170 -48
- package/template/samples/java/EntityServerClient.java +208 -61
- package/template/samples/java/EntityServerExample.java +4 -3
- package/template/samples/kotlin/EntityServerClient.kt +175 -45
- package/template/samples/node/src/EntityServerClient.js +232 -59
- package/template/samples/node/src/example.js +9 -9
- package/template/samples/php/ci4/Config/EntityServer.php +0 -1
- package/template/samples/php/ci4/Libraries/EntityServer.php +206 -53
- package/template/samples/php/laravel/Services/EntityServerService.php +190 -41
- package/template/samples/python/entity_server.py +181 -68
- package/template/samples/python/example.py +7 -6
- package/template/samples/react/src/example.tsx +41 -25
- package/template/samples/swift/EntityServerClient.swift +143 -37
- package/template/scripts/run.ps1 +12 -3
- package/template/scripts/run.sh +12 -8
- package/template/scripts/update-server.ps1 +68 -2
- package/template/scripts/update-server.sh +59 -2
- package/template/samples/entities/order_notification.json +0 -51
- package/template/samples/react/src/api/entityServerClient.ts +0 -413
- package/template/samples/react/src/hooks/useEntity.ts +0 -173
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
package com.example.entityserver;
|
|
2
2
|
|
|
3
|
+
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
|
|
3
4
|
import org.bouncycastle.crypto.modes.ChaCha20Poly1305;
|
|
4
5
|
import org.bouncycastle.crypto.params.AEADParameters;
|
|
6
|
+
import org.bouncycastle.crypto.params.HKDFParameters;
|
|
5
7
|
import org.bouncycastle.crypto.params.KeyParameter;
|
|
8
|
+
import org.bouncycastle.crypto.digests.SHA256Digest;
|
|
6
9
|
|
|
7
10
|
import javax.crypto.Mac;
|
|
8
11
|
import javax.crypto.spec.SecretKeySpec;
|
|
@@ -14,6 +17,7 @@ import java.net.HttpURLConnection;
|
|
|
14
17
|
import java.net.URL;
|
|
15
18
|
import java.nio.charset.StandardCharsets;
|
|
16
19
|
import java.security.MessageDigest;
|
|
20
|
+
import java.security.SecureRandom;
|
|
17
21
|
import java.util.Arrays;
|
|
18
22
|
import java.util.Collections;
|
|
19
23
|
import java.util.HashMap;
|
|
@@ -29,9 +33,9 @@ import java.util.UUID;
|
|
|
29
33
|
*
|
|
30
34
|
* 환경변수 또는 생성자로 설정:
|
|
31
35
|
* ENTITY_SERVER_URL http://localhost:47200
|
|
32
|
-
* ENTITY_SERVER_API_KEY your-api-key
|
|
33
|
-
* ENTITY_SERVER_HMAC_SECRET your-hmac-secret
|
|
34
|
-
*
|
|
36
|
+
* ENTITY_SERVER_API_KEY your-api-key (HMAC 모드)
|
|
37
|
+
* ENTITY_SERVER_HMAC_SECRET your-hmac-secret (HMAC 모드)
|
|
38
|
+
* ENTITY_SERVER_TOKEN your-jwt-token (JWT 모드)
|
|
35
39
|
*
|
|
36
40
|
* 사용 예:
|
|
37
41
|
* EntityServerClient es = new EntityServerClient();
|
|
@@ -55,12 +59,15 @@ import java.util.UUID;
|
|
|
55
59
|
*/
|
|
56
60
|
public class EntityServerClient {
|
|
57
61
|
|
|
58
|
-
private final String
|
|
59
|
-
private final String
|
|
60
|
-
private final String
|
|
61
|
-
private final int
|
|
62
|
-
|
|
63
|
-
private
|
|
62
|
+
private final String baseUrl;
|
|
63
|
+
private final String apiKey;
|
|
64
|
+
private final String hmacSecret;
|
|
65
|
+
private final int timeoutMs;
|
|
66
|
+
/** true 이면 POST 요청 바디를 XChaCha20-Poly1305로 암호화합니다. */
|
|
67
|
+
private boolean encryptRequests;
|
|
68
|
+
private boolean packetEncryption = false;
|
|
69
|
+
private String token = "";
|
|
70
|
+
private String activeTxId = null;
|
|
64
71
|
|
|
65
72
|
public EntityServerClient() {
|
|
66
73
|
this(
|
|
@@ -68,43 +75,100 @@ public class EntityServerClient {
|
|
|
68
75
|
getEnv("ENTITY_SERVER_API_KEY", ""),
|
|
69
76
|
getEnv("ENTITY_SERVER_HMAC_SECRET", ""),
|
|
70
77
|
10_000,
|
|
71
|
-
|
|
78
|
+
false
|
|
72
79
|
);
|
|
80
|
+
this.token = getEnv("ENTITY_SERVER_TOKEN", "");
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this
|
|
80
|
-
this.magicLen = magicLen;
|
|
83
|
+
/** JWT Bearer 토큰을 설정합니다. HMAC 모드와 배타적으로 사용합니다. */
|
|
84
|
+
public void setToken(String token) { this.token = token; }
|
|
85
|
+
|
|
86
|
+
public EntityServerClient(String baseUrl, String apiKey, String hmacSecret, int timeoutMs) {
|
|
87
|
+
this(baseUrl, apiKey, hmacSecret, timeoutMs, false);
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
public EntityServerClient(String baseUrl, String apiKey, String hmacSecret, int timeoutMs, boolean encryptRequests) {
|
|
91
|
+
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
|
92
|
+
this.apiKey = apiKey;
|
|
93
|
+
this.hmacSecret = hmacSecret;
|
|
94
|
+
this.timeoutMs = timeoutMs;
|
|
95
|
+
this.encryptRequests = encryptRequests;
|
|
96
|
+
}
|
|
84
97
|
|
|
98
|
+
// ─── CRUD ────────────────────────────────────────────────────────────────
|
|
99
|
+
/**
|
|
100
|
+
* 서버 헬스 체크를 수행하고 패킷 암호화 활성 여부를 자동으로 감지합니다.
|
|
101
|
+
* 서버가 packet_encryption: true 를 응답하면 이후 모든 요청에 암호화가 자동 적용됩니다.
|
|
102
|
+
* @return JSON 문자열 (예: {"ok":true} 또는 {"ok":true,"packet_encryption":true})
|
|
103
|
+
*/
|
|
104
|
+
public String checkHealth() throws IOException {
|
|
105
|
+
String json = request("GET", "/v1/health", null);
|
|
106
|
+
if (json != null && json.contains("\"packet_encryption\":true")) {
|
|
107
|
+
packetEncryption = true;
|
|
108
|
+
}
|
|
109
|
+
return json;
|
|
110
|
+
}
|
|
85
111
|
/** 단건 조회 */
|
|
86
112
|
public String get(String entity, long seq) throws IOException {
|
|
87
|
-
return
|
|
113
|
+
return get(entity, seq, false);
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
/**
|
|
91
|
-
public String
|
|
116
|
+
/** 단건 조회 (skipHooks 지원) */
|
|
117
|
+
public String get(String entity, long seq, boolean skipHooks) throws IOException {
|
|
118
|
+
String q = skipHooks ? "?skipHooks=true" : "";
|
|
119
|
+
return request("GET", "/v1/entity/" + entity + "/" + seq + q, null);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 조건으로 단건 조회 — conditions 에 맞는 행 1건 반환, 없으면 404 */
|
|
123
|
+
public String find(String entity, String conditionsJson) throws IOException {
|
|
124
|
+
return find(entity, conditionsJson, false);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 조건으로 단건 조회 (skipHooks 지원) */
|
|
128
|
+
public String find(String entity, String conditionsJson, boolean skipHooks) throws IOException {
|
|
129
|
+
String q = skipHooks ? "?skipHooks=true" : "";
|
|
130
|
+
return request("POST", "/v1/entity/" + entity + "/find" + q,
|
|
131
|
+
conditionsJson != null ? conditionsJson : "{}");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 목록 조회 (POST + conditions body)
|
|
136
|
+
* @param orderBy 정렬 기준 필드명. null 이면 기본 정렬. - 접두사로 내림차순
|
|
137
|
+
* @param conditionsJson 필터 조건 JSON 객체. index/hash/unique 필드만 사용 가능. null 이면 전체
|
|
138
|
+
*/
|
|
139
|
+
public String list(String entity, int page, int limit, String orderBy, String conditionsJson) throws IOException {
|
|
92
140
|
String query = "?page=" + page + "&limit=" + limit + (orderBy != null ? "&order_by=" + orderBy : "");
|
|
93
|
-
return request("
|
|
141
|
+
return request("POST", "/v1/entity/" + entity + "/list" + query, conditionsJson != null ? conditionsJson : "{}");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 목록 조회 (조건 없음) */
|
|
145
|
+
public String list(String entity, int page, int limit, String orderBy) throws IOException {
|
|
146
|
+
return list(entity, page, limit, orderBy, null);
|
|
94
147
|
}
|
|
95
148
|
|
|
96
149
|
/** 건수 조회 */
|
|
97
150
|
public String count(String entity) throws IOException {
|
|
98
|
-
return
|
|
151
|
+
return count(entity, null);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 건수 조회 (conditions 지원) */
|
|
155
|
+
public String count(String entity, String conditionsJson) throws IOException {
|
|
156
|
+
return request("POST", "/v1/entity/" + entity + "/count", conditionsJson != null ? conditionsJson : "{}");
|
|
99
157
|
}
|
|
100
158
|
|
|
101
159
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
160
|
+
* 커스텀 SQL 조회 (SELECT 전용, 인덱스 테이블만, JOIN 지원)
|
|
161
|
+
*
|
|
162
|
+
* @param sql SELECT SQL문. 사용자 입력은 반드시 ? 로 바인딩 (SQL Injection 방지)
|
|
163
|
+
* @param paramsJson 바인딩 파라미터 JSON 배열. 예: "[\"pending\"]" (null 이면 빈 배열)
|
|
164
|
+
* @param limit 최대 반환 건수 (최대 1000. 0 이하이면 서버 기본값 적용)
|
|
104
165
|
*/
|
|
105
|
-
public String query(String entity, String
|
|
106
|
-
|
|
107
|
-
|
|
166
|
+
public String query(String entity, String sql, String paramsJson, int limit) throws IOException {
|
|
167
|
+
StringBuilder body = new StringBuilder("{\"sql\":").append(jsonString(sql));
|
|
168
|
+
body.append(",\"params\":").append(paramsJson != null ? paramsJson : "[]");
|
|
169
|
+
if (limit > 0) body.append(",\"limit\":").append(limit);
|
|
170
|
+
body.append("}");
|
|
171
|
+
return request("POST", "/v1/entity/" + entity + "/query", body.toString());
|
|
108
172
|
}
|
|
109
173
|
|
|
110
174
|
/**
|
|
@@ -159,29 +223,42 @@ public class EntityServerClient {
|
|
|
159
223
|
* @param dataJson JSON 객체 문자열. seq 포함 시 수정, 없으면 생성.
|
|
160
224
|
*/
|
|
161
225
|
public String submit(String entity, String dataJson) throws IOException {
|
|
162
|
-
return submit(entity, dataJson, null);
|
|
226
|
+
return submit(entity, dataJson, null, false);
|
|
163
227
|
}
|
|
164
228
|
|
|
165
229
|
/** 생성 또는 수정 (트랜잭션 지원) */
|
|
166
230
|
public String submit(String entity, String dataJson, String transactionId) throws IOException {
|
|
231
|
+
return submit(entity, dataJson, transactionId, false);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** 생성 또는 수정 (skipHooks 지원) */
|
|
235
|
+
public String submit(String entity, String dataJson, String transactionId, boolean skipHooks) throws IOException {
|
|
167
236
|
Map<String, String> extra = new HashMap<>();
|
|
168
237
|
String txId = transactionId != null ? transactionId : activeTxId;
|
|
169
238
|
if (txId != null) extra.put("X-Transaction-ID", txId);
|
|
170
|
-
|
|
239
|
+
String q = skipHooks ? "?skipHooks=true" : "";
|
|
240
|
+
return request("POST", "/v1/entity/" + entity + "/submit" + q, dataJson, extra);
|
|
171
241
|
}
|
|
172
242
|
|
|
173
243
|
/** 삭제 */
|
|
174
244
|
public String delete(String entity, long seq) throws IOException {
|
|
175
|
-
return delete(entity, seq, null, false);
|
|
245
|
+
return delete(entity, seq, null, false, false);
|
|
176
246
|
}
|
|
177
247
|
|
|
178
|
-
/** 삭제 (
|
|
248
|
+
/** 삭제 (트랜잭션/하드 삭제 지원) */
|
|
179
249
|
public String delete(String entity, long seq, String transactionId, boolean hard) throws IOException {
|
|
180
|
-
|
|
250
|
+
return delete(entity, seq, transactionId, hard, false);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 삭제 (skipHooks 지원). 서버는 POST /delete/:seq 로만 처리합니다. */
|
|
254
|
+
public String delete(String entity, long seq, String transactionId, boolean hard, boolean skipHooks) throws IOException {
|
|
255
|
+
StringBuilder qb = new StringBuilder();
|
|
256
|
+
if (hard) { qb.append(qb.length() == 0 ? "?" : "&").append("hard=true"); }
|
|
257
|
+
if (skipHooks) { qb.append(qb.length() == 0 ? "?" : "&").append("skipHooks=true"); }
|
|
181
258
|
Map<String, String> extra = new HashMap<>();
|
|
182
259
|
String txId = transactionId != null ? transactionId : activeTxId;
|
|
183
260
|
if (txId != null) extra.put("X-Transaction-ID", txId);
|
|
184
|
-
return request("
|
|
261
|
+
return request("POST", "/v1/entity/" + entity + "/delete/" + seq + qb.toString(), null, extra);
|
|
185
262
|
}
|
|
186
263
|
|
|
187
264
|
/** 변경 이력 조회 */
|
|
@@ -306,40 +383,59 @@ public class EntityServerClient {
|
|
|
306
383
|
}
|
|
307
384
|
|
|
308
385
|
private String request(String method, String path, String body, Map<String, String> extraHeaders) throws IOException {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
String
|
|
312
|
-
|
|
386
|
+
// 요청 바디 결정: encryptRequests 시 POST 바디를 암호화
|
|
387
|
+
byte[] bodyBytes;
|
|
388
|
+
String contentType;
|
|
389
|
+
if ((encryptRequests || packetEncryption) && body != null && !body.isEmpty()) {
|
|
390
|
+
try {
|
|
391
|
+
bodyBytes = encryptPacket(body.getBytes(StandardCharsets.UTF_8));
|
|
392
|
+
} catch (Exception e) {
|
|
393
|
+
throw new IOException("Packet encryption failed: " + e.getMessage(), e);
|
|
394
|
+
}
|
|
395
|
+
contentType = "application/octet-stream";
|
|
396
|
+
} else {
|
|
397
|
+
bodyBytes = body != null ? body.getBytes(StandardCharsets.UTF_8) : new byte[0];
|
|
398
|
+
contentType = "application/json";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
boolean isHmacMode = !apiKey.isEmpty() && !hmacSecret.isEmpty();
|
|
313
402
|
|
|
314
403
|
URL url = new URL(baseUrl + path);
|
|
315
404
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
316
405
|
conn.setRequestMethod(method);
|
|
317
406
|
conn.setConnectTimeout(timeoutMs);
|
|
318
407
|
conn.setReadTimeout(timeoutMs);
|
|
319
|
-
conn.setRequestProperty("Content-Type",
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
408
|
+
conn.setRequestProperty("Content-Type", contentType);
|
|
409
|
+
if (isHmacMode) {
|
|
410
|
+
String timestamp = String.valueOf(System.currentTimeMillis() / 1000L);
|
|
411
|
+
String nonce = UUID.randomUUID().toString();
|
|
412
|
+
String signature = sign(method, path, timestamp, nonce, bodyBytes);
|
|
413
|
+
conn.setRequestProperty("X-API-Key", apiKey);
|
|
414
|
+
conn.setRequestProperty("X-Timestamp", timestamp);
|
|
415
|
+
conn.setRequestProperty("X-Nonce", nonce);
|
|
416
|
+
conn.setRequestProperty("X-Signature", signature);
|
|
417
|
+
} else if (token != null && !token.isEmpty()) {
|
|
418
|
+
conn.setRequestProperty("Authorization", "Bearer " + token);
|
|
419
|
+
}
|
|
324
420
|
for (Map.Entry<String, String> h : extraHeaders.entrySet()) {
|
|
325
421
|
conn.setRequestProperty(h.getKey(), h.getValue());
|
|
326
422
|
}
|
|
327
423
|
|
|
328
|
-
if (
|
|
424
|
+
if (bodyBytes.length > 0) {
|
|
329
425
|
conn.setDoOutput(true);
|
|
330
426
|
try (OutputStream os = conn.getOutputStream()) {
|
|
331
|
-
os.write(
|
|
427
|
+
os.write(bodyBytes);
|
|
332
428
|
}
|
|
333
429
|
}
|
|
334
430
|
|
|
335
431
|
int status = conn.getResponseCode();
|
|
336
|
-
String
|
|
432
|
+
String respCT = conn.getContentType();
|
|
337
433
|
InputStream stream = status >= 400 ? conn.getErrorStream() : conn.getInputStream();
|
|
338
434
|
byte[] rawBytes = readAllBytes(stream);
|
|
339
435
|
|
|
340
436
|
// 패킷 암호화 응답: application/octet-stream → 복호화
|
|
341
437
|
String response;
|
|
342
|
-
if (
|
|
438
|
+
if (respCT != null && respCT.contains("application/octet-stream")) {
|
|
343
439
|
try {
|
|
344
440
|
response = decryptPacket(rawBytes);
|
|
345
441
|
} catch (Exception e) {
|
|
@@ -355,17 +451,68 @@ public class EntityServerClient {
|
|
|
355
451
|
return response;
|
|
356
452
|
}
|
|
357
453
|
|
|
454
|
+
/**
|
|
455
|
+
* 패킷 암호화 키를 유도합니다.
|
|
456
|
+
* - HMAC 모드: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
457
|
+
* - JWT 모드: SHA256(token)
|
|
458
|
+
*/
|
|
459
|
+
private byte[] derivePacketKey() {
|
|
460
|
+
if (token != null && !token.isEmpty() && hmacSecret.isEmpty()) {
|
|
461
|
+
try {
|
|
462
|
+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
463
|
+
return digest.digest(token.getBytes(StandardCharsets.UTF_8));
|
|
464
|
+
} catch (Exception e) {
|
|
465
|
+
throw new RuntimeException("SHA-256 failed", e);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
HKDFBytesGenerator gen = new HKDFBytesGenerator(new SHA256Digest());
|
|
469
|
+
gen.init(new HKDFParameters(
|
|
470
|
+
hmacSecret.getBytes(StandardCharsets.UTF_8),
|
|
471
|
+
"entity-server:hkdf:v1".getBytes(StandardCharsets.UTF_8),
|
|
472
|
+
"entity-server:packet-encryption".getBytes(StandardCharsets.UTF_8)
|
|
473
|
+
));
|
|
474
|
+
byte[] key = new byte[32];
|
|
475
|
+
gen.generateBytes(key, 0, 32);
|
|
476
|
+
return key;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* XChaCha20-Poly1305 패킷 암호화
|
|
481
|
+
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
482
|
+
* magicLen: 2 + (key[31] & 0xFF) % 14
|
|
483
|
+
*/
|
|
484
|
+
private byte[] encryptPacket(byte[] plaintext) throws Exception {
|
|
485
|
+
byte[] key = derivePacketKey();
|
|
486
|
+
int magicLen = 2 + (key[31] & 0xFF) % 14;
|
|
487
|
+
byte[] magic = new byte[magicLen];
|
|
488
|
+
byte[] nonce = new byte[24];
|
|
489
|
+
SecureRandom rng = new SecureRandom();
|
|
490
|
+
rng.nextBytes(magic);
|
|
491
|
+
rng.nextBytes(nonce);
|
|
492
|
+
|
|
493
|
+
ChaCha20Poly1305 aead = new ChaCha20Poly1305();
|
|
494
|
+
aead.init(true, new AEADParameters(new KeyParameter(key), 128, nonce));
|
|
495
|
+
byte[] ciphertext = new byte[aead.getOutputSize(plaintext.length)];
|
|
496
|
+
int len = aead.processBytes(plaintext, 0, plaintext.length, ciphertext, 0);
|
|
497
|
+
aead.doFinal(ciphertext, len);
|
|
498
|
+
|
|
499
|
+
byte[] result = new byte[magic.length + nonce.length + ciphertext.length];
|
|
500
|
+
System.arraycopy(magic, 0, result, 0, magic.length);
|
|
501
|
+
System.arraycopy(nonce, 0, result, magic.length, nonce.length);
|
|
502
|
+
System.arraycopy(ciphertext, 0, result, magic.length + nonce.length, ciphertext.length);
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
|
|
358
506
|
/**
|
|
359
507
|
* XChaCha20-Poly1305 패킷 복호화
|
|
360
508
|
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
361
|
-
* 키:
|
|
362
|
-
*
|
|
363
|
-
* Bouncy Castle ChaCha20Poly1305 사용 (nonce 24바이트 → 자동으로 XChaCha20 선택)
|
|
509
|
+
* 키: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
364
510
|
*/
|
|
365
511
|
private String decryptPacket(byte[] data) throws Exception {
|
|
366
|
-
byte[] key
|
|
367
|
-
|
|
368
|
-
byte[]
|
|
512
|
+
byte[] key = derivePacketKey();
|
|
513
|
+
int magicLen = 2 + (key[31] & 0xFF) % 14;
|
|
514
|
+
byte[] nonce = Arrays.copyOfRange(data, magicLen, magicLen + 24);
|
|
515
|
+
byte[] ctext = Arrays.copyOfRange(data, magicLen + 24, data.length);
|
|
369
516
|
|
|
370
517
|
ChaCha20Poly1305 aead = new ChaCha20Poly1305();
|
|
371
518
|
aead.init(false, new AEADParameters(new KeyParameter(key), 128, nonce));
|
|
@@ -376,23 +523,23 @@ public class EntityServerClient {
|
|
|
376
523
|
return new String(plaintext, StandardCharsets.UTF_8);
|
|
377
524
|
}
|
|
378
525
|
|
|
379
|
-
/**
|
|
380
|
-
|
|
526
|
+
/**
|
|
527
|
+
* HMAC-SHA256 서명. bodyBytes 는 JSON 또는 암호화된 바이너리 모두 지원합니다.
|
|
528
|
+
*/
|
|
529
|
+
private String sign(String method, String path, String timestamp, String nonce, byte[] bodyBytes) {
|
|
381
530
|
try {
|
|
382
|
-
String payload = String.join("|", method, path, timestamp, nonce, body);
|
|
383
531
|
Mac mac = Mac.getInstance("HmacSHA256");
|
|
384
532
|
mac.init(new SecretKeySpec(hmacSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
|
385
|
-
|
|
386
|
-
|
|
533
|
+
String prefix = method + "|" + path + "|" + timestamp + "|" + nonce + "|";
|
|
534
|
+
mac.update(prefix.getBytes(StandardCharsets.UTF_8));
|
|
535
|
+
if (bodyBytes != null && bodyBytes.length > 0) mac.update(bodyBytes);
|
|
536
|
+
byte[] hash = mac.doFinal();
|
|
537
|
+
return HexFormat.of().formatHex(hash);
|
|
387
538
|
} catch (Exception e) {
|
|
388
539
|
throw new RuntimeException("HMAC signing failed", e);
|
|
389
540
|
}
|
|
390
541
|
}
|
|
391
542
|
|
|
392
|
-
private static byte[] sha256(byte[] input) throws Exception {
|
|
393
|
-
return MessageDigest.getInstance("SHA-256").digest(input);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
543
|
private static byte[] readAllBytes(InputStream in) throws IOException {
|
|
397
544
|
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
|
398
545
|
byte[] chunk = new byte[8192];
|
|
@@ -31,10 +31,11 @@ public class EntityServerExample {
|
|
|
31
31
|
String updated = es.submit("product", "{\"seq\":1,\"name\":\"게이밍 노트북\",\"price\":2000000}");
|
|
32
32
|
System.out.println("Updated: " + updated);
|
|
33
33
|
|
|
34
|
-
//
|
|
34
|
+
// 커스텀 SQL 검색
|
|
35
35
|
String results = es.query("product",
|
|
36
|
-
"
|
|
37
|
-
|
|
36
|
+
"SELECT seq, name, category FROM product WHERE category = ?",
|
|
37
|
+
"[\"electronics\"]",
|
|
38
|
+
10
|
|
38
39
|
);
|
|
39
40
|
System.out.println("Query: " + results);
|
|
40
41
|
|