/** * Convert a binary string into a hexadecimal string without cache-timing * leaks, returning uppercase letters (as per RFC 4648) * * @param string $bin_string (raw binary) * @return string */ public static function encodeUpper(string $bin_string) : string { $hex = ''; $len = Binary::safeStrlen($bin_string); for ($i = 0; $i < $len; ++$i) { $chunk = \unpack('C', Binary::safeSubstr($bin_string, $i, 2)); $c = $chunk[1] & 0xf; $b = $chunk[1] >> 4; $hex .= pack('CC', 55 + $b + ($b - 10 >> 8 & ~6), 55 + $c + ($c - 10 >> 8 & ~6)); } return $hex; }
/** * Create a unique ID (e.g. for permalinks) * * @param int $length * @return string */ function uniqueId(int $length = 24) : string { if ($length < 1) { return ''; } $n = (int) ceil($length * 0.75); $str = \random_bytes($n); return Binary::safeSubstr(Base64UrlSafe::encode($str), 0, $length); }
/** * Get/verify/parse a JSON response * * The _server_ is the one that signs the message. * We're just verifying the Ed25519 signature. * * @param string $url * @param SignaturePublicKey $publicKey * @param array $args * @param array $options * @return array * @throws \Exception */ public static function postSignedJSON(string $url, SignaturePublicKey $publicKey, array $args = [], array $options = []) : array { $body = self::post($url, $args, $options); if (empty($body)) { throw new \Exception('Empty response from ' . $url); } if (self::$debug) { \var_dump($body); } $firstNewLine = \strpos($body, "\n"); // There should be a newline immediately after the base64urlsafe-encoded signature if ($firstNewLine !== self::ENCODED_SIGNATURE_LENGTH) { throw new \Exception('Invalid Signature'); } $sig = Base64UrlSafe::decode(Binary::safeSubstr($body, 0, self::ENCODED_SIGNATURE_LENGTH)); $msg = Binary::safeSubstr($body, self::ENCODED_SIGNATURE_LENGTH + 1); if (!Asymmetric::verify($msg, $publicKey, $sig, true)) { throw new \Exception('Invalid Signature'); } return \json_decode($msg, true); }
/** * decode from base64 into binary * * Base64 character set "./[A-Z][a-z][0-9]" * * @param string $src * @param bool $strictPadding * @return string|bool * @throws \RangeException */ public static function decode(string $src, bool $strictPadding = false) : string { // Remove padding $srcLen = Binary::safeStrlen($src); if ($srcLen === 0) { return ''; } if ($strictPadding) { if (($srcLen & 3) === 0) { if ($src[$srcLen - 1] === '=') { $srcLen--; if ($src[$srcLen - 1] === '=') { $srcLen--; } } } if (($srcLen & 3) === 1) { throw new \RangeException('Incorrect padding'); } if ($src[$srcLen - 1] === '=') { throw new \RangeException('Incorrect padding'); } } else { $src = \rtrim($src, '='); $srcLen = Binary::safeStrlen($src); } $err = 0; $dest = ''; // Main loop (no padding): for ($i = 0; $i + 4 <= $srcLen; $i += 4) { $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 4)); $c0 = static::decode6Bits($chunk[1]); $c1 = static::decode6Bits($chunk[2]); $c2 = static::decode6Bits($chunk[3]); $c3 = static::decode6Bits($chunk[4]); $dest .= \pack('CCC', ($c0 << 2 | $c1 >> 4) & 0xff, ($c1 << 4 | $c2 >> 2) & 0xff, ($c2 << 6 | $c3) & 0xff); $err |= ($c0 | $c1 | $c2 | $c3) >> 8; } // The last chunk, which may have padding: if ($i < $srcLen) { $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); $c0 = static::decode6Bits($chunk[1]); if ($i + 2 < $srcLen) { $c1 = static::decode6Bits($chunk[2]); $c2 = static::decode6Bits($chunk[3]); $dest .= \pack('CC', ($c0 << 2 | $c1 >> 4) & 0xff, ($c1 << 4 | $c2 >> 2) & 0xff); $err |= ($c0 | $c1 | $c2) >> 8; } elseif ($i + 1 < $srcLen) { $c1 = static::decode6Bits($chunk[2]); $dest .= \pack('C', ($c0 << 2 | $c1 >> 4) & 0xff); $err |= ($c0 | $c1) >> 8; } elseif ($i < $srcLen && $strictPadding) { $err |= 1; } } if ($err !== 0) { throw new \RangeException('Base64::decode() only expects characters in the correct base64 alphabet'); } return $dest; }
/** * Generate, store, and return the index and token * * @param string $lockTo What URI endpoint this is valid for * @return string[] */ protected function generateToken(string $lockTo) : array { $index = Base64::encode(\random_bytes(18)); $token = Base64::encode(\random_bytes(33)); $this->session[$this->sessionIndex][$index] = ['created' => \intval(\date('YmdHis')), 'uri' => isset($this->server['REQUEST_URI']) ? $this->server['REQUEST_URI'] : $this->server['SCRIPT_NAME'], 'token' => $token]; if (\preg_match('#/$#', $lockTo)) { $lockTo = Binary::safeSubstr($lockTo, 0, Binary::safeStrlen($lockTo) - 1); } $this->session[$this->sessionIndex][$index]['lockTo'] = $lockTo; $this->recycleTokens(); return [$index, $token]; }
/** * This propagates the new update through the network. */ protected function notifyPeersOfNewUpdate() { $state = State::instance(); if (IDE_HACKS) { $state->hail = new Hail(new Client()); } $resp = []; $peers = \Airship\loadJSON(ROOT . '/config/channel_peers/' . $this->channel . '.json'); foreach ($peers as $peer) { foreach ($peer['urls'] as $url) { $resp[] = $state->hail->getAsync($url, ['challenge' => Base64UrlSafe::encode(\random_bytes(21))]); } } foreach ($resp as $r) { $r->then(function (ResponseInterface $response) { $body = (string) $response->getBody(); $context = \json_decode(Binary::safeSubstr($body, 89)); $this->log('Peer notified of channel update', LogLevel::INFO, $context); }); } }
/** * Base32 Decoding * * @param string $src * @param bool $upper * @return string */ protected static function doEncode(string $src, bool $upper = false) : string { // We do this to reduce code duplication: $method = $upper ? 'encode5BitsUpper' : 'encode5Bits'; $dest = ''; $srcLen = Binary::safeStrlen($src); // Main loop (no padding): for ($i = 0; $i + 5 <= $srcLen; $i += 5) { $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 5)); $b0 = $chunk[1]; $b1 = $chunk[2]; $b2 = $chunk[3]; $b3 = $chunk[4]; $b4 = $chunk[5]; $dest .= static::$method($b0 >> 3 & 31) . static::$method(($b0 << 2 | $b1 >> 6) & 31) . static::$method($b1 >> 1 & 31) . static::$method(($b1 << 4 | $b2 >> 4) & 31) . static::$method(($b2 << 1 | $b3 >> 7) & 31) . static::$method($b3 >> 2 & 31) . static::$method(($b3 << 3 | $b4 >> 5) & 31) . static::$method($b4 & 31); } // The last chunk, which may have padding: if ($i < $srcLen) { $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); $b0 = $chunk[1]; if ($i + 3 < $srcLen) { $b1 = $chunk[2]; $b2 = $chunk[3]; $b3 = $chunk[4]; $dest .= static::$method($b0 >> 3 & 31) . static::$method(($b0 << 2 | $b1 >> 6) & 31) . static::$method($b1 >> 1 & 31) . static::$method(($b1 << 4 | $b2 >> 4) & 31) . static::$method(($b2 << 1 | $b3 >> 7) & 31) . static::$method($b3 >> 2 & 31) . static::$method($b3 << 3 & 31) . '='; } elseif ($i + 2 < $srcLen) { $b1 = $chunk[2]; $b2 = $chunk[3]; $dest .= static::$method($b0 >> 3 & 31) . static::$method(($b0 << 2 | $b1 >> 6) & 31) . static::$method($b1 >> 1 & 31) . static::$method(($b1 << 4 | $b2 >> 4) & 31) . static::$method($b2 << 1 & 31) . '==='; } elseif ($i + 1 < $srcLen) { $b1 = $chunk[2]; $dest .= static::$method($b0 >> 3 & 31) . static::$method(($b0 << 2 | $b1 >> 6) & 31) . static::$method($b1 >> 1 & 31) . static::$method($b1 << 4 & 31) . '===='; } else { $dest .= static::$method($b0 >> 3 & 31) . static::$method($b0 << 2 & 31) . '======'; } } return $dest; }
/** * Renders ReStructuredText * * @param string $string * @param bool $return * @output HTML * @return string */ function render_rst(string $string = '', bool $return = false) : string { static $rst = null; if (empty($rst)) { $rst = (new RSTParser())->setIncludePolicy(false); } $checksum = CryptoUtil::hash('ReStructuredText' . $string); $h1 = Binary::safeSubstr($checksum, 0, 2); $h2 = Binary::safeSubstr($checksum, 2, 2); $hash = Binary::safeSubstr($checksum, 4); $cacheDir = \implode('/', [ROOT, 'tmp', 'cache', 'rst', $h1, $h2]); if (\file_exists($cacheDir . '/' . $hash . '.txt')) { $output = \file_get_contents($cacheDir . '/' . $hash . '.txt'); } else { if (!\is_dir($cacheDir)) { \mkdir($cacheDir, 0775, true); } $output = (string) $rst->parse($string); // Cache for later \file_put_contents($cacheDir . '/' . $hash . '.txt', $output); \chmod($cacheDir . '/' . $hash . '.txt', 0664); } if ($return) { return $output; } echo $output; return ''; }
/** * Parse a signed JSON response * * @param Response $response * @param SignaturePublicKey $publicKey * @return mixed * @throws SignatureFailed * @throws TransferException */ public function parseSignedJSON(Response $response, SignaturePublicKey $publicKey) { $code = $response->getStatusCode(); if ($code >= 200 && $code < 300) { $body = (string) $response->getBody(); $firstNewLine = \strpos($body, "\n"); // There should be a newline immediately after the base64urlsafe-encoded signature if ($firstNewLine !== self::ENCODED_SIGNATURE_LENGTH) { throw new SignatureFailed(\sprintf("First newline found at position %s, expected %d.\n%s", \print_r($firstNewLine, true), \print_r(self::ENCODED_SIGNATURE_LENGTH, true), Base64::encode($body))); } $sig = Base64UrlSafe::decode(Binary::safeSubstr($body, 0, 88)); $msg = Binary::safeSubstr($body, 89); if (!Asymmetric::verify($msg, $publicKey, $sig, true)) { throw new SignatureFailed(); } return \Airship\parseJSON($msg, true); } throw new TransferException(); }
/** * Replace the existing long-term authentication cookie * * @param string $token * @param int $userId * @return mixed */ public function rotateToken(string $token, int $userId = 0) { try { $decoded = Base64::decode($token); } catch (\RangeException $ex) { return false; } if ($decoded === false) { return false; } elseif (Binary::safeStrlen($decoded) !== self::LONG_TERM_AUTH_BYTES) { return false; } $sel = Binary::safeSubstr($decoded, 0, self::SELECTOR_BYTES); \Sodium\memzero($decoded); // Delete the old token $this->db->delete($this->tableConfig['table']['longterm'], [$this->tableConfig['fields']['longterm']['selector'] => Base64::encode($sel)]); // Let's get a new token return $this->createAuthToken($userId); }