Inheritance: implements IteratorAggregate, implements ArrayAccess, implements Serializable, implements Countable
Example #1
0
 /**
  * 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();
     }
 }
Example #2
0
 /**
  * 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;
 }
Example #3
0
 /**
  * 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);
 }
Example #4
0
 /**
  * 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);
 }
Example #5
0
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;
Example #6
0
 /**
  * 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);
 }
Example #7
0
 /**
  * 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();
 }
Example #8
0
 /**
  * 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;
 }
Example #9
0
 /**
  * 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'];
     }
 }
Example #10
0
 /**
  * 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();
     }
 }
Example #11
0
 /**
  * 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;
 }
Example #12
0
 /**
  * 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]);
 }
Example #13
0
 /**
  * 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);
 }
Example #14
0
 /**
  * @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');
     }
 }
Example #15
0
 /**
  * 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);
 }
Example #16
0
 * 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';
Example #17
0
 /**
  * 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 [];
 }