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.
218 lines
6.6 KiB
PHP
218 lines
6.6 KiB
PHP
<?php
|
|
|
|
|
|
namespace CubiStore\Web\Service;
|
|
|
|
use CubiStore\Web\Model\App;
|
|
use DI\Container;
|
|
use Doctrine\ORM\EntityManager;
|
|
use RuntimeException;
|
|
use ZipArchive;
|
|
|
|
class FDroidRepoService
|
|
{
|
|
/**
|
|
* @var Container
|
|
*/
|
|
private $container;
|
|
|
|
/**
|
|
* FDroidRepoService constructor.
|
|
* @param Container $container
|
|
*/
|
|
public function __construct(Container $container)
|
|
{
|
|
$this->container = $container;
|
|
}
|
|
|
|
public function createIndexV1Array()
|
|
{
|
|
/** @var EntityManager $em */
|
|
$em = $this->container->get(EntityManager::class);
|
|
|
|
/** @var App[] $apps */
|
|
$apps = $em
|
|
->getRepository(App::class)
|
|
->findBy([
|
|
'status' => App::STATUS_APPROVED
|
|
]);
|
|
|
|
$appObjects = [];
|
|
$packageObjects = [];
|
|
|
|
/** @var App $app */
|
|
foreach ($apps as $app) {
|
|
$appObjects[] = [
|
|
"authorEmail" => "example@example.com",
|
|
"authorName" => "example",
|
|
"authorWebSite" => "https://example.org",
|
|
"categories" => [
|
|
'example'
|
|
],
|
|
"suggestedVersionCode" => $app->getReleases()->first()->getVersionCode(),
|
|
"name" => $app->getLabel(),
|
|
"added" => 0,
|
|
"packageName" => $app->getName(),
|
|
"lastUpdated" => 0,
|
|
"license" => "no"
|
|
];
|
|
|
|
$packages = [];
|
|
|
|
foreach ($app->getReleases() as $release) {
|
|
$packages[] = [
|
|
"added" => 0,
|
|
"apkName" => "/apk/" . $app->getName() . "/" . $release->getVersionCode() . ".apk",
|
|
"hash" => hash('sha256', $release->getApk()),
|
|
"hashType" => "sha256",
|
|
"minSdkVersion" => 4,
|
|
"packageName" => $app->getName(),
|
|
"sig" => "deadbeef",
|
|
"signer" => "deadbeef",
|
|
"size" => filesize($release->getApk()),
|
|
"targetSdkVersion" => 28,
|
|
"versionCode" => $release->getVersionCode(),
|
|
"versionName" => $release->getVersionName()
|
|
];
|
|
}
|
|
|
|
$packageObjects[$app->getName()] = $packages;
|
|
}
|
|
|
|
return [
|
|
'repo' => [
|
|
'timestamp' => time() * 1000,
|
|
'version' => 21,
|
|
'maxage' => 14,
|
|
'name' => $this->container->get('app.name'),
|
|
'description' => $this->container->get('app.description'),
|
|
'address' => "https://" . $this->container->get('app.domain') . '/repo'
|
|
],
|
|
'requests' => [
|
|
'install' => [],
|
|
'uninstall' => [],
|
|
],
|
|
"apps" => $appObjects,
|
|
"packages" => $packageObjects
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
{
|
|
$zip = new ZipArchive();
|
|
$zipPath = tempnam(sys_get_temp_dir(), 'zip');
|
|
if ($zipPath === false) {
|
|
throw new RuntimeException("Failed to create temp file");
|
|
}
|
|
|
|
$fileDigest = hash('sha256', $contents, true);
|
|
$zip->open($zipPath, ZipArchive::CREATE);
|
|
|
|
$fileHeader = 'Name: ' . $file . "\n";
|
|
$fileHeader .= 'SHA-256-Digest: ' . base64_encode($fileDigest) . "\n\n";
|
|
|
|
$fileHeaderDigest = hash('sha256', $fileHeader, true);
|
|
|
|
$manifest = "Manifest-Version: 1.0\n";
|
|
$manifest .= "Created-By: CubiStore\n\n";
|
|
$manifestHeaderDigest = hash('sha256', $manifest, true);
|
|
|
|
$manifest .= $fileHeader;
|
|
$manifestDigest = hash('sha256', $manifest, true);
|
|
|
|
$fileManifest = "Signature-Version: 1.0\n";
|
|
$fileManifest .= "SHA-256-Digest-Manifest-Main-Attributes: " . base64_encode($manifestHeaderDigest) . "\n";
|
|
$fileManifest .= "SHA-256-Digest-Manifest: " . base64_encode($manifestDigest) . "\n";
|
|
$fileManifest .= "Created-By: CubiStore\n\n";
|
|
$fileManifest .= "Name: " . $file . "\n";
|
|
$fileManifest .= "SHA-256-Digest: " . base64_encode($fileHeaderDigest) . "\n\n";
|
|
|
|
$zip->addFromString('META-INF/MANIFEST.MF', $manifest);
|
|
$zip->addFromString('META-INF/1.SF', $fileManifest);
|
|
|
|
$in = tempnam(sys_get_temp_dir(), 'repo');
|
|
if ($in === false) {
|
|
throw new RuntimeException("Failed to create temp file");
|
|
}
|
|
|
|
$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(
|
|
$in,
|
|
$out,
|
|
'file://' . __DIR__ . '/../../certs/cert.pem',
|
|
'file://' . __DIR__ . '/../../certs/key.pem',
|
|
[],
|
|
PKCS7_BINARY | PKCS7_NOATTR
|
|
);
|
|
|
|
if (!$success) {
|
|
$error = '';
|
|
while ($line = openssl_error_string()) {
|
|
$error .= $line . "\n";
|
|
}
|
|
|
|
throw new RuntimeException($error);
|
|
}
|
|
|
|
unlink($in);
|
|
$signed = file_get_contents($out);
|
|
unlink($out);
|
|
|
|
if ($signed === false) {
|
|
throw new RuntimeException("Signature wasn't created");
|
|
}
|
|
|
|
$contentOffset = strpos($signed, "\n\n");
|
|
if ($contentOffset === false) {
|
|
throw new RuntimeException("Signature is not as expected");
|
|
}
|
|
|
|
$content = substr($signed, $contentOffset);
|
|
$base64 = str_replace("\n", "", $content);
|
|
$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->close();
|
|
$zipContent = file_get_contents($zipPath);
|
|
unlink($zipPath);
|
|
|
|
if ($zipContent === false) {
|
|
return new RuntimeException("Failed to read resulting jar");
|
|
}
|
|
|
|
return $zipContent;
|
|
}
|
|
}
|