/** * @covers Base64UrlSafe::encode() * @covers Base64UrlSafe::decode() */ public function testRandom() { for ($i = 1; $i < 32; ++$i) { for ($j = 0; $j < 50; ++$j) { $random = \random_bytes($i); $enc = Base64UrlSafe::encode($random); $this->assertSame($random, Base64UrlSafe::decode($enc)); $this->assertSame(\strtr(\base64_encode($random), '+/', '-_'), $enc); } } }
/** * Peer constructor. * @param array $config */ public function __construct(array $config = []) { $this->name = $config['name']; $this->publicKey = new SignaturePublicKey(Base64UrlSafe::decode($config['public_key'])); $this->urls = $config['urls']; foreach ($this->urls as $url) { if (\Airship\isOnionUrl($url)) { $this->onion = true; break; } } }
/** * Get the configuration for this version of halite * * @param string $stored A stored password hash * @return SymmetricConfig * @throws InvalidMessage */ protected static function getConfig(string $stored) : SymmetricConfig { $length = Util::safeStrlen($stored); // This doesn't even have a header. if ($length < 8) { throw new InvalidMessage('Encrypted password hash is way too short.'); } if (\hash_equals(Util::safeSubstr($stored, 0, 5), Halite::VERSION_PREFIX)) { return SymmetricConfig::getConfig(Base64UrlSafe::decode($stored), 'encrypt'); } $v = \Sodium\hex2bin(Util::safeSubstr($stored, 0, 8)); return SymmetricConfig::getConfig($v, 'encrypt'); }
/** * Coerce a string into base64 format. * * @param string $hash * @param string $algo * @return string * @throws \Exception */ protected function coerceBase64(string $hash, string $algo = 'sha256') : string { switch ($algo) { case 'sha256': $limits = ['raw' => 32, 'hex' => 64, 'pad_min' => 40, 'pad_max' => 44]; break; default: throw new \Exception('Browsers currently only support sha256 public key pins.'); } $len = Binary::safeStrlen($hash); if ($len === $limits['hex']) { $hash = Base64::encode(Hex::decode($hash)); } elseif ($len === $limits['raw']) { $hash = Base64::encode($hash); } elseif ($len > $limits['pad_min'] && $len < $limits['pad_max']) { // Padding was stripped! $hash .= \str_repeat('=', $len % 4); // Base64UrlSsafe encoded. if (\strpos($hash, '_') !== false || \strpos($hash, '-') !== false) { $hash = Base64UrlSafe::decode($hash); } else { $hash = Base64::decode($hash); } $hash = Base64::encode($hash); } return $hash; }
/** * Validate a request based on $_SESSION and $_POST data * * @return bool */ public function check() : bool { if (!isset($_SESSION[$this->sessionIndex])) { // We don't even have a session array initialized $_SESSION[$this->sessionIndex] = []; return false; } if (!isset($_POST[self::FORM_TOKEN]) || !\is_string($_POST[self::FORM_TOKEN])) { return false; } if (\strpos($_POST[self::FORM_TOKEN], ':') === false) { return false; } // Let's pull the POST data list($index, $token) = \explode(':', $_POST[self::FORM_TOKEN]); if (empty($index) || empty($token)) { return false; } if (!isset($_SESSION[$this->sessionIndex][$index])) { // CSRF Token not found return false; } // Grab the value stored at $index $stored = $_SESSION[$this->sessionIndex][$index]; // We don't need this anymore unset($_SESSION[$this->sessionIndex][$index]); // Which form action="" is this token locked to? $lockTo = $_SERVER['REQUEST_URI']; if (\preg_match('#/$#', $lockTo)) { // Trailing slashes are to be ignored $lockTo = substr($lockTo, 0, strlen($lockTo) - 1); } if (!empty($stored['lockto'])) { if (!\hash_equals($lockTo, $stored['lockto'])) { // Form target did not match the request this token is locked to! return false; } } // This is the expected token value if ($this->hmacIP === false) { // We just stored it wholesale $expected = $stored['token']; } else { // We mixed in the client IP address to generate the output $expected = Base64UrlSafe::encode(CryptoUtil::raw_keyed_hash($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1', Base64UrlSafe::decode($stored['token']))); } return \hash_equals($token, $expected); }
/** * Interpret the TreeUpdate objects from the API response. OR verify the signature * of the "no updates" message to prevent a DoS. * * Dear future security auditors: This is important. * * @param Channel $chan * @param array $response * @return TreeUpdate[] * @throws ChannelSignatureFailed * @throws CouldNotUpdate */ protected function parseTreeUpdateResponse(Channel $chan, array $response) : array { if (!empty($response['no_updates'])) { // The "no updates" message should be authenticated. $signatureVerified = AsymmetricCrypto::verify($response['no_updates'], $chan->getPublicKey(), Base64UrlSafe::decode($response['signature']), true); if (!$signatureVerified) { throw new ChannelSignatureFailed(); } $datetime = new \DateTime($response['no_updates']); // One day ago: $stale = (new \DateTime('now'))->sub(new \DateInterval('P01D')); if ($datetime < $stale) { throw new CouldNotUpdate(\__('Stale response.')); } // We got nothing to do: return []; } // We were given updates. Let's validate them! $TreeUpdateArray = []; foreach ($response['updates'] as $update) { $data = Base64UrlSafe::decode($update['data']); $sig = Base64UrlSafe::decode($update['signature']); $signatureVerified = AsymmetricCrypto::verify($data, $chan->getPublicKey(), $sig, true); if (!$signatureVerified) { // Invalid signature throw new ChannelSignatureFailed(); } // Now that we know it was signed by the channel, time to update $TreeUpdateArray[] = new TreeUpdate($chan, \json_decode($data, true)); } // Sort by ID \uasort($TreeUpdateArray, function (TreeUpdate $a, TreeUpdate $b) : int { return (int) ($a->getChannelId() <=> $b->getChannelId()); }); return $TreeUpdateArray; }