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.
web/src/Service/FDroidRepoService.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;
}
}