create-entity-server 0.0.9 → 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 +26 -8
- package/package.json +1 -1
- package/template/.env.example +20 -3
- package/template/configs/database.json +173 -10
- package/template/configs/jwt.json +1 -0
- package/template/configs/oauth.json +37 -0
- package/template/configs/push.json +26 -0
- package/template/entities/Account/account_audit.json +4 -5
- package/template/entities/README.md +4 -4
- package/template/entities/{Auth → System/Auth}/account.json +0 -14
- package/template/entities/System/system_audit_log.json +14 -8
- package/template/samples/README.md +43 -21
- package/template/samples/browser/entity-server-client.js +453 -0
- package/template/samples/browser/example.html +498 -0
- package/template/samples/entities/01_basic_fields.json +39 -0
- package/template/samples/entities/02_types_and_defaults.json +67 -0
- package/template/samples/entities/03_hash_and_unique.json +33 -0
- package/template/samples/entities/04_fk_and_composite_unique.json +29 -0
- package/template/samples/entities/05_cache.json +55 -0
- package/template/samples/entities/06_history_and_hard_delete.json +60 -0
- package/template/samples/entities/07_license_scope.json +52 -0
- package/template/samples/entities/08_hook_sql.json +52 -0
- package/template/samples/entities/09_hook_entity.json +65 -0
- package/template/samples/entities/10_hook_submit_delete.json +78 -0
- package/template/samples/entities/11_hook_webhook.json +84 -0
- package/template/samples/entities/12_hook_push.json +73 -0
- package/template/samples/entities/13_read_only.json +54 -0
- package/template/samples/entities/14_optimistic_lock.json +29 -0
- package/template/samples/entities/15_reset_defaults.json +94 -0
- package/template/samples/entities/16_isolated_license.json +62 -0
- package/template/samples/entities/README.md +91 -0
- package/template/samples/flutter/lib/entity_server_client.dart +261 -48
- package/template/samples/java/EntityServerClient.java +325 -61
- package/template/samples/java/EntityServerExample.java +4 -3
- package/template/samples/kotlin/EntityServerClient.kt +261 -45
- package/template/samples/node/src/EntityServerClient.js +348 -59
- package/template/samples/node/src/example.js +9 -9
- package/template/samples/php/ci4/Config/EntityServer.php +14 -0
- package/template/samples/php/ci4/Controllers/EntityController.php +202 -0
- package/template/samples/php/ci4/Controllers/ProductController.php +16 -76
- package/template/samples/php/ci4/Libraries/EntityServer.php +352 -60
- package/template/samples/php/laravel/Services/EntityServerService.php +245 -40
- package/template/samples/python/entity_server.py +287 -68
- package/template/samples/python/example.py +7 -6
- package/template/samples/react/src/example.tsx +41 -25
- package/template/samples/swift/EntityServerClient.swift +248 -37
- package/template/scripts/normalize-entities.sh +10 -10
- package/template/scripts/run.ps1 +12 -3
- package/template/scripts/run.sh +120 -37
- package/template/scripts/update-server.ps1 +160 -4
- package/template/scripts/update-server.sh +132 -4
- package/template/samples/react/src/api/entityServerClient.ts +0 -290
- package/template/samples/react/src/hooks/useEntity.ts +0 -105
- /package/template/entities/{Auth → System/Auth}/api_keys.json +0 -0
- /package/template/entities/{Auth → System/Auth}/license.json +0 -0
- /package/template/entities/{Auth → System/Auth}/rbac_roles.json +0 -0
|
@@ -4,6 +4,7 @@ namespace App\Services;
|
|
|
4
4
|
|
|
5
5
|
use Illuminate\Support\Facades\Http;
|
|
6
6
|
use Illuminate\Support\Str;
|
|
7
|
+
use Illuminate\Http\Request;
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Entity Server 클라이언트 서비스 (Laravel)
|
|
@@ -14,7 +15,6 @@ use Illuminate\Support\Str;
|
|
|
14
15
|
* ENTITY_SERVER_URL=http://localhost:47200
|
|
15
16
|
* ENTITY_SERVER_API_KEY=your-api-key
|
|
16
17
|
* ENTITY_SERVER_HMAC_SECRET=your-hmac-secret
|
|
17
|
-
* ENTITY_PACKET_MAGIC_LEN=4
|
|
18
18
|
*
|
|
19
19
|
* 서비스 프로바이더 등록:
|
|
20
20
|
* $this->app->singleton(EntityServerService::class);
|
|
@@ -39,7 +39,10 @@ class EntityServerService
|
|
|
39
39
|
private string $baseUrl;
|
|
40
40
|
private string $apiKey;
|
|
41
41
|
private string $hmacSecret;
|
|
42
|
-
private
|
|
42
|
+
private string $token = '';
|
|
43
|
+
private bool $requireEncryptedRequest;
|
|
44
|
+
private bool $encryptRequests;
|
|
45
|
+
private bool $packetEncryption = false;
|
|
43
46
|
private ?string $activeTxId = null;
|
|
44
47
|
|
|
45
48
|
public function __construct()
|
|
@@ -47,31 +50,115 @@ class EntityServerService
|
|
|
47
50
|
$this->baseUrl = rtrim(config('services.entity_server.url', env('ENTITY_SERVER_URL', 'http://localhost:47200')), '/');
|
|
48
51
|
$this->apiKey = config('services.entity_server.api_key', env('ENTITY_SERVER_API_KEY', ''));
|
|
49
52
|
$this->hmacSecret = config('services.entity_server.hmac_secret', env('ENTITY_SERVER_HMAC_SECRET', ''));
|
|
50
|
-
$this->
|
|
53
|
+
$this->token = config('services.entity_server.token', env('ENTITY_SERVER_TOKEN', ''));
|
|
54
|
+
$this->requireEncryptedRequest = (bool) config('services.entity_server.require_encrypted_request', env('ENTITY_REQUIRE_ENCRYPTED_REQUEST', true));
|
|
55
|
+
$this->encryptRequests = (bool) config('services.entity_server.encrypt_requests', env('ENTITY_ENCRYPT_REQUESTS', false));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** JWT Bearer 토큰을 설정합니다. HMAC 모드와 배타적으로 사용합니다. */
|
|
59
|
+
public function setToken(string $token): void
|
|
60
|
+
{
|
|
61
|
+
$this->token = $token;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
// ─── CRUD ────────────────────────────────────────────────────────────────
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
/**
|
|
67
|
+
* 서버 헬스 체크를 수행하고 패킷 암호화 활성 여부를 자동으로 감지합니다.
|
|
68
|
+
* 서버가 packet_encryption: true 를 응답하면 이후 모든 요청에 암호화가 자동 적용됩니다.
|
|
69
|
+
*/
|
|
70
|
+
public function checkHealth(): array
|
|
56
71
|
{
|
|
57
|
-
|
|
72
|
+
$response = Http::timeout(10)->get($this->baseUrl . '/v1/health');
|
|
73
|
+
$decoded = $response->json() ?? [];
|
|
74
|
+
if (!empty($decoded['packet_encryption'])) {
|
|
75
|
+
$this->packetEncryption = true;
|
|
76
|
+
}
|
|
77
|
+
return $decoded;
|
|
58
78
|
}
|
|
59
79
|
|
|
60
|
-
|
|
80
|
+
/**
|
|
81
|
+
* 단건 조회
|
|
82
|
+
*
|
|
83
|
+
* @param bool $skipHooks true 이면 after_get 훅 미실행
|
|
84
|
+
*/
|
|
85
|
+
public function get(string $entity, int $seq, bool $skipHooks = false): array
|
|
61
86
|
{
|
|
62
|
-
$
|
|
63
|
-
return $this->request('GET', "/v1/entity/{$entity}/
|
|
87
|
+
$q = $skipHooks ? '?skipHooks=true' : '';
|
|
88
|
+
return $this->request('GET', "/v1/entity/{$entity}/{$seq}{$q}");
|
|
64
89
|
}
|
|
65
90
|
|
|
66
|
-
|
|
91
|
+
/**
|
|
92
|
+
* 조건으로 단건 조회 (POST + conditions body)
|
|
93
|
+
*
|
|
94
|
+
* @param array $conditions 필터 조건. index/hash/unique 필드만 사용 가능.
|
|
95
|
+
* 예: ['email' => 'user@example.com']
|
|
96
|
+
* @param bool $skipHooks true 이면 after_find 훅 미실행
|
|
97
|
+
*/
|
|
98
|
+
public function find(string $entity, array $conditions, bool $skipHooks = false): array
|
|
67
99
|
{
|
|
68
|
-
|
|
100
|
+
$q = $skipHooks ? '?skipHooks=true' : '';
|
|
101
|
+
return $this->request('POST', "/v1/entity/{$entity}/find{$q}", $conditions);
|
|
69
102
|
}
|
|
70
103
|
|
|
71
|
-
|
|
104
|
+
/**
|
|
105
|
+
* 목록 조회 (POST + conditions body)
|
|
106
|
+
*
|
|
107
|
+
* @param array $params 펙이지/정렬 파라미터 (page, limit, orderBy, orderDir, fields)
|
|
108
|
+
* @param array $conditions 필터 조건 POST body. index/hash/unique 필드만 사용 가능.
|
|
109
|
+
* fields 예: ['*'] 시 전체 필드 반환, 미지정 시 인덱스 필드만 반환 (기본, 가장 빠름)
|
|
110
|
+
* fields 예: ['name','email'] 또는 미지정
|
|
111
|
+
*/
|
|
112
|
+
public function list(string $entity, array $params = [], array $conditions = []): array
|
|
72
113
|
{
|
|
73
|
-
$
|
|
74
|
-
|
|
114
|
+
$queryParams = array_merge(['page' => 1, 'limit' => 20], $params);
|
|
115
|
+
|
|
116
|
+
if (isset($queryParams['orderDir'])) {
|
|
117
|
+
$dir = strtoupper((string) $queryParams['orderDir']);
|
|
118
|
+
$orderBy = (string) ($queryParams['orderBy'] ?? '');
|
|
119
|
+
if ($dir === 'DESC' && $orderBy !== '') {
|
|
120
|
+
$queryParams['orderBy'] = '-' . ltrim($orderBy, '-');
|
|
121
|
+
}
|
|
122
|
+
unset($queryParams['orderDir']);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isset($queryParams['fields']) && is_array($queryParams['fields'])) {
|
|
126
|
+
$queryParams['fields'] = implode(',', $queryParams['fields']);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
$query = http_build_query($queryParams);
|
|
130
|
+
return $this->request('POST', "/v1/entity/{$entity}/list?{$query}", $conditions);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 건수 조회
|
|
135
|
+
*
|
|
136
|
+
* @param array $conditions 필터 조건 (list() 와 동일 규칙)
|
|
137
|
+
*/
|
|
138
|
+
public function count(string $entity, array $conditions = []): array
|
|
139
|
+
{
|
|
140
|
+
return $this->request('POST', "/v1/entity/{$entity}/count", $conditions);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 커스텀 SQL 조회 (SELECT 전용, 인덱스 테이블만, JOIN 지원)
|
|
145
|
+
*
|
|
146
|
+
* - SELECT 쿼리만 허용 (INSERT/UPDATE/DELETE 불가)
|
|
147
|
+
* - SELECT * 불가. 최대 반환 건수 1000
|
|
148
|
+
* - 사용자 입력은 반드시 $params 로 바인딩 (SQL Injection 방지)
|
|
149
|
+
*
|
|
150
|
+
* @param string $entity URL 라우트용 기본 엔티티명
|
|
151
|
+
* @param string $sql SELECT SQL
|
|
152
|
+
* @param array $params ? 플레이스홀더 바인딩 값
|
|
153
|
+
* @param int|null $limit 최대 반환 건수 (최대 1000)
|
|
154
|
+
*/
|
|
155
|
+
public function query(string $entity, string $sql, array $params = [], ?int $limit = null): array
|
|
156
|
+
{
|
|
157
|
+
$body = ['sql' => $sql, 'params' => $params];
|
|
158
|
+
if ($limit !== null) {
|
|
159
|
+
$body['limit'] = $limit;
|
|
160
|
+
}
|
|
161
|
+
return $this->request('POST', "/v1/entity/{$entity}/query", $body);
|
|
75
162
|
}
|
|
76
163
|
|
|
77
164
|
/**
|
|
@@ -112,20 +199,34 @@ class EntityServerService
|
|
|
112
199
|
}
|
|
113
200
|
|
|
114
201
|
/** 생성 또는 수정 (seq 포함시 수정, 없으면 생성) */
|
|
115
|
-
public function submit(string $entity, array $data, ?string $transactionId = null): array
|
|
202
|
+
public function submit(string $entity, array $data, ?string $transactionId = null, bool $skipHooks = false): array
|
|
116
203
|
{
|
|
117
204
|
$txId = $transactionId ?? $this->activeTxId;
|
|
118
205
|
$extra = $txId ? ['X-Transaction-ID' => $txId] : [];
|
|
119
|
-
|
|
206
|
+
$q = $skipHooks ? '?skipHooks=true' : '';
|
|
207
|
+
return $this->request('POST', "/v1/entity/{$entity}/submit{$q}", $data, $extra);
|
|
120
208
|
}
|
|
121
209
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
210
|
+
/**
|
|
211
|
+
* 삭제 (서버는 POST /delete/:seq 로만 처리)
|
|
212
|
+
*
|
|
213
|
+
* @param bool $hard true 이면 하드(물리) 삭제. false(기본) 소프트 삭제 (rollback 복원 가능)
|
|
214
|
+
* @param string|null $transactionId transStart() 로 얻은 ID (생략 시 활성 트랜잭션 자동)
|
|
215
|
+
* @param bool $skipHooks true 이면 before/after_delete 훅 미실행
|
|
216
|
+
*/
|
|
217
|
+
public function delete(string $entity, int $seq, ?string $transactionId = null, bool $hard = false, bool $skipHooks = false): array
|
|
124
218
|
{
|
|
125
|
-
$
|
|
219
|
+
$queryParams = [];
|
|
220
|
+
if ($hard) {
|
|
221
|
+
$queryParams[] = 'hard=true';
|
|
222
|
+
}
|
|
223
|
+
if ($skipHooks) {
|
|
224
|
+
$queryParams[] = 'skipHooks=true';
|
|
225
|
+
}
|
|
226
|
+
$q = $queryParams ? '?' . implode('&', $queryParams) : '';
|
|
126
227
|
$txId = $transactionId ?? $this->activeTxId;
|
|
127
228
|
$extra = $txId ? ['X-Transaction-ID' => $txId] : [];
|
|
128
|
-
return $this->request('
|
|
229
|
+
return $this->request('POST', "/v1/entity/{$entity}/delete/{$seq}{$q}", [], $extra);
|
|
129
230
|
}
|
|
130
231
|
|
|
131
232
|
public function history(string $entity, int $seq, int $page = 1, int $limit = 50): array
|
|
@@ -138,34 +239,101 @@ class EntityServerService
|
|
|
138
239
|
return $this->request('POST', "/v1/entity/{$entity}/rollback/{$historySeq}");
|
|
139
240
|
}
|
|
140
241
|
|
|
242
|
+
/**
|
|
243
|
+
* 푸시 발송 트리거 엔티티에 submit합니다.
|
|
244
|
+
*/
|
|
245
|
+
public function push(string $pushEntity, array $payload, ?string $transactionId = null): array
|
|
246
|
+
{
|
|
247
|
+
return $this->submit($pushEntity, $payload, $transactionId);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* push_log 목록 조회 헬퍼
|
|
252
|
+
*/
|
|
253
|
+
public function pushLogList(array $params = []): array
|
|
254
|
+
{
|
|
255
|
+
return $this->list('push_log', $params);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Laravel Request에서 암호화 패킷 또는 평문 JSON 본문을 읽어 배열로 반환합니다.
|
|
260
|
+
*/
|
|
261
|
+
public function readRequestBody(Request $request, ?bool $requireEncrypted = null): array
|
|
262
|
+
{
|
|
263
|
+
$requireEncrypted = $requireEncrypted ?? $this->requireEncryptedRequest;
|
|
264
|
+
|
|
265
|
+
$contentType = strtolower((string) $request->header('Content-Type', ''));
|
|
266
|
+
$rawBody = (string) $request->getContent();
|
|
267
|
+
$isEncrypted = str_contains($contentType, 'application/octet-stream');
|
|
268
|
+
|
|
269
|
+
if ($requireEncrypted && !$isEncrypted) {
|
|
270
|
+
throw new \RuntimeException('Encrypted request required: Content-Type must be application/octet-stream');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if ($isEncrypted) {
|
|
274
|
+
if ($rawBody === '') {
|
|
275
|
+
throw new \RuntimeException('Encrypted request body is empty');
|
|
276
|
+
}
|
|
277
|
+
$jsonStr = $this->decryptPacket($rawBody);
|
|
278
|
+
$decoded = json_decode($jsonStr, true);
|
|
279
|
+
if (!is_array($decoded)) {
|
|
280
|
+
throw new \RuntimeException('Invalid encrypted JSON payload');
|
|
281
|
+
}
|
|
282
|
+
return $decoded;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if ($rawBody === '') {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
$decoded = json_decode($rawBody, true);
|
|
289
|
+
if (!is_array($decoded)) {
|
|
290
|
+
throw new \RuntimeException('Invalid JSON payload');
|
|
291
|
+
}
|
|
292
|
+
return $decoded;
|
|
293
|
+
}
|
|
294
|
+
|
|
141
295
|
// ─── 내부 ─────────────────────────────────────────────────────────────────
|
|
142
296
|
|
|
143
297
|
private function request(string $method, string $path, array $body = [], array $extraHeaders = []): array
|
|
144
298
|
{
|
|
145
|
-
|
|
146
|
-
$
|
|
147
|
-
$
|
|
148
|
-
$
|
|
299
|
+
// 요청 바디 결정: encryptRequests 시 POST 바디를 암호화
|
|
300
|
+
$bodyJson = empty($body) ? '' : json_encode($body, JSON_UNESCAPED_UNICODE);
|
|
301
|
+
$bodyData = $bodyJson;
|
|
302
|
+
$contentType = 'application/json';
|
|
303
|
+
|
|
304
|
+
if (($this->encryptRequests || $this->packetEncryption) && $bodyJson !== '') {
|
|
305
|
+
$bodyData = $this->encryptPacket($bodyJson);
|
|
306
|
+
$contentType = 'application/octet-stream';
|
|
307
|
+
}
|
|
149
308
|
|
|
150
|
-
$
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
309
|
+
$isHmacMode = $this->apiKey !== '' && $this->hmacSecret !== '';
|
|
310
|
+
|
|
311
|
+
$http = Http::timeout(10);
|
|
312
|
+
if ($isHmacMode) {
|
|
313
|
+
$timestamp = (string) time();
|
|
314
|
+
$nonce = (string) Str::uuid();
|
|
315
|
+
$signature = $this->sign($method, $path, $timestamp, $nonce, $bodyData);
|
|
316
|
+
$http = $http->withHeaders(array_merge([
|
|
317
|
+
'X-API-Key' => $this->apiKey,
|
|
318
|
+
'X-Timestamp' => $timestamp,
|
|
319
|
+
'X-Nonce' => $nonce,
|
|
320
|
+
'X-Signature' => $signature,
|
|
321
|
+
], $extraHeaders));
|
|
322
|
+
} else {
|
|
323
|
+
$authHeaders = $this->token !== '' ? ['Authorization' => 'Bearer ' . $this->token] : [];
|
|
324
|
+
$http = $http->withHeaders(array_merge($authHeaders, $extraHeaders));
|
|
325
|
+
}
|
|
156
326
|
|
|
157
327
|
$response = match ($method) {
|
|
158
328
|
'GET' => $http->get($this->baseUrl . $path),
|
|
159
|
-
'POST' => $http->withBody($
|
|
329
|
+
'POST' => $http->withBody($bodyData, $contentType)->post($this->baseUrl . $path),
|
|
160
330
|
'DELETE' => $http->delete($this->baseUrl . $path),
|
|
161
331
|
default => throw new \InvalidArgumentException("Unsupported method: {$method}"),
|
|
162
332
|
};
|
|
163
333
|
|
|
164
|
-
$decoded = $response->json();
|
|
165
|
-
|
|
166
334
|
// 패킷 암호화 응답: application/octet-stream → 복호화
|
|
167
|
-
$
|
|
168
|
-
if (str_contains($
|
|
335
|
+
$respContentType = $response->header('Content-Type') ?? '';
|
|
336
|
+
if (str_contains($respContentType, 'application/octet-stream')) {
|
|
169
337
|
$jsonStr = $this->decryptPacket($response->body());
|
|
170
338
|
$decoded = json_decode($jsonStr, true);
|
|
171
339
|
} else {
|
|
@@ -182,18 +350,51 @@ class EntityServerService
|
|
|
182
350
|
return $decoded;
|
|
183
351
|
}
|
|
184
352
|
|
|
353
|
+
/**
|
|
354
|
+
* 패킷 암호화 키를 유도합니다.
|
|
355
|
+
* - HMAC 모드: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
356
|
+
* - JWT 모드: SHA256(token)
|
|
357
|
+
*/
|
|
358
|
+
private function derivePacketKey(): string
|
|
359
|
+
{
|
|
360
|
+
if ($this->token !== '' && $this->hmacSecret === '') {
|
|
361
|
+
return hash('sha256', $this->token, true);
|
|
362
|
+
}
|
|
363
|
+
$salt = 'entity-server:hkdf:v1';
|
|
364
|
+
$info = 'entity-server:packet-encryption';
|
|
365
|
+
// HKDF-Extract: PRK = HMAC-SHA256(salt, IKM)
|
|
366
|
+
$prk = hash_hmac('sha256', $this->hmacSecret, $salt, true);
|
|
367
|
+
// HKDF-Expand(PRK, info, 32): T(1) = HMAC-SHA256(PRK, info || 0x01)
|
|
368
|
+
return substr(hash_hmac('sha256', $info . chr(1), $prk, true), 0, 32);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* XChaCha20-Poly1305 패킷 암호화
|
|
373
|
+
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
374
|
+
*/
|
|
375
|
+
private function encryptPacket(string $plaintext): string
|
|
376
|
+
{
|
|
377
|
+
$key = $this->derivePacketKey();
|
|
378
|
+
$magicLen = 2 + (ord($key[31]) % 14);
|
|
379
|
+
$magic = random_bytes($magicLen);
|
|
380
|
+
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); // 24
|
|
381
|
+
$ct = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($plaintext, '', $nonce, $key);
|
|
382
|
+
return $magic . $nonce . $ct;
|
|
383
|
+
}
|
|
384
|
+
|
|
185
385
|
/**
|
|
186
386
|
* XChaCha20-Poly1305 패킷 복호화
|
|
187
387
|
* 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
|
|
188
|
-
* 키:
|
|
388
|
+
* 키: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
|
|
189
389
|
*
|
|
190
390
|
* ext-sodium 사용 (PHP 7.2+ 내장)
|
|
191
391
|
*/
|
|
192
392
|
private function decryptPacket(string $data): string
|
|
193
393
|
{
|
|
194
|
-
$key =
|
|
195
|
-
$
|
|
196
|
-
$
|
|
394
|
+
$key = $this->derivePacketKey();
|
|
395
|
+
$magicLen = 2 + (ord($key[31]) % 14);
|
|
396
|
+
$nonce = substr($data, $magicLen, 24);
|
|
397
|
+
$ciphertext = substr($data, $magicLen + 24);
|
|
197
398
|
|
|
198
399
|
$plaintext = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($ciphertext, '', $nonce, $key);
|
|
199
400
|
if ($plaintext === false) {
|
|
@@ -202,9 +403,13 @@ class EntityServerService
|
|
|
202
403
|
return $plaintext;
|
|
203
404
|
}
|
|
204
405
|
|
|
406
|
+
/**
|
|
407
|
+
* HMAC-SHA256 서명. $body 는 JSON 스트링 또는 바이너리 암호화 페이로드 모두 지원합니다.
|
|
408
|
+
* prefix = "METHOD|path|timestamp|nonce|" 뒤에 $body 를 바로 이어 붙여 서명합니다.
|
|
409
|
+
*/
|
|
205
410
|
private function sign(string $method, string $path, string $timestamp, string $nonce, string $body): string
|
|
206
411
|
{
|
|
207
|
-
$
|
|
208
|
-
return hash_hmac('sha256', $
|
|
412
|
+
$prefix = implode('|', [$method, $path, $timestamp, $nonce]) . '|';
|
|
413
|
+
return hash_hmac('sha256', $prefix . $body, $this->hmacSecret);
|
|
209
414
|
}
|
|
210
415
|
}
|