few updates
Some checks reported errors
continuous-integration/drone/push Build encountered an error
Some checks reported errors
continuous-integration/drone/push Build encountered an error
This commit is contained in:
parent
d16ccd468c
commit
0950740480
21 changed files with 1983 additions and 16 deletions
29
.drone.yml
29
.drone.yml
|
@ -1,12 +1,37 @@
|
||||||
|
---
|
||||||
name: default
|
name: default
|
||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: code checks
|
- name: setup
|
||||||
image: composer
|
image: composer
|
||||||
|
volumes:
|
||||||
|
- name: composer
|
||||||
|
path: /composer
|
||||||
|
environment:
|
||||||
|
COMPOSER_HOME: /composer
|
||||||
commands:
|
commands:
|
||||||
- composer install --no-ansi --no-interaction
|
- composer install --no-ansi --no-interaction
|
||||||
- ./bin/setup
|
- ./bin/setup
|
||||||
|
|
||||||
|
- name: code styling
|
||||||
|
image: php
|
||||||
|
commands:
|
||||||
- ./vendor/bin/php-cs-fixer fix --dry-run src
|
- ./vendor/bin/php-cs-fixer fix --dry-run src
|
||||||
- ./vendor/bin/phpstan analyse -a config/autoload_with_route.php src
|
- name: static analysis
|
||||||
|
image: php
|
||||||
|
commands:
|
||||||
|
- ./vendor/bin/phpstan analyse -l max -a config/autoload_with_route.php src
|
||||||
|
- name: testing
|
||||||
|
image: php
|
||||||
|
commands:
|
||||||
|
- ./vendor/bin/phpunit
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: composer
|
||||||
|
host:
|
||||||
|
path: composer
|
||||||
|
---
|
||||||
|
kind: signature
|
||||||
|
hmac: 9dee19d61430e7e305581f66554fbefc139db4b0e7bc9eda1c7fdcd57a6c5ddc
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,5 +1,7 @@
|
||||||
/vendor/
|
/vendor/
|
||||||
/var
|
/var/*
|
||||||
|
!/var/testdata
|
||||||
/certs
|
/certs
|
||||||
/.idea/
|
/.idea/
|
||||||
.php_cs.cache
|
.php_cs.cache
|
||||||
|
.phpunit.result.cache
|
||||||
|
|
|
@ -14,11 +14,14 @@
|
||||||
"doctrine/migrations": "^2.2",
|
"doctrine/migrations": "^2.2",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"ext-simplexml": "*",
|
"ext-simplexml": "*",
|
||||||
"ext-openssl": "*"
|
"ext-openssl": "*",
|
||||||
|
"phpunit/phpunit": "^8.4"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "^0.11.19",
|
"phpstan/phpstan": "^0.11.19",
|
||||||
"friendsofphp/php-cs-fixer": "^2.16"
|
"friendsofphp/php-cs-fixer": "^2.16",
|
||||||
|
"phpstan/phpstan-doctrine": "^0.11.6",
|
||||||
|
"phpstan/extension-installer": "^1.0"
|
||||||
},
|
},
|
||||||
"license": "GPLv3",
|
"license": "GPLv3",
|
||||||
"authors": [
|
"authors": [
|
||||||
|
@ -31,5 +34,10 @@
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"CubiStore\\Web\\": "src/"
|
"CubiStore\\Web\\": "src/"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"CubiStore\\Tests\\Web\\": "tests/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1513
composer.lock
generated
1513
composer.lock
generated
File diff suppressed because it is too large
Load diff
12
phpunit.xml
Normal file
12
phpunit.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<phpunit
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.3/phpunit.xsd"
|
||||||
|
bootstrap="config/autoload_with_route.php"
|
||||||
|
>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="suite">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
|
@ -42,6 +42,11 @@ class AppManager
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmp = tempnam(sys_get_temp_dir(), 'apk-');
|
$tmp = tempnam(sys_get_temp_dir(), 'apk-');
|
||||||
|
|
||||||
|
if ($tmp === false) {
|
||||||
|
throw new \RuntimeException("Failed creating temp file");
|
||||||
|
}
|
||||||
|
|
||||||
$apkUpload->moveTo($tmp);
|
$apkUpload->moveTo($tmp);
|
||||||
|
|
||||||
$apk = new Apk($tmp);
|
$apk = new Apk($tmp);
|
||||||
|
|
|
@ -13,7 +13,7 @@ class Repo
|
||||||
$jsonArr = $repoService->createIndexV1Array();
|
$jsonArr = $repoService->createIndexV1Array();
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->getBody()->write(json_encode($jsonArr));
|
->getBody()->write((string)json_encode($jsonArr));
|
||||||
|
|
||||||
return $response
|
return $response
|
||||||
->withHeader('Content-Type', 'application/json');
|
->withHeader('Content-Type', 'application/json');
|
||||||
|
@ -24,6 +24,10 @@ class Repo
|
||||||
$jsonArr = $repoService->createIndexV1Array();
|
$jsonArr = $repoService->createIndexV1Array();
|
||||||
$jsonFile = json_encode($jsonArr);
|
$jsonFile = json_encode($jsonArr);
|
||||||
|
|
||||||
|
if ($jsonFile === false) {
|
||||||
|
throw new \RuntimeException("Failed creating repo");
|
||||||
|
}
|
||||||
|
|
||||||
$jar = $repoService->createSigned('index-v1.json', $jsonFile);
|
$jar = $repoService->createSigned('index-v1.json', $jsonFile);
|
||||||
$response->getBody()->write($jar);
|
$response->getBody()->write($jar);
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,44 @@ class Release
|
||||||
*/
|
*/
|
||||||
private $id;
|
private $id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \DateTime
|
||||||
|
*/
|
||||||
|
private $added;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var App
|
* @var App
|
||||||
* @ORM\ManyToOne(targetEntity="App", inversedBy="releases")
|
* @ORM\ManyToOne(targetEntity="App", inversedBy="releases")
|
||||||
*/
|
*/
|
||||||
private $app;
|
private $app;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var integer
|
||||||
|
* @ORM\Column(type="integer")
|
||||||
|
*/
|
||||||
|
private $minSdkVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var integer
|
||||||
|
* @ORM\Column(type="integer")
|
||||||
|
*/
|
||||||
|
private $targetSdkVersion;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $hash;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $hashAlgorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var integer
|
||||||
|
*/
|
||||||
|
private $size;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string
|
* @var string
|
||||||
* @ORM\Column(type="string")
|
* @ORM\Column(type="string")
|
||||||
|
|
|
@ -6,6 +6,7 @@ namespace CubiStore\Web\Service;
|
||||||
use CubiStore\Web\Model\App;
|
use CubiStore\Web\Model\App;
|
||||||
use DI\Container;
|
use DI\Container;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use RuntimeException;
|
||||||
use ZipArchive;
|
use ZipArchive;
|
||||||
|
|
||||||
class FDroidRepoService
|
class FDroidRepoService
|
||||||
|
@ -29,6 +30,7 @@ class FDroidRepoService
|
||||||
/** @var EntityManager $em */
|
/** @var EntityManager $em */
|
||||||
$em = $this->container->get(EntityManager::class);
|
$em = $this->container->get(EntityManager::class);
|
||||||
|
|
||||||
|
/** @var App[] $apps */
|
||||||
$apps = $em
|
$apps = $em
|
||||||
->getRepository(App::class)
|
->getRepository(App::class)
|
||||||
->findBy([
|
->findBy([
|
||||||
|
@ -38,8 +40,8 @@ class FDroidRepoService
|
||||||
$appObjects = [];
|
$appObjects = [];
|
||||||
$packageObjects = [];
|
$packageObjects = [];
|
||||||
|
|
||||||
|
/** @var App $app */
|
||||||
foreach ($apps as $app) {
|
foreach ($apps as $app) {
|
||||||
/** @var $app App */
|
|
||||||
$appObjects[] = [
|
$appObjects[] = [
|
||||||
"authorEmail" => "example@example.com",
|
"authorEmail" => "example@example.com",
|
||||||
"authorName" => "example",
|
"authorName" => "example",
|
||||||
|
@ -95,12 +97,36 @@ class FDroidRepoService
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create's a signed jar with a single file.
|
||||||
|
*
|
||||||
|
* A jar is a zip file.
|
||||||
|
* Contains the following files:
|
||||||
|
*
|
||||||
|
* META-INF/MANIFEST.MF
|
||||||
|
* Contains manifest header
|
||||||
|
* Contains header (and hashes) for files
|
||||||
|
*
|
||||||
|
* META-INF/1.SF
|
||||||
|
* Contains hash of full manifest, manifest header and file header
|
||||||
|
*
|
||||||
|
* META-INF/1.RSA
|
||||||
|
* PKCS7 Signature of META-INF/1.SF
|
||||||
|
*
|
||||||
|
* @param string $file The filename of the file to add in the jar
|
||||||
|
* @param string $contents The contents of the file to add in the jar
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
public function createSigned($file, $contents)
|
public function createSigned($file, $contents)
|
||||||
{
|
{
|
||||||
$zip = new ZipArchive();
|
$zip = new ZipArchive();
|
||||||
$zipPath = tempnam(sys_get_temp_dir(), 'zip');
|
$zipPath = tempnam(sys_get_temp_dir(), 'zip');
|
||||||
$zip->open($zipPath, ZipArchive::CREATE);
|
if ($zipPath === false) {
|
||||||
|
throw new RuntimeException("Failed to create temp file");
|
||||||
|
}
|
||||||
|
|
||||||
$fileDigest = hash('sha256', $contents, true);
|
$fileDigest = hash('sha256', $contents, true);
|
||||||
|
$zip->open($zipPath, ZipArchive::CREATE);
|
||||||
|
|
||||||
$fileHeader = 'Name: ' . $file . "\n";
|
$fileHeader = 'Name: ' . $file . "\n";
|
||||||
$fileHeader .= 'SHA-256-Digest: ' . base64_encode($fileDigest) . "\n\n";
|
$fileHeader .= 'SHA-256-Digest: ' . base64_encode($fileDigest) . "\n\n";
|
||||||
|
@ -125,8 +151,18 @@ class FDroidRepoService
|
||||||
$zip->addFromString('META-INF/1.SF', $fileManifest);
|
$zip->addFromString('META-INF/1.SF', $fileManifest);
|
||||||
|
|
||||||
$in = tempnam(sys_get_temp_dir(), 'repo');
|
$in = tempnam(sys_get_temp_dir(), 'repo');
|
||||||
file_put_contents($in, $fileManifest);
|
if ($in === false) {
|
||||||
|
throw new RuntimeException("Failed to create temp file");
|
||||||
|
}
|
||||||
|
|
||||||
$out = tempnam(sys_get_temp_dir(), 'repo');
|
$out = tempnam(sys_get_temp_dir(), 'repo');
|
||||||
|
if ($out === false) {
|
||||||
|
throw new RuntimeException("Failed to create temp file");
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents($in, $fileManifest);
|
||||||
|
|
||||||
|
|
||||||
$success = openssl_pkcs7_sign(
|
$success = openssl_pkcs7_sign(
|
||||||
$in,
|
$in,
|
||||||
$out,
|
$out,
|
||||||
|
@ -142,22 +178,40 @@ class FDroidRepoService
|
||||||
$error .= $line . "\n";
|
$error .= $line . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new \RuntimeException($error);
|
throw new RuntimeException($error);
|
||||||
}
|
}
|
||||||
|
|
||||||
unlink($in);
|
unlink($in);
|
||||||
$signed = file_get_contents($out);
|
$signed = file_get_contents($out);
|
||||||
unlink($out);
|
unlink($out);
|
||||||
|
|
||||||
|
if ($signed === false) {
|
||||||
|
throw new RuntimeException("Signature wasn't created");
|
||||||
|
}
|
||||||
|
|
||||||
$contentOffset = strpos($signed, "\n\n");
|
$contentOffset = strpos($signed, "\n\n");
|
||||||
|
if ($contentOffset === false) {
|
||||||
|
throw new RuntimeException("Signature is not as expected");
|
||||||
|
}
|
||||||
|
|
||||||
$content = substr($signed, $contentOffset);
|
$content = substr($signed, $contentOffset);
|
||||||
$base64 = str_replace("\n", "", $content);
|
$base64 = str_replace("\n", "", $content);
|
||||||
$zip->addFromString('META-INF/1.RSA', base64_decode($base64));
|
$der = base64_decode($base64);
|
||||||
|
|
||||||
|
if ($der === false) {
|
||||||
|
throw new RuntimeException("Failed to decode DER from signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->addFromString('META-INF/1.RSA', $der);
|
||||||
$zip->addFromString($file, $contents);
|
$zip->addFromString($file, $contents);
|
||||||
$zip->close();
|
$zip->close();
|
||||||
$zipContent = file_get_contents($zipPath);
|
$zipContent = file_get_contents($zipPath);
|
||||||
unlink($zipPath);
|
unlink($zipPath);
|
||||||
|
|
||||||
|
if ($zipContent === false) {
|
||||||
|
return new RuntimeException("Failed to read resulting jar");
|
||||||
|
}
|
||||||
|
|
||||||
return $zipContent;
|
return $zipContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
src/Utils/APKSignatureInfo.php
Normal file
18
src/Utils/APKSignatureInfo.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace CubiStore\Web\Utils;
|
||||||
|
|
||||||
|
class APKSignatureInfo
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Which version of signature is used 1, 2, and 3 exist
|
||||||
|
* 1. uses JAR Signing
|
||||||
|
* 2. uses APK Signature Scheme
|
||||||
|
* 3. extended APK Signature Scheme
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $version;
|
||||||
|
public $signer;
|
||||||
|
public $signature;
|
||||||
|
}
|
|
@ -29,7 +29,17 @@ class Apk
|
||||||
{
|
{
|
||||||
// Get first 4 bytes
|
// Get first 4 bytes
|
||||||
$fh = fopen($this->file, 'r');
|
$fh = fopen($this->file, 'r');
|
||||||
|
|
||||||
|
if ($fh === false) {
|
||||||
|
throw new \RuntimeException("Failed to open file '{$this->file}'");
|
||||||
|
}
|
||||||
|
|
||||||
$header = fread($fh, 4);
|
$header = fread($fh, 4);
|
||||||
|
|
||||||
|
if ($header === false) {
|
||||||
|
throw new \RuntimeException("Failed to read file '{$this->file}'");
|
||||||
|
}
|
||||||
|
|
||||||
fclose($fh);
|
fclose($fh);
|
||||||
|
|
||||||
// Check if uploaded file is a valid ZIP file
|
// Check if uploaded file is a valid ZIP file
|
||||||
|
@ -49,7 +59,13 @@ class Apk
|
||||||
{
|
{
|
||||||
if ($this->manifest === null) {
|
if ($this->manifest === null) {
|
||||||
$xml = $this->cmd('androguard', 'axml', '-o', '/dev/stdout', $this->file);
|
$xml = $this->cmd('androguard', 'axml', '-o', '/dev/stdout', $this->file);
|
||||||
$this->manifest = simplexml_load_string($xml);
|
$simpleXml = simplexml_load_string($xml);
|
||||||
|
|
||||||
|
if ($simpleXml === false) {
|
||||||
|
throw new \RuntimeException("Failed to parse AndroidManifest.xml from '{$this->file}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->manifest = $simpleXml;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manifest;
|
return $this->manifest;
|
||||||
|
@ -89,7 +105,12 @@ class Apk
|
||||||
|
|
||||||
private function cmd(string $cmd, string... $args): string
|
private function cmd(string $cmd, string... $args): string
|
||||||
{
|
{
|
||||||
$proc = popen($cmd . ' ' . implode(' ', array_map('escapeshellarg', $args)), 'r');
|
$fullCommand = $cmd . ' ' . implode(' ', array_map('escapeshellarg', $args));
|
||||||
|
$proc = popen($fullCommand, 'r');
|
||||||
|
if ($proc === false) {
|
||||||
|
throw new \RuntimeException("Failed running: $fullCommand");
|
||||||
|
}
|
||||||
|
|
||||||
$data = '';
|
$data = '';
|
||||||
while ($text = fread($proc, 1024)) {
|
while ($text = fread($proc, 1024)) {
|
||||||
$data .= $text;
|
$data .= $text;
|
||||||
|
|
212
src/Utils/ApkSignUtils.php
Normal file
212
src/Utils/ApkSignUtils.php
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
<?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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
36
src/Utils/Bytes.php
Normal file
36
src/Utils/Bytes.php
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace CubiStore\Web\Utils;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
class Bytes
|
||||||
|
{
|
||||||
|
public static function readUint2LE($bytes, $offset = 0)
|
||||||
|
{
|
||||||
|
return static::unpackFirst('v', $bytes, $offset, 'int2');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function readUint4LE($bytes, $offset = 0)
|
||||||
|
{
|
||||||
|
return static::unpackFirst('V', $bytes, $offset, 'int4');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function readUint8LE($bytes, $offset = 0)
|
||||||
|
{
|
||||||
|
return static::unpackFirst('P', $bytes, $offset, 'int8');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static function unpackFirst($format, $data, $offset, $what = 'bytes')
|
||||||
|
{
|
||||||
|
$result = unpack($format, $data, $offset);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
throw new RuntimeException("Failed reading $what from buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
return reset($result);
|
||||||
|
}
|
||||||
|
}
|
|
@ -113,6 +113,11 @@ class Route
|
||||||
if ($this->type === self::TYPE_GROUP) {
|
if ($this->type === self::TYPE_GROUP) {
|
||||||
$name = "\$group" . strlen($this->fullPath);
|
$name = "\$group" . strlen($this->fullPath);
|
||||||
$code = str_repeat(' ', $indent) . "${var}->group(" . $this->json($this->path) . ", function (RouteCollectorProxy $name) {\n";
|
$code = str_repeat(' ', $indent) . "${var}->group(" . $this->json($this->path) . ", function (RouteCollectorProxy $name) {\n";
|
||||||
|
|
||||||
|
if ($this->children === null) {
|
||||||
|
$this->children = [];
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->children as $child) {
|
foreach ($this->children as $child) {
|
||||||
$code .= $child->compileRoute($name, $indent + 4) . "\n";
|
$code .= $child->compileRoute($name, $indent + 4) . "\n";
|
||||||
}
|
}
|
||||||
|
@ -121,6 +126,11 @@ class Route
|
||||||
|
|
||||||
if ($this->type === self::TYPE_ROOT) {
|
if ($this->type === self::TYPE_ROOT) {
|
||||||
$code = '';
|
$code = '';
|
||||||
|
|
||||||
|
if ($this->children === null) {
|
||||||
|
$this->children = [];
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->children as $child) {
|
foreach ($this->children as $child) {
|
||||||
$code .= $child->compileRoute($var, $indent) . "\n";
|
$code .= $child->compileRoute($var, $indent) . "\n";
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,8 @@ class RouteCompiler
|
||||||
|
|
||||||
foreach ($routes as $route => $details) {
|
foreach ($routes as $route => $details) {
|
||||||
if (is_array($details)) {
|
if (is_array($details)) {
|
||||||
$route = Route::group($target, $route);
|
$routeObj = Route::group($target, $route);
|
||||||
$this->addRoutes($details, $route);
|
$this->addRoutes($details, $routeObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_string($details)) {
|
if (!is_string($details)) {
|
||||||
|
|
17
tests/Utils/ApkSignUtilsTest.php
Normal file
17
tests/Utils/ApkSignUtilsTest.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace CubiStore\Tests\Web\Utils;
|
||||||
|
|
||||||
|
|
||||||
|
use CubiStore\Web\Utils\ApkSignUtils;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ApkSignUtilsTest extends TestCase
|
||||||
|
{
|
||||||
|
function testSha1Signature()
|
||||||
|
{
|
||||||
|
$utils = new ApkSignUtils();
|
||||||
|
$utils->findSignature(__DIR__ . '/../../var/testdata/signedapks/apksignv2.apk');
|
||||||
|
}
|
||||||
|
}
|
BIN
var/testdata/signedapks/apksignv2.apk
vendored
Normal file
BIN
var/testdata/signedapks/apksignv2.apk
vendored
Normal file
Binary file not shown.
BIN
var/testdata/signedapks/oldgms.apk
vendored
Normal file
BIN
var/testdata/signedapks/oldgms.apk
vendored
Normal file
Binary file not shown.
BIN
var/testdata/signedapks/sha1.apk
vendored
Normal file
BIN
var/testdata/signedapks/sha1.apk
vendored
Normal file
Binary file not shown.
BIN
var/testdata/signedapks/sha256.apk
vendored
Normal file
BIN
var/testdata/signedapks/sha256.apk
vendored
Normal file
Binary file not shown.
BIN
var/testdata/signedapks/zip64.jar
vendored
Normal file
BIN
var/testdata/signedapks/zip64.jar
vendored
Normal file
Binary file not shown.
Loading…
Reference in a new issue