create-entity-server 0.0.15 → 0.0.23
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
|
@@ -15,7 +15,7 @@ use Config\EntityServer as EntityServerConfig;
|
|
|
15
15
|
* ENTITY_SERVER_URL=http://localhost:47200
|
|
16
16
|
* ENTITY_SERVER_API_KEY=your-api-key
|
|
17
17
|
* ENTITY_SERVER_HMAC_SECRET=your-hmac-secret
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
*
|
|
20
20
|
* 컨트롤러 사용법:
|
|
21
21
|
* $es = new \App\Libraries\EntityServer();
|
|
@@ -28,67 +28,158 @@ class EntityServer
|
|
|
28
28
|
private string $baseUrl;
|
|
29
29
|
private string $apiKey;
|
|
30
30
|
private string $hmacSecret;
|
|
31
|
+
private string $token = '';
|
|
31
32
|
private int $timeout;
|
|
32
|
-
private int $magicLen;
|
|
33
33
|
private bool $requireEncryptedRequest;
|
|
34
|
+
private bool $encryptRequests;
|
|
35
|
+
private bool $packetEncryption = false;
|
|
34
36
|
private ?string $activeTxId = null;
|
|
35
37
|
|
|
36
38
|
public function __construct(
|
|
37
39
|
?string $baseUrl = null,
|
|
38
40
|
?string $apiKey = null,
|
|
39
41
|
?string $hmacSecret = null,
|
|
42
|
+
?string $token = null,
|
|
40
43
|
?int $timeout = null,
|
|
41
|
-
?
|
|
42
|
-
?bool $
|
|
44
|
+
?bool $requireEncryptedRequest = null,
|
|
45
|
+
?bool $encryptRequests = null
|
|
43
46
|
) {
|
|
44
47
|
$config = class_exists(EntityServerConfig::class) ? new EntityServerConfig() : null;
|
|
45
48
|
|
|
46
49
|
$configBaseUrl = $config?->baseUrl ?? env('ENTITY_SERVER_URL', 'http://localhost:47200');
|
|
47
50
|
$configApiKey = $config?->apiKey ?? env('ENTITY_SERVER_API_KEY', '');
|
|
48
51
|
$configHmacSecret = $config?->hmacSecret ?? env('ENTITY_SERVER_HMAC_SECRET', '');
|
|
52
|
+
$configToken = $config?->token ?? env('ENTITY_SERVER_TOKEN', '');
|
|
49
53
|
$configTimeout = $config?->timeout ?? (int) env('ENTITY_SERVER_TIMEOUT', 10);
|
|
50
|
-
$configMagicLen = $config?->magicLen ?? (int) env('ENTITY_PACKET_MAGIC_LEN', 4);
|
|
51
54
|
$configRequireEncrypted = $config?->requireEncryptedRequest ?? true;
|
|
55
|
+
$configEncryptRequests = $config?->encryptRequests ?? false;
|
|
52
56
|
|
|
53
57
|
$this->baseUrl = rtrim($baseUrl ?? (string) $configBaseUrl, '/');
|
|
54
58
|
$this->apiKey = (string) ($apiKey ?? $configApiKey);
|
|
55
59
|
$this->hmacSecret = (string) ($hmacSecret ?? $configHmacSecret);
|
|
60
|
+
$this->token = (string) ($token ?? $configToken);
|
|
56
61
|
$this->timeout = (int) ($timeout ?? $configTimeout);
|
|
57
|
-
$this->magicLen = (int) ($magicLen ?? $configMagicLen);
|
|
58
62
|
$this->requireEncryptedRequest = (bool) ($requireEncryptedRequest ?? $configRequireEncrypted);
|
|
63
|
+
$this->encryptRequests = (bool) ($encryptRequests ?? $configEncryptRequests);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** JWT Bearer 토큰을 설정합니다. HMAC 모드와 배타적으로 사용합니다. */
|
|
67
|
+
public function setToken(string $token): void
|
|
68
|
+
{
|
|
69
|
+
$this->token = $token;
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
// ─── CRUD ────────────────────────────────────────────────────────────────
|
|
62
73
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
74
|
+
/** * 서버 헬스 체크를 수행하고 패킷 암호화 활성 여부를 자동으로 감지합니다.
|
|
75
|
+
* 서버가 packet_encryption: true 를 응답하면 이후 모든 요청에 암호화가 자동 적용됩니다.
|
|
76
|
+
*/
|
|
77
|
+
public function checkHealth(): array
|
|
78
|
+
{
|
|
79
|
+
$ch = curl_init($this->baseUrl . '/v1/health');
|
|
80
|
+
curl_setopt_array($ch, [
|
|
81
|
+
CURLOPT_RETURNTRANSFER => true,
|
|
82
|
+
CURLOPT_TIMEOUT => $this->timeout,
|
|
83
|
+
]);
|
|
84
|
+
$response = curl_exec($ch);
|
|
85
|
+
curl_close($ch);
|
|
86
|
+
$decoded = json_decode($response, true) ?? [];
|
|
87
|
+
if (!empty($decoded['packet_encryption'])) {
|
|
88
|
+
$this->packetEncryption = true;
|
|
89
|
+
}
|
|
90
|
+
return $decoded;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** * 단건 조회
|
|
94
|
+
*
|
|
95
|
+
* @param bool $skipHooks true 이면 after_get 훅 미실행
|
|
96
|
+
*/
|
|
97
|
+
public function get(string $entity, int $seq, bool $skipHooks = false): array
|
|
65
98
|
{
|
|
66
|
-
|
|
99
|
+
$q = $skipHooks ? '?skipHooks=true' : '';
|
|
100
|
+
return $this->request('GET', "/v1/entity/{$entity}/{$seq}{$q}");
|
|
67
101
|
}
|
|
68
102
|
|
|
69
|
-
/**
|
|
70
|
-
|
|
103
|
+
/**
|
|
104
|
+
* 조건으로 단건 조회 (POST + conditions body)
|
|
105
|
+
*
|
|
106
|
+
* @param array $conditions 필터 조건. index/hash/unique 필드만 사용 가능.
|
|
107
|
+
* 예: ['email' => 'user@example.com']
|
|
108
|
+
* @param bool $skipHooks true 이면 after_find 훅 미실행
|
|
109
|
+
*/
|
|
110
|
+
public function find(string $entity, array $conditions, bool $skipHooks = false): array
|
|
71
111
|
{
|
|
72
|
-
$
|
|
73
|
-
return $this->request('
|
|
112
|
+
$q = $skipHooks ? '?skipHooks=true' : '';
|
|
113
|
+
return $this->request('POST', "/v1/entity/{$entity}/find{$q}", $conditions);
|
|
74
114
|
}
|
|
75
115
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
116
|
+
/**
|
|
117
|
+
* 목록 조회
|
|
118
|
+
*
|
|
119
|
+
* @param array $params 페이지/정렬 파라미터 (page, limit, orderBy, orderDir, fields)
|
|
120
|
+
* @param array $conditions 필터 조건 POST body. index/hash/unique 필드만 사용 가능.
|
|
121
|
+
* 예: ['status' => 'active']
|
|
122
|
+
* fields 예: ['*'] 시 전체 필드 반환, 미지정 시 인덱스 필드만 반환 (기본, 가장 빠름)
|
|
123
|
+
* fields 예: ['name','email'] 또는 미지정
|
|
124
|
+
*/
|
|
125
|
+
public function list(string $entity, array $params = [], array $conditions = []): array
|
|
78
126
|
{
|
|
79
|
-
|
|
127
|
+
$queryParams = array_merge(['page' => 1, 'limit' => 20], $params);
|
|
128
|
+
|
|
129
|
+
// orderBy + orderDir → orderBy 앞에 - 접두사 방식으로 변환
|
|
130
|
+
if (isset($queryParams['orderDir'])) {
|
|
131
|
+
$dir = strtoupper((string) $queryParams['orderDir']);
|
|
132
|
+
$orderBy = (string) ($queryParams['orderBy'] ?? '');
|
|
133
|
+
if ($dir === 'DESC' && $orderBy !== '') {
|
|
134
|
+
$queryParams['orderBy'] = '-' . ltrim($orderBy, '-');
|
|
135
|
+
}
|
|
136
|
+
unset($queryParams['orderDir']);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// fields 배열 → 쉼표 구분 문자열
|
|
140
|
+
if (isset($queryParams['fields']) && is_array($queryParams['fields'])) {
|
|
141
|
+
$queryParams['fields'] = implode(',', $queryParams['fields']);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
$query = http_build_query($queryParams);
|
|
145
|
+
return $this->request('POST', "/v1/entity/{$entity}/list?{$query}", $conditions);
|
|
80
146
|
}
|
|
81
147
|
|
|
82
148
|
/**
|
|
83
|
-
*
|
|
149
|
+
* 건수 조회
|
|
84
150
|
*
|
|
85
|
-
* @param array $
|
|
86
|
-
* @param array $params 예: ['page' => 1, 'limit' => 20, 'order_by' => 'name']
|
|
151
|
+
* @param array $conditions 필터 조건 (list()와 동일 규칙)
|
|
87
152
|
*/
|
|
88
|
-
public function
|
|
153
|
+
public function count(string $entity, array $conditions = []): array
|
|
89
154
|
{
|
|
90
|
-
$
|
|
91
|
-
|
|
155
|
+
return $this->request('POST', "/v1/entity/{$entity}/count", $conditions);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 커스텀 SQL 조회 (SELECT 전용, 인덱스 테이블만)
|
|
160
|
+
*
|
|
161
|
+
* - SELECT 쿼리만 허용 (INSERT/UPDATE/DELETE 불가)
|
|
162
|
+
* - 인덱스 테이블(`entity_idx_*`)만 접근 가능. SELECT * 불가
|
|
163
|
+
* - JOIN 지원. 최대 반환 건수 1000
|
|
164
|
+
* - 사용자 입력은 반드시 params 로 바인딩 (SQL Injection 방지)
|
|
165
|
+
*
|
|
166
|
+
* @param string $entity URL 라우트용 기본 엔티티명
|
|
167
|
+
* @param string $sql SELECT SQL
|
|
168
|
+
* @param array $params ? 플레이스홀더 바인딩 값
|
|
169
|
+
* @param int|null $limit 최대 반환 건수 (최대 1000)
|
|
170
|
+
*
|
|
171
|
+
* 예:
|
|
172
|
+
* $es->query('order',
|
|
173
|
+
* 'SELECT o.seq, o.status, u.name FROM order o JOIN account u ON u.data_seq = o.account_seq WHERE o.status = ?',
|
|
174
|
+
* ['pending'], 100);
|
|
175
|
+
*/
|
|
176
|
+
public function query(string $entity, string $sql, array $params = [], ?int $limit = null): array
|
|
177
|
+
{
|
|
178
|
+
$body = ['sql' => $sql, 'params' => $params];
|
|
179
|
+
if ($limit !== null) {
|
|
180
|
+
$body['limit'] = $limit;
|
|
181
|
+
}
|
|
182
|
+
return $this->request('POST', "/v1/entity/{$entity}/query", $body);
|
|
92
183
|
}
|
|
93
184
|
|
|
94
185
|
/**
|
|
@@ -135,26 +226,39 @@ class EntityServer
|
|
|
135
226
|
* 생성 또는 수정
|
|
136
227
|
* - body에 'seq' 포함 → 수정
|
|
137
228
|
* - body에 'seq' 없음 → 생성 (seq 반환)
|
|
229
|
+
* - unique 필드 기준 중복 시 자동 UPDATE (upsert)
|
|
230
|
+
*
|
|
138
231
|
* @param string|null $transactionId transStart() 로 얻은 ID (생략 시 활성 트랜잭션 자동 사용)
|
|
232
|
+
* @param bool $skipHooks true 이면 before/after_insert, before/after_update 훅 미실행
|
|
139
233
|
*/
|
|
140
|
-
public function submit(string $entity, array $data, ?string $transactionId = null): array
|
|
234
|
+
public function submit(string $entity, array $data, ?string $transactionId = null, bool $skipHooks = false): array
|
|
141
235
|
{
|
|
142
236
|
$txId = $transactionId ?? $this->activeTxId;
|
|
143
237
|
$extra = $txId ? ['X-Transaction-ID: ' . $txId] : [];
|
|
144
|
-
|
|
238
|
+
$q = $skipHooks ? '?skipHooks=true' : '';
|
|
239
|
+
return $this->request('POST', "/v1/entity/{$entity}/submit{$q}", $data, $extra);
|
|
145
240
|
}
|
|
146
241
|
|
|
147
242
|
/**
|
|
148
|
-
* 삭제
|
|
149
|
-
*
|
|
243
|
+
* 삭제 (서버는 POST /delete/:seq 로만 처리)
|
|
244
|
+
*
|
|
245
|
+
* @param bool $hard true 이면 하드(물리) 삭제. false(기본) 이면 소프트 삭제 (rollback 으로 복원 가능)
|
|
150
246
|
* @param string|null $transactionId transStart() 로 얻은 ID (생략 시 활성 트랜잭션 자동 사용)
|
|
247
|
+
* @param bool $skipHooks true 이면 before/after_delete 훅 미실행
|
|
151
248
|
*/
|
|
152
|
-
public function delete(string $entity, int $seq, ?string $transactionId = null, bool $hard = false): array
|
|
249
|
+
public function delete(string $entity, int $seq, ?string $transactionId = null, bool $hard = false, bool $skipHooks = false): array
|
|
153
250
|
{
|
|
154
|
-
$
|
|
251
|
+
$queryParams = [];
|
|
252
|
+
if ($hard) {
|
|
253
|
+
$queryParams[] = 'hard=true';
|
|
254
|
+
}
|
|
255
|
+
if ($skipHooks) {
|
|
256
|
+
$queryParams[] = 'skipHooks=true';
|
|
257
|
+
}
|
|
258
|
+
$q = $queryParams ? '?' . implode('&', $queryParams) : '';
|
|
155
259
|
$txId = $transactionId ?? $this->activeTxId;
|
|
156
260
|
$extra = $txId ? ['X-Transaction-ID: ' . $txId] : [];
|
|
157
|
-
return $this->request('
|
|
261
|
+
return $this->request('POST', "/v1/entity/{$entity}/delete/{$seq}{$q}", [], $extra);
|
|
158
262
|
}
|
|
159
263
|
|
|
160
264
|
/** 변경 이력 조회 */
|
|
@@ -298,18 +402,32 @@ class EntityServer
|
|
|
298
402
|
|
|
299
403
|
private function request(string $method, string $path, array $body = [], array $extraHeaders = []): array
|
|
300
404
|
{
|
|
301
|
-
|
|
302
|
-
$
|
|
303
|
-
$
|
|
304
|
-
$
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
'
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
405
|
+
// 요청 바디 결정: encryptRequests 시 POST 바디를 암호화
|
|
406
|
+
$bodyJson = empty($body) ? '' : json_encode($body, JSON_UNESCAPED_UNICODE);
|
|
407
|
+
$bodyData = $bodyJson;
|
|
408
|
+
$contentType = 'application/json';
|
|
409
|
+
if (($this->encryptRequests || $this->packetEncryption) && $bodyJson !== '') {
|
|
410
|
+
$bodyData = $this->encryptPacket($bodyJson);
|
|
411
|
+
$contentType = 'application/octet-stream';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
$isHmacMode = $this->apiKey !== '' && $this->hmacSecret !== '';
|
|
415
|
+
|
|
416
|
+
$headers = ["Content-Type: {$contentType}"];
|
|
417
|
+
if ($isHmacMode) {
|
|
418
|
+
$timestamp = (string) time();
|
|
419
|
+
$nonce = $this->generateNonce();
|
|
420
|
+
$signature = $this->sign($method, $path, $timestamp, $nonce, $bodyData);
|
|
421
|
+
$headers = array_merge($headers, [
|
|
422
|
+
'X-API-Key: ' . $this->apiKey,
|
|
423
|
+
'X-Timestamp: ' . $timestamp,
|
|
424
|
+
'X-Nonce: ' . $nonce,
|
|
425
|
+
'X-Signature: ' . $signature,
|
|
426
|
+
]);
|
|
427
|
+
} elseif ($this->token !== '') {
|
|
428
|
+
$headers[] = 'Authorization: Bearer ' . $this->token;
|
|
429
|
+
}
|
|
430
|
+
$headers = array_merge($headers, $extraHeaders);
|
|
313
431
|
|
|
314
432
|
$url = $this->baseUrl . $path;
|
|
315
433
|
$ch = curl_init($url);
|
|
@@ -321,13 +439,13 @@ class EntityServer
|
|
|
321
439
|
CURLOPT_TIMEOUT => $this->timeout,
|
|
322
440
|
]);
|
|
323
441
|
|
|
324
|
-
if ($
|
|
325
|
-
curl_setopt($ch, CURLOPT_POSTFIELDS, $
|
|
442
|
+
if ($bodyData !== '') {
|
|
443
|
+
curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyData);
|
|
326
444
|
}
|
|
327
445
|
|
|
328
446
|
$response = curl_exec($ch);
|
|
329
447
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
330
|
-
$
|
|
448
|
+
$respContentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE) ?? '';
|
|
331
449
|
$error = curl_error($ch);
|
|
332
450
|
curl_close($ch);
|
|
333
451
|
|
|
@@ -336,7 +454,7 @@ class EntityServer
|
|
|
336
454
|
}
|
|
337
455
|
|
|
338
456
|
// 패킷 암호화 응답: application/octet-stream → 복호화
|
|
339
|
-
if (str_contains($
|
|
457
|
+
if (str_contains($respContentType, 'application/octet-stream')) {
|
|
340
458
|
$jsonStr = $this->decryptPacket($response);
|
|
341
459
|
$decoded = json_decode($jsonStr, true);
|
|
342
460
|
} else {
|
|
@@ -354,18 +472,51 @@ class EntityServer
|
|
|
354
472
|
return $decoded;
|
|
355
473
|
}
|
|
356
474
|
|
|
475
|
+
/**
|
|
476
|
+
* 패킷 암호화 키를 유도합니다.
|
|
477
|
+
* - HMAC 모드: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
478
|
+
* - JWT 모드: SHA256(token)
|
|
479
|
+
*/
|
|
480
|
+
private function derivePacketKey(): string
|
|
481
|
+
{
|
|
482
|
+
if ($this->token !== '' && $this->hmacSecret === '') {
|
|
483
|
+
return hash('sha256', $this->token, true);
|
|
484
|
+
}
|
|
485
|
+
$salt = 'entity-server:hkdf:v1';
|
|
486
|
+
$info = 'entity-server:packet-encryption';
|
|
487
|
+
// HKDF-Extract: PRK = HMAC-SHA256(salt, IKM)
|
|
488
|
+
$prk = hash_hmac('sha256', $this->hmacSecret, $salt, true);
|
|
489
|
+
// HKDF-Expand(PRK, info, 32): T(1) = HMAC-SHA256(PRK, info || 0x01)
|
|
490
|
+
return substr(hash_hmac('sha256', $info . chr(1), $prk, true), 0, 32);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* XChaCha20-Poly1305 패킷 암호화
|
|
495
|
+
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
496
|
+
*/
|
|
497
|
+
private function encryptPacket(string $plaintext): string
|
|
498
|
+
{
|
|
499
|
+
$key = $this->derivePacketKey();
|
|
500
|
+
$magicLen = 2 + (ord($key[31]) % 14);
|
|
501
|
+
$magic = random_bytes($magicLen);
|
|
502
|
+
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); // 24
|
|
503
|
+
$ct = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($plaintext, '', $nonce, $key);
|
|
504
|
+
return $magic . $nonce . $ct;
|
|
505
|
+
}
|
|
506
|
+
|
|
357
507
|
/**
|
|
358
508
|
* XChaCha20-Poly1305 패킷 복호화
|
|
359
509
|
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
360
|
-
* 키:
|
|
510
|
+
* 키: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
361
511
|
*
|
|
362
512
|
* ext-sodium 사용 (PHP 7.2+ 내장)
|
|
363
513
|
*/
|
|
364
514
|
private function decryptPacket(string $data): string
|
|
365
515
|
{
|
|
366
|
-
$key =
|
|
367
|
-
$
|
|
368
|
-
$
|
|
516
|
+
$key = $this->derivePacketKey();
|
|
517
|
+
$magicLen = 2 + (ord($key[31]) % 14);
|
|
518
|
+
$nonce = substr($data, $magicLen, 24);
|
|
519
|
+
$ciphertext = substr($data, $magicLen + 24);
|
|
369
520
|
|
|
370
521
|
$plaintext = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($ciphertext, '', $nonce, $key);
|
|
371
522
|
if ($plaintext === false) {
|
|
@@ -374,12 +525,14 @@ class EntityServer
|
|
|
374
525
|
return $plaintext;
|
|
375
526
|
}
|
|
376
527
|
|
|
377
|
-
/**
|
|
528
|
+
/**
|
|
529
|
+
* HMAC-SHA256 서명. $body 는 JSON 스트링 또는 바이너리 암호화 페이로드 모두 지원합니다.
|
|
530
|
+
* prefix = "METHOD|path|timestamp|nonce|" 뒤에 $body 를 바로 이어 붙여 서명합니다.
|
|
531
|
+
*/
|
|
378
532
|
private function sign(string $method, string $path, string $timestamp, string $nonce, string $body): string
|
|
379
533
|
{
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return hash_hmac('sha256', $payload, $this->hmacSecret);
|
|
534
|
+
$prefix = implode('|', [$method, $path, $timestamp, $nonce]) . '|';
|
|
535
|
+
return hash_hmac('sha256', $prefix . $body, $this->hmacSecret);
|
|
383
536
|
}
|
|
384
537
|
|
|
385
538
|
private function generateNonce(): string
|