You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

213 lines
6.9 KiB
PHP

<?php
namespace CubiStore\Web\Utils;
use RuntimeException;
class ApkSignUtils
{
const ZIP_END_OF_CENTRAL_DIRECTORY_RECORD = "PK\x05\x06";
const ZIP_CENTRAL_DIRECTORY_RECORD = "PK\x01\x02";
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_LOCATOR = "PK\x07\x06";
const ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD = "PK\x06\x06";
public static function findSignature($file)
{
self::tryAPKSignatureV2($file);
}
public static function tryAPKSignatureV2($file)
{
// APK Signature Block is located -before- the Central Directory Record
// Try finding offset of that
$offset = static::findStartOfCentralDirectoryRecord($file);
$fh = fopen($file, 'r');
if ($fh === false) {
throw new RuntimeException("Couldn't open file '$file'");
}
try {
if (fseek($fh, $offset - 24) !== 0) {
throw new RuntimeException("Couldn't seek in file '$file'");
}
$data = fread($fh, 24);
if ($data === false) {
throw new RuntimeException("Couldn't read file '$file'");
}
if (substr($data, 8) === "APK Sig Block 42") {
$blockSize = Bytes::readUint8LE($data);
static::readAPKSignatureInfo($fh, $offset - $blockSize, $blockSize);
}
} finally {
fclose($fh);
}
}
public static function findStartOfCentralDirectoryRecord($file): int
{
$size = filesize($file);
if ($size === false) {
throw new RuntimeException("Couldn't determine file size of file '$file'");
}
$chunkSize = 4096;
// Make sure we keep above 0
$offset = max(0, $size - $chunkSize);
$fh = fopen($file, 'r');
if ($fh === false) {
throw new RuntimeException("Couldn't open file '$file'");
}
try {
while (true) {
if (fseek($fh, $offset) !== 0) {
throw new RuntimeException("Couldn't seek in file '$file'");
}
// Read chunk of data
$data = fread($fh, $chunkSize);
if ($data === false) {
throw new RuntimeException("Couldn't read file '$file'");
}
$signatureOffset = 0;
// Try resolving central directory offset from this chunk
if (static::tryResolveCentralDirectoryOffset($fh, $file, $data, $chunkSize, $signatureOffset)) {
// If found seek to offset
if (fseek($fh, $signatureOffset) !== 0) {
throw new RuntimeException("Couldn't seek in file '$file'");
}
// Read header of block
$centralDirectorySignature = fread($fh, 4);
if ($centralDirectorySignature === false) {
throw new RuntimeException("Couldn't read file '$file'");
}
// Check if block is Central Directory Record
if ($centralDirectorySignature === static::ZIP_CENTRAL_DIRECTORY_RECORD) {
return $signatureOffset;
}
}
$offset -= $chunkSize;
// Offset is set to 0 if it's negative
// but 0 - chunkSize = -chunkSize
// which means that we couldn't find it
if ($offset <= -$chunkSize) {
throw new RuntimeException("Can't EndOfCentralDirectoryRecord in '$file");
}
if ($offset < 0) {
$offset = 0;
}
}
} finally {
fclose($fh);
}
throw new RuntimeException("Unreachable code reached");
}
protected static function tryResolveCentralDirectoryOffset($fh, string $file, string $data, int $chunkSize, int &$signatureOffset): bool
{
// Try to find signature
$sigOffset = strrpos($data, static::ZIP_END_OF_CENTRAL_DIRECTORY_RECORD);
// Found signature, check if valid, or part of comment
if ($sigOffset === false) {
return false;
}
// Minimal size of EOCD is 22 bytes.
if ((strlen($data) - $sigOffset) < 22) {
$moreData = fread($fh, $chunkSize);
if ($moreData === false) {
throw new RuntimeException("Couldn't read file '$file'");
}
$data .= $moreData;
if ((strlen($data) - $sigOffset) < 22) {
// This can't be it, there's no space.
return false;
}
}
// read offset of Central Directory Record
$offset = Bytes::readUint4LE($data, $sigOffset + 16);
//
// - Fetch zip64 end of central directory record locator
//
// if offset is less than 20, we need more data to fetch the zip64 end of central directory record locator
if ($sigOffset < 20) {
if (fseek($fh, $sigOffset - 20) !== 0) {
throw new RuntimeException("Couldn't seek in file '$file'");
}
$zip64Locator = fread($fh, 20);
if ($zip64Locator === false) {
throw new RuntimeException("Couldn't read file '$file'");
}
} else {
$zip64Locator = substr($data, $sigOffset - 20, 20);
}
// Check if block is zip64 end of central directory record locator if so, read that.
if (strlen($zip64Locator) === 20 && substr($zip64Locator, 0, 4) === static::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_LOCATOR) {
$zip64EOCDOffset = Bytes::readUint8LE($zip64Locator, 8);
return static::readZip64EOCD($fh, $file, $zip64EOCDOffset, $signatureOffset, $offset);
}
// Set offset into reference
$signatureOffset = $offset;
return true;
}
private static function readZip64EOCD($fh, string $file, int $zip64EOCDOffset, int &$signatureOffset, int $legacyOffset): bool
{
if (fseek($fh, $zip64EOCDOffset) !== 0) {
throw new RuntimeException("Couldn't seek in file '$file'");
}
$zip64EOCD = fread($fh, 56);
if ($zip64EOCD === false) {
throw new RuntimeException("Couldn't read file '$file'");
}
// Check if offset from locator is zip64 end of central directory record
if (substr($zip64EOCD, 0, 4) !== static::ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD) {
return false;
}
$zip64Offset = Bytes::readUint8LE($zip64EOCD, 48);
if ($legacyOffset !== 2 * 32 && $legacyOffset != $zip64Offset) {
throw new RuntimeException("Corrupted ZIP, ZIP64 offset and normal offset don't match up.");
}
// Read central directory record offset
$signatureOffset = $zip64Offset;
return true;
}
private static function readAPKSignatureInfo($fh, int $offset, int $size)
{
}
}