Files
coruna-lab/server/app/Services/CorunaArchive.php
T
hashbro ea0be57180 init
2026-08-04 05:00:42 +08:00

117 lines
3.8 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Process;
use RuntimeException;
/**
* Repair Coruna obfuscated 7z headers and extract with p7zip.
*/
class CorunaArchive
{
private const STANDARD_PREFIX = "7z\xBC\xAF'\x1C";
private const HEADER_XOR = 0x1234567800ABCDEF;
private const HEADER_MARKER_1 = 0x000A000900010804;
private const HEADER_MARKER_2 = 0x009812000B0F0D0C;
public function __construct(
private readonly CorunaCrypto $crypto,
private readonly string $sevenZip = '/opt/homebrew/opt/p7zip/bin/7z',
) {}
public function isCorunaHeader(string $data): bool
{
if (strlen($data) < 32) {
return false;
}
if (str_starts_with($data, self::STANDARD_PREFIX)) {
return false;
}
$m1 = unpack('P', substr($data, 16, 8))[1];
$m2 = unpack('P', substr($data, 24, 8))[1];
return $m1 === self::HEADER_MARKER_1 && $m2 === self::HEADER_MARKER_2;
}
public function repairHeader(string $data): string
{
if (str_starts_with($data, self::STANDARD_PREFIX)) {
return $data;
}
if (strlen($data) < 33 || ! $this->isCorunaHeader($data)) {
throw new RuntimeException('not a Coruna header-obfuscated 7z archive');
}
$nextHeaderOffset = unpack('P', substr($data, 0, 8))[1] ^ self::HEADER_XOR;
$nextHeaderSize = unpack('P', substr($data, 8, 8))[1] ^ self::HEADER_XOR;
$nextHeaderStart = 32 + $nextHeaderOffset;
$nextHeaderEnd = $nextHeaderStart + $nextHeaderSize;
if ($nextHeaderSize === 0 || $nextHeaderEnd > strlen($data)) {
throw new RuntimeException('invalid Coruna 7z bounds');
}
$repaired = $data;
$repaired = substr_replace($repaired, self::STANDARD_PREFIX."\x00\x04", 0, 8);
$repaired = substr_replace($repaired, pack('P', $nextHeaderOffset), 12, 8);
$repaired = substr_replace($repaired, pack('P', $nextHeaderSize), 20, 8);
$nextCrc = crc32(substr($repaired, $nextHeaderStart, $nextHeaderSize)) & 0xFFFFFFFF;
$repaired = substr_replace($repaired, pack('V', $nextCrc), 28, 4);
$startCrc = crc32(substr($repaired, 12, 20)) & 0xFFFFFFFF;
$repaired = substr_replace($repaired, pack('V', $startCrc), 8, 4);
return $repaired;
}
public function extract(string $wireData, string $destDir, string $batchBase = '0'): array
{
if (! is_dir($destDir)) {
mkdir($destDir, 0755, true);
}
try {
$repaired = $this->repairHeader($wireData);
} catch (\Throwable $e) {
$repaired = $wireData;
}
$archive = $destDir.'/capture.7z';
file_put_contents($archive, $repaired);
file_put_contents($destDir.'/wire.bin', $wireData);
$password = $this->crypto->archivePassword($batchBase);
$membersDir = $destDir.'/members';
@mkdir($membersDir, 0755, true);
$bin = is_executable($this->sevenZip) ? $this->sevenZip : '7z';
$result = Process::timeout(120)->run([
$bin, 'x', '-y',
'-p'.$password,
'-o'.$membersDir,
$archive,
]);
$files = [];
if (is_dir($membersDir)) {
$it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(
$membersDir,
\FilesystemIterator::SKIP_DOTS
));
foreach ($it as $file) {
if ($file->isFile()) {
$files[] = $file->getPathname();
}
}
}
return [
'ok' => $result->successful() && count($files) > 0,
'stderr' => $result->errorOutput(),
'files' => $files,
'password_recipe' => 'session_key||'.$batchBase,
];
}
}