/** * Fire the add command! * * @param array $args * @return bool */ public function fire(array $args = []) : bool { try { $this->getSession(); $dir = $this->session['dir'] . $this->findRelativeDir(); } catch (\Error $e) { echo $e->getMessage(), "\n"; return false; } if (\count($args) === 0) { echo 'No file passed.', "\n"; return false; } if (!isset($this->session['add'])) { echo 'Creating session data', "\n"; $this->session['add'] = []; } $added = 0; foreach ($args as $file) { $l = Binary::safeStrlen($file) - 1; if ($file[$l] === DIRECTORY_SEPARATOR) { $file = Binary::safeSubstr($file, 0, -1); } $added += $this->addFile($file, $dir); } echo $added, ' file', $added === 1 ? '' : 's', ' added.', "\n"; return true; }
/** * SeedSpring constructor. * * @param string $seed * @param int $counter */ public function __construct($seed = '', $counter = 0) { if (Binary::safeStrlen($seed) !== 16) { throw new \InvalidArgumentException('Seed must be 16 bytes'); } $this->seed('set', $seed); $this->counter = 0; }
/** * Request a value. * @param string $text * @return string */ function prompt(string $text = '') { static $fp = null; if ($fp === null) { $fp = \fopen('php://stdin', 'r'); } echo $text; return Binary::safeSubstr(\fgets($fp), 0, -1); }
/** * 1. VerifyHMAC-then-Decrypt the ciphertext to get the hash * 2. Verify that the password matches the hash * * @param string $password * @param string $ciphertext * @param string $aesKey - must be exactly 16 bytes * @return bool * @throws \Exception * @throws \InvalidArgumentException */ public static function decryptAndVerifyLegacy(string $password, string $ciphertext, string $aesKey) : bool { if (!\is_string($password)) { throw new \InvalidArgumentException('Password must be a string.'); } if (Binary::safeStrlen($aesKey) !== 16) { throw new \Exception("Encryption keys must be 16 bytes long"); } $hash = Crypto::legacyDecrypt($ciphertext, $aesKey); return \password_verify(Base64::encode(\hash('sha256', $password, true)), $hash); }
/** * Work around poorly-configured web servers by parsing out the GET parameters * * Be forewarned: this will overwrite $lastPiece * * @param string& $lastPiece (optional) * @return array */ protected function httpGetParams(string &$lastPiece = null) : array { if ($lastPiece === null) { return $_GET ?? []; } $p = \strpos($lastPiece, '?'); if ($p !== false && empty($_GET)) { $_GET = \Airship\array_from_http_query(Binary::safeSubstr($lastPiece, $p + 1)); $lastPiece = Binary::safeSubstr($lastPiece, 0, $p); } return $_GET; }
/** * FileStore constructor. * @param string $baseDirectory * @param string $logfileFormat * @param string $timeFormat */ public function __construct(string $baseDirectory = '', string $logfileFormat = self::FILE_FORMAT, string $timeFormat = self::TIME_FORMAT) { if (Binary::safeStrlen($baseDirectory) < 2) { $this->basedir = ROOT . '/tmp/logs/'; } else { $this->basedir = $baseDirectory; } if (!\is_dir($this->basedir)) { \mkdir($this->basedir, 0775); } $this->fileFormat = $logfileFormat; $this->timeFormat = $timeFormat; }
/** * If a file path is absolute, but still in the root, truncate it. * If a file path is relative to the root, return it. * Otherwise, thorw an error! * * @param string $file * @return string * @throws \Error */ protected function getRealPath(string $file) : string { if (!\file_exists($file)) { throw new \Error('File not found: ' . $file); } if (\strpos($file, $this->session['dir']) === 0) { $x = Binary::safeStrlen($this->session['dir']); return Binary::safeSubstr($file, $x + 1); } elseif ($file[0] !== DIRECTORY_SEPARATOR) { return $file; } else { throw new \Error('File path is outside the root directory: ' . $file); } }
/** * Our nonce logic needs to match OpenSSL's internals. */ public function testCtrModeNonce() { $seed = random_bytes(16); $rnd1 = new SeedSpring($seed); $rnd2 = new SeedSpring($seed); foreach ([4096, 4097, 8192, 16384, 65536] as $test) { $buf1 = ''; for ($i = 0; $i < $test; $i += 16) { $buf1 .= $rnd1->getBytes($i + 16 > $test ? $test - $i : 16); } $buf2 = $rnd2->getBytes($test); $this->assertSame(\ParagonIE\ConstantTime\Binary::safeStrlen($buf1), \ParagonIE\ConstantTime\Binary::safeStrlen($buf2), 'Not the same length - test ' . $test); $this->assertSame(bin2hex(substr($buf1, 0, 16)), bin2hex(substr($buf2, 0, 16)), 'AES CTR nonce isn\'t correct - first 16 - test ' . $test); $this->assertSame(bin2hex(substr($buf1, 16, 16)), bin2hex(substr($buf2, 16, 16)), 'AES CTR nonce isn\'t correct - next 16 - test ' . $test); $this->assertSame(bin2hex(substr($buf1, -16, 16)), bin2hex(substr($buf2, -16, 16)), 'AES CTR nonce isn\'t correct - last 16 - test ' . $test); } }
/** * The homepage for an Airship. * * @route / */ public function index() { $this->blog = $this->blueprint('Blog'); if (!\file_exists(ROOT . '/public/robots.txt')) { // Default robots.txt \file_put_contents(ROOT . '/public/robots.txt', "User-agent: *\nAllow: /"); } $blogRoll = $this->blog->recentFullPosts((int) ($this->config('homepage.blog-posts') ?? 5)); $mathJAX = false; foreach ($blogRoll as $i => $blog) { $blogRoll[$i] = $this->blog->getSnippet($blog); if (Binary::safeStrlen($blogRoll[$i]['snippet']) !== Binary::safeStrlen($blog['body'])) { $blogRoll[$i]['snippet'] = \rtrim($blogRoll[$i]['snippet'], "\n"); } $mathJAX |= \strpos($blog['body'], '$$') !== false; } $args = ['blogposts' => $blogRoll]; $this->config('blog.cachelists') ? $this->stasis('index', $args) : $this->lens('index', $args); }
/** * Load all of the supplier's Ed25519 public keys * * @param string $supplier * @param boolean $force_flush * @return SupplierObject|SupplierObject[] * @throws NoSupplier */ public function getSupplier(string $supplier = '', bool $force_flush = false) { if (empty($supplier)) { // Fetch all suppliers if ($force_flush || empty($this->supplierCache)) { $supplierCache = []; $allSuppliers = \Airship\list_all_files(ROOT . '/config/supplier_keys', 'json'); foreach ($allSuppliers as $supplierKeyFile) { // We want everything except the .json $supplier = $this->escapeSupplierName(Binary::safeSubstr($this->getEndPiece($supplierKeyFile), 0, -5)); try { $data = \Airship\loadJSON($supplierKeyFile); } catch (FileNotFound $ex) { $data = []; } $supplierCache[$supplier] = new SupplierObject($supplier, $data); } $this->supplierCache = $supplierCache; } return $this->supplierCache; } // Otherwise, we're just fetching one supplier's keys if ($force_flush || empty($this->supplierCache[$supplier])) { try { $supplier = $this->escapeSupplierName($supplier); $supplierFile = ROOT . '/config/supplier_keys/' . $supplier . '.json'; if (!\file_exists($supplierFile)) { throw new NoSupplier(\__("Supplier file not found: %s", "default", $supplierFile)); } $data = \Airship\loadJSON($supplierFile); } catch (FileNotFound $ex) { throw new NoSupplier(\__("Supplier not found: %s", "default", $supplier), 0, $ex); } $this->supplierCache[$supplier] = new SupplierObject($supplier, $data); } if (isset($this->supplierCache[$supplier])) { return $this->supplierCache[$supplier]; } throw new NoSupplier(); }
/** * Blog post home * * @route / */ public function index() { $blogRoll = $this->blog->recentFullPosts((int) $this->config('homepage.blog-posts') ?? 5); $mathJAX = false; foreach ($blogRoll as $i => $blog) { $blogRoll[$i] = $this->blog->getSnippet($blog); if (Binary::safeStrlen($blogRoll[$i]['snippet']) !== Binary::safeStrlen($blog['body'])) { $blogRoll[$i]['snippet'] = \rtrim($blogRoll[$i]['snippet'], "\n"); } $mathJAX = $mathJAX || \strpos($blog['body'], '$$') !== false; } $args = ['pageTitle' => \__('Blog'), 'blogroll' => $blogRoll, 'mathjax' => $mathJAX]; $this->config('blog.cachelists') ? $this->stasis('blog/index', $args) : $this->lens('blog/index', $args); }
/** * WordPress's internal password hashing algorithm. Only used for migrations. * The actual security of CMS Airship doesn't depend on this algorithm. * * @internal * @param HiddenString $password * @param string $setting * @return string */ private function wordPressCryptPrivate(HiddenString $password, string $setting) : string { $output = '*0'; if (Binary::safeSubstr($setting, 0, 2) === $output) { $output = '*1'; } $id = Binary::safeSubstr($setting, 0, 3); if ($id !== '$P$' && $id !== '$H$') { return $output; } // This is a really weird way to encode iteration count. $count_log2 = \strpos($this->itoa64, $setting[3]); if ($count_log2 < 7 || $count_log2 > 30) { return $output; } $count = 1 << $count_log2; $salt = Binary::safeSubstr($setting, 4, 8); if (Binary::safeStrlen($salt) !== 8) { return $output; } // And now we do our (default 8192) rounds of MD5... $hash = \md5($salt . $password->getString(), true); do { $hash = \md5($hash . $password->getString(), true); } while (--$count); $output = Binary::safeSubstr($setting, 0, 12); $output .= $this->encode64($hash, 16); return $output; }
/** * Binary-safe strlen() implementation * * @param string $str * @return int */ public static function stringLength(string $str) : int { return Binary::safeStrlen($str); }
/** * Configure the application event logger here */ $log_setup_closure = function () { $state = State::instance(); $loggerClass = Gears::getName('Ledger'); $args = []; /** * Here we build our logger storage class */ switch ($state->universal['ledger']['driver']) { case 'file': $path = $state->universal['ledger']['path']; if (Binary::safeStrlen($path) >= 2) { if ($path[0] === '~' && $path[1] === '/') { $path = ROOT . '/' . Binary::safeSubstr($path, 2); } } $storage = new FileStore($path, $state->universal['ledger']['file-format'] ?? FileStore::FILE_FORMAT, $state->universal['ledger']['time-format'] ?? FileStore::TIME_FORMAT); break; case 'database': $path = $state->universal['ledger']['connection']; try { $storage = new DBStore($path, $state->universal['ledger']['table'] ?? DBStore::DEFAULT_TABLE); } catch (\Throwable $ex) { \http_response_code(500); echo \file_get_contents(__DIR__ . '/error_pages/uncaught-exception.html'); if ($state->universal['debug']) { echo '<pre>', $ex->getTraceAsString(), '</pre>'; } exit(1);
/** * @covers \Airship\uniqueId() */ public function testUniqueId() { $strings = []; for ($i = 0; $i < 1024; ++$i) { $strings[] = \Airship\uniqueId(33); } $this->assertSame($strings, \array_unique($strings), 'Collision!'); for ($i = 18; $i < 30; ++$i) { $unique = \trim(\Airship\uniqueId($i), '='); $this->assertSame($i, Binary::safeStrlen($unique)); } }
<?php declare (strict_types=1); use ParagonIE\ConstantTime\Binary; /** * Paragon Initiative Enterprises * PSR-4 compatible autoloader */ \spl_autoload_register(function ($class) { // Project-specific namespace prefix $prefix = 'Airship\\Hangar'; // Base directory for the namespace prefix $base_dir = __DIR__ . DIRECTORY_SEPARATOR; // Does the class use the namespace prefix? $len = \strlen($prefix); if (\strncmp($prefix, $class, $len) !== 0) { // no, move to the next registered autoloader return; } // Get the relative class name $relative_class = Binary::safeSubstr($class, $len); // Replace the namespace prefix with the base directory, replace namespace // separators with directory separators in the relative class name, append // with .php $file = $base_dir . \str_replace(['\\', '_'], DIRECTORY_SEPARATOR, $relative_class) . '.php'; // If the file exists, require it if (\file_exists($file)) { require $file; } });
} /** * Let the user know precisely what's wrong, if anything is wrong. */ if (!Halite::isLibsodiumSetupCorrectly()) { // Easiest way to grab this info: \ob_start(); \phpinfo(); $data = \ob_get_clean(); $version = ''; foreach (\explode("\n", $data) as $line) { if (empty($line)) { continue; } if (\strpos($line, 'libsodium compiled version') !== false) { $version = \trim(Binary::safeSubstr(\trim($line), -6)); break; } } die("Your libsodium is not setup correctly. Please make sure you have at least:\n\n" . "\tlibsodium v1.0.10 (Installed: " . \Sodium\version_string() . ")\n" . "\tlibsodium-php v1.0.6 (Installed: " . $version . ")\n"); } /** * 3. Process the CLI parameters */ $showAll = true; if ($argc < 2) { // Default behavior: Display the help menu $argv[1] = 'help'; $showAll = false; $argc = 2; }
$link = ROOT . '/public/static/' . $active['name']; if (!\is_link($link)) { // Remove copies, we only allow symlinks in static if (\is_dir($link)) { \rmdir($link); } elseif (\file_exists($link)) { \unlink($link); } // Create a symlink from public/static/* to Cabin/*/public /** @noinspection PhpUsageOfSilenceOperatorInspection */ @\symlink(CABIN_DIR . '/public', ROOT . '/public/static/' . $active['name']); } } // Let's load the default cargo modules if (\is_dir(CABIN_DIR . '/Lens/cargo')) { $cargoCacheFile = ROOT . '/tmp/cache/cargo-' . $active['name'] . '.cache.json'; if (\file_exists($cargoCacheFile)) { $data = Airship\loadJSON($cargoCacheFile); $state->cargo = $data; } else { $dir = \getcwd(); \chdir(CABIN_DIR . '/Lens'); foreach (\Airship\list_all_files('cargo', 'twig') as $cargo) { $idx = \str_replace(['__', '/'], ['', '__'], Binary::safeSubstr($cargo, 6, -5)); Gadgets::loadCargo($idx, $cargo); } \chdir($dir); // Store the cache file \Airship\saveJSON($cargoCacheFile, $state->cargo); } }
/** * Get a preview snippet of a blog post * * @param array $post Post data * @param boolean $after Do we want the content after the fold? * @return array */ public function getSnippet(array $post, bool $after = false) : array { // First, let's try cutting it off by section... if (empty($post['body'])) { return $post; } $i = 0; if ($post['format'] === 'Rich Text') { $post['body'] = \str_replace('><', '>' . "\n" . '<', $post['body']); } // Just in case: $post['body'] = \str_replace("\r\n", "\n", $post['body']); $lines = \explode("\n", $post['body']); // If we find <!--FOLD--> in the body, split on that instead: $search = \array_search($this->defaultSeparator, $lines); if ($search !== false) { $post['snippet'] = \implode("\n", \array_slice($lines, 0, $search - 1)); if ($after) { $post['after_fold'] = \implode("\n", \array_slice($lines, $search)); } return $post; } // Short post? Just dump it as-is: if (count($lines) < 4 || Binary::safeStrlen($post['body']) < 200) { $post['snippet'] = $post['body']; $post['after_fold'] = ''; return $post; } $cutoff = null; $regex = '#^([' . \preg_quote('!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~', '#') . '])#'; foreach ($lines as $i => $line) { if (empty($line)) { continue; } if ($post['format'] === 'RST') { if (\preg_match($regex, $line[0], $m)) { if ($i > 2 && \trim($line) === \str_repeat($m[1], Binary::safeStrlen($line))) { $cutoff = $i; break; } } } elseif ($post['format'] === 'Markdown') { if (\preg_match('#^([' . \preg_quote('#', '#') . ']{1,})#', $line[0], $m)) { $cutoff = $i; break; } } elseif ($post['format'] === 'HTML' || $post['format'] === 'Rich Text') { if (\preg_match('#^<' . 'h[1-7]>#', $line[0], $m)) { $cutoff = $i; break; } } } if ($cutoff !== null) { $post['snippet'] = \implode("\n", \array_slice($lines, 0, $i - 1)); if ($after) { $post['after_fold'] = \implode("\n", \array_slice($lines, $i - 1)); } return $post; } // Next, let's find the 37% mark for breaks $split = $post['format'] === 'Rich Text' || $post['format'] === 'HTML' ? "\n" : "\n\n"; $sects = \explode($split, $post['body']); // 37% is approximately 1/e (the mathematical constant) $cut = (int) \ceil(0.37 * \count($sects)); if ($sects < 2) { $post['snippet'] = $post['body']; $post['after_fold'] = ''; return $post; } if (\preg_match('#^\\.\\. #', $sects[$cut - 1])) { --$cut; } $post['snippet'] = \implode("\n\n", \array_slice($sects, 0, $cut - 1)) . "\n"; if ($after) { $post['after_fold'] = "\n" . \implode($split, \array_slice($sects, $cut - 1)); } return $post; }
<?php declare (strict_types=1); use ParagonIE\ConstantTime\Binary; require_once \dirname(__DIR__) . '/src/bootstrap.php'; /** * Grabs a random file and tells you to audit it. */ if ($argc > 1) { $extensions = \array_slice($argv, 1); } else { $extensions = ['php', 'twig']; } $fileList = []; foreach ($extensions as $ex) { foreach (\Airship\list_all_files(\dirname(__DIR__) . '/src/', $ex) as $file) { $fileList[] = $file; } } $choice = \random_int(0, \count($fileList) - 1); echo "Audit this file:\n\t"; $l = Binary::safeStrlen(\dirname(__DIR__)); echo Binary::safeSubstr($fileList[$choice], $l), "\n";
// Are we still installing? /** @noinspection PhpUsageOfSilenceOperatorInspection */ if (@\is_readable(dirname(__DIR__) . '/tmp/installing.json') || !\file_exists(dirname(__DIR__) . '/config/databases.json')) { include dirname(__DIR__) . '/Installer/launch.php'; exit; } /** * Load the bare minimum: */ require_once \dirname(__DIR__) . '/preload.php'; $start = \microtime(true); if (empty($_POST)) { /** * Let's get rid of trailing slashes in URLs without POST data */ $sliceAt = Binary::safeStrlen($_SERVER['REQUEST_URI']) - 1; if ($sliceAt > 0 && $_SERVER['REQUEST_URI'][$sliceAt] === '/') { \Airship\redirect('/' . \trim($_SERVER['REQUEST_URI'], '/')); } /** * Let's handle static content caching */ if (\extension_loaded('apcu')) { $staticCache = (new MemoryCache())->personalize('staticPage:'); $cspCache = (new MemoryCache())->personalize('contentSecurityPolicy:'); } else { if (!\is_dir(ROOT . '/tmp/cache/static')) { require_once ROOT . '/tmp_dirs.php'; } $staticCache = new FileCache(ROOT . '/tmp/cache/static'); $cspCache = new FileCache(ROOT . '/tmp/cache/csp_static');
/** * Actually serve the HTTP request */ public function route() { $this->loadInjectedRoutes(); $args = []; foreach ($this->cabin['data']['routes'] as $path => $landing) { $path = self::makePath($path); if (self::testLanding($path, $_SERVER['REQUEST_URI'], $args)) { self::$mypath = $path; self::$path = Binary::safeSubstr($_SERVER['REQUEST_URI'], Binary::safeStrlen(self::$patternPrefix) + 1); try { // Attempt to serve the page: return $this->serve($landing, \array_slice($args, 1)); } catch (EmulatePageNotFound $ex) { // If this exception is throw, we will attempt to serve // the fallback route (which might end up with a 404 page) return $this->serveFallback(); } } } return $this->serveFallback(); }
/** * 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; }