feat: app
This commit is contained in:
@@ -239,7 +239,7 @@ class KeystoreController extends Controller
|
|||||||
if (! $this->keystoreAllowed($keystore)) {
|
if (! $this->keystoreAllowed($keystore)) {
|
||||||
return response()->json(['code' => 1, 'msg' => '无权操作'], 403);
|
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);
|
return response()->json(['code' => 1, 'msg' => '该钥匙串未标记为需要密码'], 400);
|
||||||
}
|
}
|
||||||
$password = trim((string) $request->input('password', ''));
|
$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) {
|
{ title: '操作', width: 260, align: 'center', templet: function (d) {
|
||||||
var html = '';
|
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.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 (d.password_decrypt_url && (Number(d.needs_password) === 1 || d.has_web3_keystore)) {
|
||||||
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>';
|
html += '<a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="decryptPassword">密码解密</a>';
|
||||||
|
}
|
||||||
return html || '—';
|
return html || '—';
|
||||||
} }
|
} }
|
||||||
]],
|
]],
|
||||||
@@ -765,30 +766,6 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
|||||||
|
|
||||||
if (tab === 'keystores') {
|
if (tab === 'keystores') {
|
||||||
table.on('tool(LAY-device-tab-list)', function (obj) {
|
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.event === 'decryptPassword') {
|
||||||
if (!obj.data.password_decrypt_url) return layer.msg('无法密码解密');
|
if (!obj.data.password_decrypt_url) return layer.msg('无法密码解密');
|
||||||
layer.prompt({
|
layer.prompt({
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use App\Models\WalletMnemonic;
|
|||||||
use App\Services\EthKeystore;
|
use App\Services\EthKeystore;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use kornrunner\Keccak;
|
||||||
use PHPUnit\Framework\Attributes\Test;
|
use PHPUnit\Framework\Attributes\Test;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -89,8 +90,7 @@ class KeystoreAdminTest extends TestCase
|
|||||||
$this->actingAs($admin, 'admin')
|
$this->actingAs($admin, 'admin')
|
||||||
->get(route('admin.devices.show', [$device, 'tab' => 'keystores']))
|
->get(route('admin.devices.show', [$device, 'tab' => 'keystores']))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('钥匙串')
|
->assertSee('钥匙串');
|
||||||
->assertSee('lay-event="decrypt">密码解密</a>', false);
|
|
||||||
|
|
||||||
$this->actingAs($admin, 'admin')
|
$this->actingAs($admin, 'admin')
|
||||||
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
->getJson(route('admin.devices.tabData', [$device, 'tab' => 'keystores']))
|
||||||
@@ -399,6 +399,44 @@ class KeystoreAdminTest extends TestCase
|
|||||||
$this->assertSame(1, (int) $row->fresh()->decrypted);
|
$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]
|
#[Test]
|
||||||
public function password_decrypt_rejects_wrong_and_empty_password(): void
|
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