/** * Do the update check, * * 1. Update all cabins * 2. Update all gadgets * 3. Update the core */ public function doUpdateCheck() { $config = State::instance(); // First, update each cabin foreach ($this->getCabins() as $cabin) { if ($cabin instanceof CabinUpdater) { $cabin->autoUpdate(); } } // Next, update each gadget foreach ($this->getGadgets() as $gadget) { if ($gadget instanceof GadgetUpdater) { $gadget->autoUpdate(); } } // Also, motifs: foreach ($this->getMotifs() as $motif) { if ($motif instanceof MotifUpdater) { $motif->autoUpdate(); } } // Finally, let's update the core $s = $config->universal['airship']['trusted-supplier']; if (!empty($s)) { $ha = new AirshipUpdater($this->hail, $this->getSupplier($s)); $ha->autoUpdate(); } }
/** * Get all URLs * * @param string $suffix * @param bool $doNotShuffle * @return string[] */ public function getAllURLs(string $suffix = '', bool $doNotShuffle = false) : array { $state = State::instance(); $candidates = []; if ($state->universal['tor-only']) { // Prioritize Tor Hidden Services $after = []; foreach ($this->urls as $url) { if (\Airship\isOnionUrl($url)) { $candidates[] = $url . $suffix; } else { $after[] = $url . $suffix; } } if (!$doNotShuffle) { \Airship\secure_shuffle($candidates); \Airship\secure_shuffle($after); } foreach ($after as $url) { $candidates[] = $url . $suffix; } } else { $candidates = $this->urls; if (!$doNotShuffle) { \Airship\secure_shuffle($candidates); } foreach (\array_keys($candidates) as $i) { $candidates[$i] .= $suffix; } } return $candidates; }
/** * This function is called after the dependencies have been injected by * AutoPilot. Think of it as a user-land constructor. */ public function airshipLand() { parent::airshipLand(); $config = State::instance(); if (empty($config->universal['notary']['enabled'])) { \Airship\json_response(['status' => 'error', 'message' => 'This Airship does not offer Notary services.']); } $this->sk = $config->keyring['notary.online_signing_key']; $this->pk = $this->sk->derivePublicKey(); $this->channel = $config->universal['notary']['channel']; $this->chanUp = $this->blueprint('ChannelUpdates', $this->channel); }
/** * Log a message with a specific error level * * @param string $message * @param string $level * @param array $context * @return mixed */ public function log(string $message, string $level = LogLevel::ERROR, array $context = []) { if ($level === LogLevel::DEBUG) { $state = State::instance(); if (!$state->universal['debug']) { // Don't log debug messages unless debug mode is on: return null; } } $state = State::instance(); return $state->logger->log($level, $message, $context); }
declare (strict_types=1); use ParagonIE\Halite\KeyFactory; /** * This sets up the contents of our keyring. */ $key_management_closure = function () { if (!\is_dir(ROOT . '/config/keyring/')) { \mkdir(ROOT . '/config/keyring/', 0750); } $keyRing = \Airship\loadJSON(ROOT . '/config/keyring.json'); if (empty($keyRing)) { // This is critical to Airship's functioning. throw new \Error(\trk('errors.crypto.keyring_missing')); } $state = \Airship\Engine\State::instance(); $keys = []; foreach ($keyRing as $index => $keyConfig) { $path = ROOT . '/config/keyring/' . $keyConfig['file']; if (\file_exists($path)) { // Load it from disk switch ($keyConfig['type']) { case 'AuthenticationKey': $keys[$index] = KeyFactory::loadAuthenticationKey($path); break; case 'EncryptionKey': $keys[$index] = KeyFactory::loadEncryptionKey($path); break; case 'EncryptionPublicKey': $keys[$index] = KeyFactory::loadEncryptionPublicKey($path); break;
/** * This serves the fallback route, if it's defined. * * The fallback route handles: * * - Custom pages (if any exist), or * - Redirects * * @return mixed */ protected function serveFallback() { // If we're still here, let's try the fallback handler if (isset($this->cabin['data']['route_fallback'])) { \preg_match('#^/?' . self::$patternPrefix . '/(.*)$#', $_SERVER['REQUEST_URI'], $args); try { return $this->serve($this->cabin['data']['route_fallback'], \explode('/', $args[1] ?? '')); } catch (FallbackLoop $e) { $state = State::instance(); $state->logger->error('Missing route definition', ['exception' => \Airship\throwableToArray($e)]); // We only catch this one } } // If we don't have a fallback handler defined, just give a 404 status and kill the script. \http_response_code(404); exit(255); }
/** * TreeUpdate constructor. * * @param Channel $chan * @param array $updateData */ public function __construct(Channel $chan, array $updateData) { /** * This represents data from the base64urlsafe encoded JSON blob that is signed by the channel. */ $this->channelId = (int) $updateData['id']; $this->channelName = $chan->getName(); $this->merkleRoot = $updateData['root']; $this->stored = $updateData['stored']; $this->action = $this->stored['action']; $packageRelatedActions = [self::ACTION_CORE_UPDATE, self::ACTION_PACKAGE_UPDATE]; if (\in_array($this->action, $packageRelatedActions)) { // This is a package-related update: $this->checksum = $this->stored['checksum']; // This is the JSON message from the tree node, stored as an array: $data = \json_decode($updateData['data'], true); $this->updateMessage = $data; // What action are we performing? if ($this->action === self::ACTION_PACKAGE_UPDATE) { $this->packageType = $data['pkg_type']; $this->packageName = $data['name']; } else { $this->packageType = 'Core'; $this->packageName = 'Airship'; } if ($this->action === self::ACTION_CORE_UPDATE) { $state = State::instance(); $trustedSupplier = (string) ($state->universal['airship']['trusted-supplier'] ?? 'paragonie'); $this->supplier = $chan->getSupplier($trustedSupplier); } else { $this->supplier = $chan->getSupplier($data['supplier']); } } else { // This is a key-related update: if (!empty($updateData['master_signature'])) { $this->masterSig = $updateData['master_signature']; } $data = \json_decode($updateData['data'], true); try { $this->unpackMessageUpdate($chan, $data); } catch (NoSupplier $ex) { $this->isNewSupplier = true; $chan->createSupplier($data); $this->supplier = $chan->getSupplier($data['supplier']); } $this->keyType = $data['type']; $this->newPublicKey = $data['public_key']; } $this->supplierName = $this->supplier->getName(); }
/** * Add a new type to the Gears registry * * @param string $index * @param string $type * @return bool */ public static function lazyForge(string $index, string $type) : bool { $state = State::instance(); if (!isset($state->gears[$index])) { $gears = $state->gears; $gears[$index] = $type; $state->gears = $gears; return true; } return false; }
/** * Render a custom page * * @param array $latest * @return string */ protected function render(array $latest) : string { $state = State::instance(); switch ($latest['formatting']) { case 'Markdown': $md = new CommonMarkConverter(); if (empty($latest['raw'])) { $state->HTMLPurifier->purify($md->convertToHtml($latest['body'])); } return $md->convertToHtml($latest['body']); case 'RST': $rst = (new RSTParser())->setIncludePolicy(false); if (empty($latest['raw'])) { $state->HTMLPurifier->purify((string) $rst->parse($latest['body'])); } return (string) $rst->parse($latest['body']); case 'HTML': case 'Rich Text': default: if (empty($latest['raw'])) { return $state->HTMLPurifier->purify($latest['body']); } return $latest['body']; } }
/** * Send HTTP headers * * @param string $mimeType * @return void */ public function sendStandardHeaders(string $mimeType) { $state = State::instance(); \header('Content-Type: ' . $mimeType); \header('Content-Language: ' . $state->lang); \header('X-Content-Type-Options: nosniff'); \header('X-Frame-Options: SAMEORIGIN'); // Maybe make this configurable down the line? \header('X-XSS-Protection: 1; mode=block'); if (isset($state->HPKP) && $state->HPKP instanceof HPKPBuilder) { $state->HPKP->sendHPKPHeader(); } if (isset($state->CSP) && $state->CSP instanceof CSPBuilder) { $state->CSP->sendCSPHeader(); } }
/** * Return true if the Merkle roots match. * * Dear future security auditors: This is important. * * This employs challenge-response authentication: * @ref https://github.com/paragonie/airship/issues/13 * * @param Channel $channel * @param MerkleTree $originalTree * @param TreeUpdate[] ...$updates * @return bool * @throws CouldNotUpdate */ protected function verifyResponseWithPeers(Channel $channel, MerkleTree $originalTree, TreeUpdate ...$updates) : bool { $state = State::instance(); $nodes = $this->updatesToNodes($updates); $tree = $originalTree->getExpandedTree(...$nodes); $maxUpdateIndex = \count($updates) - 1; $expectedRoot = $updates[$maxUpdateIndex]->getRoot(); if (!\hash_equals($tree->getRoot(), $expectedRoot)) { // Calculated root did not match. self::$continuumLogger->store(LogLevel::EMERGENCY, 'Calculated Merkle root did not match the update.', [$tree->getRoot(), $expectedRoot]); throw new CouldNotUpdate(\__('Calculated Merkle root did not match the update.')); } if ($state->universal['auto-update']['ignore-peer-verification']) { // The user has expressed no interest in verification return true; } $peers = $channel->getPeerList(); $numPeers = \count($peers); /** * These numbers are negotiable in future versions. * * If P is the set of trusted peer notaries (where ||P|| is the number * of trusted peer notaries): * * 1. At least 1 must return 'success'. * 2. At least ln(||P||) must return 'success'. * 3. At most e * ln(||P||) can timeout. * 4. If any peer disagrees with what we see, our * result is discarded as invalid. * * The most harm a malicious peer can do is DoS if they * are selected. */ $minSuccess = $channel->getAppropriatePeerSize(); $maxFailure = (int) \min(\floor($minSuccess * M_E), $numPeers - 1); if ($maxFailure < 1) { $maxFailure = 1; } \Airship\secure_shuffle($peers); $success = $networkError = 0; /** * If any peers give a different answer, we're under attack. * If too many peers don't respond, assume they're being DDoS'd. * If enough peers respond in absolute agreement, we're good. */ for ($i = 0; $i < $numPeers; ++$i) { try { if (!$this->checkWithPeer($peers[$i], $tree->getRoot())) { // Merkle root mismatch? Abort. return false; } ++$success; } catch (TransferException $ex) { self::$continuumLogger->store(LogLevel::EMERGENCY, 'A transfer exception occurred', \Airship\throwableToArray($ex)); ++$networkError; } if ($success >= $minSuccess) { // We have enough good responses. return true; } elseif ($networkError >= $maxFailure) { // We can't give a confident response here. return false; } } self::$continuumLogger->store(LogLevel::EMERGENCY, 'We ran out of peers.', [$numPeers, $minSuccess, $maxFailure]); // Fail closed: return false; }
/** * Make sure we include the default params * * @param array $params * @param string $url (used for decision-making) * * @return array */ public function params(array $params = [], string $url = '') : array { $config = State::instance(); $defaults = ['curl' => [CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2]]; /** * If we have pre-configured some global parameters, let's add them to * our defaults before we check if we need Tor support. */ if (!empty($config->universal['guzzle'])) { $defaults = \array_merge($defaults, $config->universal['guzzle']); } /** * Support for Tor Hidden Services */ if (\Airship\isOnionUrl($url)) { // A .onion domain should be a Tor Hidden Service $defaults['curl'][CURLOPT_PROXY] = 'http://127.0.0.1:9050/'; $defaults['curl'][CURLOPT_PROXYTYPE] = CURLPROXY_SOCKS5; if (!\preg_match('#^https://', $url)) { // If it's a .onion site, HTTPS is not required. // If HTTPS is specified, still enforce it. unset($defaults['curl'][CURLOPT_SSLVERSION]); } } elseif (!empty($config->universal['tor-only'])) { // We were configured to use Tor for everything. $defaults['curl'][CURLOPT_PROXY] = 'http://127.0.0.1:9050/'; $defaults['curl'][CURLOPT_PROXYTYPE] = CURLPROXY_SOCKS5; } return \array_merge($defaults, ['form_params' => $params]); }
/** * Render lens content, cache it, then display it. * * @param string $name * @param array $cArgs Constructor arguments * @return bool * @exit; */ protected function stasis(string $name, ...$cArgs) : bool { // We don't want to cache anything tied to a session. $oldSession = $_SESSION; $_SESSION = []; $data = $this->airship_lens_object->render($name, ...$cArgs); $_SESSION = $oldSession; $port = $_SERVER['HTTP_PORT'] ?? ''; $cacheKey = $_SERVER['HTTP_HOST'] . ':' . $port . '/' . $_SERVER['REQUEST_URI']; if (!$this->airship_filecache_object->set($cacheKey, $data)) { return false; } $state = State::instance(); if (!\headers_sent()) { \header('Content-Type: text/html;charset=UTF-8'); \header('Content-Language: ' . $state->lang); \header('X-Content-Type-Options: nosniff'); \header('X-Frame-Options: SAMEORIGIN'); // Maybe make this configurable down the line? \header('X-XSS-Protection: 1; mode=block'); $hpkp = $state->HPKP; if ($hpkp instanceof HPKPBuilder) { $hpkp->sendHPKPHeader(); } $csp = $state->CSP; if ($csp instanceof CSPBuilder) { $csp->sendCSPHeader(); $this->airship_cspcache_object->set($_SERVER['REQUEST_URI'], \json_encode($csp->getHeaderArray())); } } die($data); }
/** * @route help */ public function helpPage() { if ($this->isLoggedIn()) { $this->storeLensVar('showmenu', true); // $cabins = $this->getCabinNamespaces(); // Get debug information. $helpInfo = ['cabins' => [], 'cabin_names' => \array_values($cabins), 'gears' => [], 'universal' => []]; /** * This might reveal "sensitive" information. By default, it's * locked out of non-administrator users. You can grant access to * other users/groups via the Permissions menu. */ if ($this->can('read')) { $state = State::instance(); if (\is_readable(ROOT . '/config/gadgets.json')) { $helpInfo['universal']['gadgets'] = \Airship\loadJSON(ROOT . '/config/gadgets.json'); } if (\is_readable(ROOT . '/config/content_security_policy.json')) { $helpInfo['universal']['content_security_policy'] = \Airship\loadJSON(ROOT . '/config/content_security_policy.json'); } foreach ($cabins as $cabin) { $cabinData = ['config' => \Airship\loadJSON(ROOT . '/Cabin/' . $cabin . '/manifest.json'), 'content_security_policy' => [], 'gadgets' => [], 'motifs' => [], 'user_motifs' => \Airship\LensFunctions\user_motif($this->getActiveUserId(), $cabin)]; $prefix = ROOT . '/Cabin/' . $cabin . '/config/'; if (\is_readable($prefix . 'gadgets.json')) { $cabinData['gadgets'] = \Airship\loadJSON($prefix . 'gadgets.json'); } if (\is_readable($prefix . 'motifs.json')) { $cabinData['motifs'] = \Airship\loadJSON($prefix . 'motifs.json'); } if (\is_readable($prefix . 'content_security_policy.json')) { $cabinData['content_security_policy'] = \Airship\loadJSON($prefix . 'content_security_policy.json'); } $helpInfo['cabins'][$cabin] = $cabinData; } $helpInfo['gears'] = []; foreach ($state->gears as $gear => $latestGear) { $helpInfo['gears'][$gear] = \Airship\get_ancestors($latestGear); } // Only grab data likely to be pertinent to common issues: $keys = ['airship', 'auto-update', 'debug', 'guzzle', 'notary', 'rate-limiting', 'session_config', 'tor-only', 'twig_cache']; $helpInfo['universal']['config'] = \Airship\keySlice($state->universal, $keys); $helpInfo['php'] = ['halite' => Halite::VERSION, 'libsodium' => ['major' => \Sodium\library_version_major(), 'minor' => \Sodium\library_version_minor(), 'version' => \Sodium\version_string()], 'version' => \PHP_VERSION, 'versionid' => \PHP_VERSION_ID]; } $this->lens('help', ['active_link' => 'bridge-link-help', 'airship' => \AIRSHIP_VERSION, 'helpInfo' => $helpInfo]); } else { // Not a registered user? Go read the docs. No info leaks for you! \Airship\redirect('https://github.com/paragonie/airship-docs'); } }
/** * Store a DEBUG message * * @param string $message * @param array $context * @return mixed */ public function debug($message, array $context = []) { $state = State::instance(); if (!$state->universal['debug']) { return null; } return $this->log(LogLevel::DEBUG, $message, $context); }
* 2. Load the Airship functions */ require_once ROOT . '/Airship.php'; /** * 3. Let's autoload the composer packages */ require_once \dirname(ROOT) . '/vendor/autoload.php'; // Let's also make sure we're using a good version of libsodium if (!Halite::isLibsodiumSetupCorrectly()) { die("Airship requires libsodium 1.0.9 or newer (with a stable version of the PHP bindings)."); } /** * 4. Autoload the Engine files */ \Airship\autoload('Airship\\Alerts', '~/Alerts'); \Airship\autoload('Airship\\Engine', '~/Engine'); /** * 5. Load up the registry singleton for latest types */ $state = State::instance(); // 5a. Initialize the Gears. require_once ROOT . '/gear_init.php'; /** * 6. Load the global functions */ require_once ROOT . '/global_functions.php'; require_once ROOT . '/lens_functions.php'; /** * 7. Load all of the cryptography keys */ require_once ROOT . '/keys.php';
/** * Render the contents of a cargo (placeholder) * * @param string $name * @param int $offset * @return array */ public static function unloadCargo(string $name, int $offset = 0) { $state = State::instance(); if (isset($state->cargo[$name])) { if (isset($state->cargo[$name][$offset])) { // Return an entire slice; Twig will use the first valid result return \array_slice($state->cargo[$name], $offset); } return $state->cargo[$name]; } return []; }