/** * 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; }
/** * 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); }
/** * 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); }
// 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');
<?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";
/** * 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); }
/** * 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; }
use Airship\Engine\{Gears, State}; use ParagonIE\ConstantTime\Binary; /** * 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>';
/** * 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); }
/** * 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(); }
/** * 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; }
/** * @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)); } }