Example #1
0
 /**
  * @covers \Airship\secure_shuffle()
  */
 public function testSecureShuffle()
 {
     $array = [];
     for ($i = 0; $i < 512; ++$i) {
         $array[] = random_int(PHP_INT_MIN, PHP_INT_MAX);
     }
     $copy = \array_values($array);
     \Airship\secure_shuffle($copy);
     $this->assertNotSame($copy, $array, 'Shuffled array should not be identical to original.');
     $this->assertCount(512, $copy, 'Shuffled array is not the correct size.');
 }
Example #2
0
 /**
  * Get all URLs
  *
  * @param bool $doNotShuffle
  * @return string[]
  */
 protected function getChannelURLs(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;
             } else {
                 $after[] = $url;
             }
         }
         // Shuffle each array separately, to maintain priority;
         if (!$doNotShuffle) {
             \Airship\secure_shuffle($candidates);
             \Airship\secure_shuffle($after);
         }
         foreach ($after as $url) {
             $candidates[] = $url;
         }
     } else {
         $candidates = $this->urls;
         if (!$doNotShuffle) {
             \Airship\secure_shuffle($candidates);
         }
     }
     return $candidates;
 }
Example #3
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 #4
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;
 }