/** * @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.'); }
/** * 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; }
/** * 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; }
/** * 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; }