/** * Execute a block of code. * * @param string $code * @param boolean $cache * @param boolean $do_not_eval * @return mixed */ protected static function coreEval(string $code, bool $cache = false, bool $do_not_eval = false) { \clearstatcache(); if ($do_not_eval || \Airship\is_disabled('eval')) { if ($cache) { if (!\file_exists(ROOT . "/tmp/cache/gear")) { \mkdir(ROOT . "/tmp/cache/gear", 0777); \clearstatcache(); } $hashed = Base64UrlSafe::encode(CryptoUtil::raw_hash($code, 33)); if (!\file_exists(ROOT . '/tmp/cache/gear/' . $hashed . '.tmp.php')) { \file_put_contents(ROOT . '/tmp/cache/gear/' . $hashed . '.tmp.php', '<?php' . "\n" . $code); } return self::sandboxRequire(ROOT . '/cache/' . $hashed . '.tmp.php'); } else { if (!\file_exists(ROOT . '/tmp/gear')) { \mkdir(ROOT . '/tmp/gear', 0777); \clearstatcache(); } $file = \Airship\tempnam('gear-', 'php', ROOT . '/tmp/gear'); \file_put_contents($file, '<?php' . "\n" . $code); \clearstatcache(); $ret = self::sandboxRequire($file); \unlink($file); \clearstatcache(); return $ret; } } else { return eval($code); } }
public function testRandomString() { $str = Base64UrlSafe::encode(\random_bytes(32)); $sets = [[true, true], [true, false], [false, true], [false, false]]; foreach ($sets as $set) { $hidden = new HiddenString($str, $set[0], $set[1]); ob_start(); var_dump($hidden); $dump = ob_get_clean(); $this->assertFalse(\strpos($dump, $str)); $print = \print_r($hidden, true); $this->assertFalse(\strpos($print, $str)); $cast = (string) $hidden; if ($set[0]) { $this->assertFalse(\strpos($cast, $str)); } else { $this->assertNotFalse(\strpos($cast, $str)); } $serial = \serialize($hidden); if ($set[1]) { $this->assertFalse(\strpos($serial, $str)); } else { $this->assertNotFalse(\strpos($serial, $str)); } } }
public function testShortQueries() { try { $db = Database::factory($this->getConfig()); } catch (DBException $ex) { $this->markTestSkipped('Database not configured'); return; } $this->assertTrue($db instanceof Database); $db->beginTransaction(); $db->insert('test_values', ['name' => 'abc', 'foo' => true]); $db->insert('test_values', ['name' => 'def', 'foo' => false]); $db->insert('test_values', ['name' => 'ghijklmnopqrstuvwxyz', 'foo' => true]); $row = $db->row('SELECT * FROM test_values WHERE NOT foo'); $this->assertTrue(\is_array($row)); $name = $row['name']; $db->rollBack(); $db->beginTransaction(); $db->insert('test_values', ['name' => 'abcdef', 'foo' => true]); $db->insert('test_values', ['name' => 'GHI', 'foo' => false]); $db->insert('test_values', ['name' => 'jklmnopqrstuvwxyz', 'foo' => true]); $rows = $db->run('SELECT * FROM test_values WHERE NOT foo'); $this->assertTrue(\count($rows) === 1); $count = $db->cell('SELECT count(*) FROM test_values WHERE name = ?', 'GHI'); $this->assertEquals(\count($rows), $count); $count = $db->cell('SELECT count(*) FROM test_values WHERE name = ?', 'def'); $this->assertNotEquals(\count($rows), $count); $value = Base64UrlSafe::encode(\random_bytes(33)); $stored = $db->insertGet('test_values', ['name' => $value, 'foo' => true], 'name'); $this->assertSame($value, $stored); $db->commit(); }
/** * Get the metadata stored in the PHP archive. * * @param InstallFile $fileInfo * @return array */ protected function getMetadata(InstallFile $fileInfo) : array { $alias = Base64UrlSafe::encode(\random_bytes(33)) . '.phar'; $phar = new \Phar($fileInfo->getPath(), \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME); $phar->setAlias($alias); $metadata = $phar->getMetadata(); unset($phar); return $metadata; }
/** * @param int $userID * @return string */ public function createSessionCanary(int $userID) : string { $canary = Base64UrlSafe::encode(\random_bytes(33)); $this->db->beginTransaction(); $this->db->update('airship_users', ['session_canary' => $canary], ['userid' => $userID]); if ($this->db->commit()) { return $canary; } return ''; }
/** * @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'); }
public function testRandomString() { $str = Base64UrlSafe::encode(\random_bytes(32)); $hidden = new HiddenString($str); ob_start(); var_dump($hidden); $dump = ob_get_clean(); $this->assertFalse(\strpos($dump, $str)); $print = \print_r($hidden, true); $this->assertFalse(\strpos($print, $str)); $cast = (string) $hidden; $this->assertFalse(\strpos($cast, $str)); }
/** * We just need to replace the Phar * * If we get to this point: * * 1. We know the signature is signed by the supplier. * 2. The hash was checked into Keyggdrasil, which * was independently vouched for by our peers. * * @param UpdateInfo $info * @param UpdateFile $file * @throws CouldNotUpdate */ protected function install(UpdateInfo $info, UpdateFile $file) { if (!$file->hashMatches($info->getChecksum())) { throw new CouldNotUpdate(\__('Checksum mismatched')); } // Create a backup of the old Gadget: \rename($this->filePath, $this->filePath . '.backup'); \rename($file->getPath(), $this->filePath); $this->log('Begin install process', LogLevel::DEBUG, ['path' => $file->getPath(), 'hash' => $file->getHash(), 'version' => $file->getVersion(), 'size' => $file->getSize()]); // Get metadata from the old version of this Gadget: $oldAlias = Base64UrlSafe::encode(\random_bytes(48)) . '.phar'; $oldGadget = new \Phar($this->filePath, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME); $oldGadget->setAlias($oldAlias); $oldMetadata = $oldGadget->getMetadata(); unset($oldGadget); unset($oldAlias); // Let's open the update package: $newGadget = new \Phar($this->filePath, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME, $this->pharAlias); $newGadget->setAlias($this->pharAlias); $metaData = $newGadget->getMetadata(); // We need to do this while we're replacing files. $this->bringSiteDown(); Sandbox::safeRequire('phar://' . $this->pharAlias . '/update_trigger.php', $oldMetadata); // Free up the updater alias $garbageAlias = Base64UrlSafe::encode(\random_bytes(48)) . '.phar'; $newGadget->setAlias($garbageAlias); unset($newGadget); // Now bring it back up. $this->bringSiteBackUp(); // Make sure we update the version info. in the DB cache: $this->updateDBRecord('Gadget', $info); if ($metaData) { $this->updateJSON($info, $metaData); } self::$continuumLogger->store(LogLevel::INFO, 'Gadget update installed', $this->getLogContext($info, $file)); }
/** * 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; }
/** * Common signing process. User selects key, provides password. * * @param array $manifest * @return SignatureSecretKey * @throws \Exception */ protected function signPreamble(array $manifest) : SignatureSecretKey { $HTAB = \str_repeat(' ', \intdiv(self::TAB_SIZE, 2)); $supplier_name = $manifest['supplier']; // Sanity checks: if (!\array_key_exists('suppliers', $this->config)) { echo 'You are not authenticated for any suppliers.', "\n"; exit(255); } if (!\array_key_exists($supplier_name, $this->config['suppliers'])) { echo 'Check the supplier in the JSON file (', $supplier_name, ') for correctness.', 'Otherwise, you might need to log in.', "\n"; exit(255); } $supplier = $this->config['suppliers'][$supplier_name]; $numKeys = 0; if ($this->signWithMasterKeys) { $good_keys = []; // This should really not be used: $numKeys = \count($supplier['signing_keys']); foreach ($supplier['signing_keys'] as $k) { if (!empty($k['salt'])) { $good_keys[] = $k; ++$numKeys; } } } else { // This should be used instead: $good_keys = []; foreach ($supplier['signing_keys'] as $k) { if ($k['type'] === 'signing' && !empty($k['salt'])) { $good_keys[] = $k; ++$numKeys; } } } if ($numKeys > 1) { echo 'You have more than one signing key available.', "\n"; $n = 1; $size = (int) \floor(\log($numKeys, 10)); $key_associations = $HTAB . "ID\t Public Key " . \str_repeat(' ', 33) . "\t Type\n"; foreach ($supplier['signing_keys'] as $sign_key) { if (!$this->signWithMasterKeys && $sign_key['type'] === 'master') { continue; } $_n = \str_pad($n, $size, ' ', STR_PAD_LEFT); // Short format: $pk = Base64UrlSafe::encode(\Sodium\hex2bin($sign_key['public_key'])); $key_associations .= $HTAB . $_n . $HTAB . $pk . $HTAB . $sign_key['type'] . "\n"; ++$n; } // Let's ascertain the user's key selection do { echo $key_associations; $choice = (int) $this->prompt('Enter the ID for the key you wish to use: '); if ($choice < 1 || $choice > $numKeys) { $choice = null; } } while (empty($choice)); $supplierKey = $good_keys[$choice - 1]; echo "\n"; } else { $supplierKey = $good_keys[0]; } // The above !empty($k['salt']) check should have rendered this check redundant: if (empty($supplierKey['salt'])) { echo 'Salt not found for this key. It is not possible to reproduce it.', "\n"; exit(255); } // Short format: $pk = Base64UrlSafe::encode(\Sodium\hex2bin($supplierKey['public_key'])); // Color coded: Master keys are red, since they take longer. // We don't support signing packages with a master key, but // this decision could be undone in the future. $c = $supplierKey['type'] === 'master' ? $this->c['red'] : $this->c['yellow']; echo 'Selected ', $supplierKey['type'], ' key: ', $c, $pk, $this->c[''], "\n"; $password = $this->silentPrompt('Enter Password for Signing Key:'); // Derive and split the SignatureKeyPair from your password and salt $salt = \Sodium\hex2bin($supplierKey['salt']); switch ($supplierKey['type']) { case 'signing': $type = KeyFactory::MODERATE; echo 'Verifying (this may take a second or two)...'; break; case 'master': $type = KeyFactory::SENSITIVE; echo 'Verifying (this may take a few seconds)...'; break; default: $type = KeyFactory::INTERACTIVE; echo 'Verifying...'; } $keyPair = KeyFactory::deriveSignatureKeyPair($password, $salt, false, $type); $sign_secret = $keyPair->getSecretKey(); $sign_public = $keyPair->getPublicKey(); echo ' Done.', "\n"; // We don't need this anymore. \Sodium\memzero($password); // Check that the public key we derived from the password matches the one on file $pubKey = \Sodium\bin2hex($sign_public->getRawKeyMaterial()); if (!\hash_equals($supplierKey['public_key'], $pubKey)) { // Zero the memory ASAP unset($sign_secret); unset($sign_public); echo 'Invalid password for selected key', "\n"; exit(255); } // Zero the memory ASAP unset($sign_public); return $sign_secret; }
/** * Sign the new key with our current master key * * @param string $supplier * @param string $messageToSign * @return string[] * @throws \Exception */ protected function signNewKeyWithMasterKey(string $supplier, string $messageToSign) : array { $master_keys = []; foreach ($this->config['suppliers'][$supplier]['signing_keys'] as $key) { if ($key['type'] === 'master' && !empty($key['salt'])) { $master_keys[] = $key; } } // This shouldn't happen, but just in case: if (empty($master_keys)) { throw new \Exception('You cannot generate another key unless you already have a master key with the salt loaded locally.'); } // Select the correct master key. if (\count($master_keys) === 1) { $signingKey = $master_keys[0]; } else { echo 'Select which master key to use:'; do { foreach ($master_keys as $index => $key) { $pk = Base64UrlSafe::encode(\Sodium\hex2bin($key['public_key'])); echo $index + 1, "\t", $pk, "\n"; } $keyIndex = $this->prompt('Enter a number: '); if (empty($keyIndex)) { // Okay, let's cancel. throw new \Exception('Aborted.'); } if ($keyIndex < 1 || $keyIndex > \count($master_keys)) { $keyIndex = 0; echo 'Please enter a number between 1 and ', \count($master_keys), ".\n"; } } while ($keyIndex < 1); $signingKey = $master_keys[--$keyIndex]; } $signature = ''; $masterSalt = \Sodium\hex2bin($signingKey['salt']); do { $password = $this->silentPrompt('Enter the password for your master key: '); if (empty($password)) { // Okay, let's cancel. throw new \Exception('Aborted.'); } $masterKeyPair = KeyFactory::deriveSignatureKeyPair($password, $masterSalt, false, KeyFactory::SENSITIVE); // We must verify the public key matches: $masterPublicKey = $masterKeyPair->getPublicKey(); if (\hash_equals($masterPublicKey->getRawKeyMaterial(), \Sodium\hex2bin($signingKey['public_key']))) { $masterSecretKey = $masterKeyPair->getSecretKey(); // Setting $signature exits the loop $signature = Asymmetric::sign($messageToSign, $masterSecretKey); } else { echo 'Incorrect master key passphrase!', "\n"; } } while (!$signature); // We are returning two strings: return [$signature, $signingKey['public_key']]; }
exit(0); } $twigLoader = new \Twig_Loader_Filesystem(ROOT . '/Installer/skins'); $twigEnv = new \Twig_Environment($twigLoader); // Expose PHP's built-in functions as a filter $twigEnv->addFilter(new Twig_SimpleFilter('addslashes', 'addslashes')); $twigEnv->addFilter(new Twig_SimpleFilter('preg_quote', 'preg_quote')); $twigEnv->addFilter(new Twig_SimpleFilter('ceil', 'ceil')); $twigEnv->addFilter(new Twig_SimpleFilter('floor', 'floor')); $twigEnv->addFilter(new Twig_SimpleFilter('cachebust', function ($relative_path) { if ($relative_path[0] !== '/') { $relative_path = '/' . $relative_path; } $absolute = $_SERVER['DOCUMENT_ROOT'] . $relative_path; if (\is_readable($absolute)) { return $relative_path . '?' . Base64UrlSafe::encode(\Sodium\crypto_generichash(\file_get_contents($absolute) . \filemtime($absolute))); } return $relative_path . '?404NotFound'; })); $twigEnv->addFunction(new Twig_SimpleFunction('form_token', function ($lockTo = '') { static $csrf = null; if ($csrf === null) { $csrf = new \Airship\Engine\Security\CSRF(); } return $csrf->insertToken($lockTo); })); $twigEnv->addFunction(new Twig_SimpleFunction('cabin_url', function () { return '/'; })); $twigEnv->addFunction(new Twig_SimpleFunction('__', function (string $str = '') { // Not translating here.
/** * Cabin install process. * * 1. Extract files to proper directory. * 2. Run the update triggers (install hooks and incremental upgrades) * 3. Create/update relevant configuration files. * 4. Create symbolic links. * 5. Clear the cache files. * * @param InstallFile $fileInfo * @return bool */ public function install(InstallFile $fileInfo) : bool { $ns = $this->makeNamespace($this->supplier->getName(), $this->package); $alias = 'cabin.' . $this->supplier->getName() . '.' . $this->package . '.phar'; $updater = new \Phar($fileInfo->getPath(), \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME); $updater->setAlias($alias); $metadata = $updater->getMetadata(); // Overwrite files $updater->extractTo(ROOT . '/Cabin/' . $ns); // Run the update trigger. $updateTrigger = ROOT . '/Cabin/' . $ns . '/update_trigger.php'; if (\file_exists($updateTrigger)) { /** * @security Make sure arbitrary RCE isn't possible here. */ \shell_exec('php -dphar.readonly=0 ' . \escapeshellarg($updateTrigger) . ' >/dev/null 2>&1 &'); } // Free up the updater alias $garbageAlias = Base64UrlSafe::encode(\random_bytes(33)) . '.phar'; $updater->setAlias($garbageAlias); unset($updater); self::$continuumLogger->store(LogLevel::INFO, 'Cabin install successful', $this->getLogContext($fileInfo)); return $this->configure($ns, $metadata); }
/** * Let's install the automatic update. * * If we get to this point: * * 1. We know the signature is signed by * Paragon Initiative Enterprises, LLC. * 2. The hash was checked into Keyggdrasil, which * was independently vouched for by our peers. * * @param UpdateInfo $info * @param UpdateFile $file * @throws CouldNotUpdate */ protected function install(UpdateInfo $info, UpdateFile $file) { if (!$file->hashMatches($info->getChecksum())) { throw new CouldNotUpdate(\__('Checksum mismatched')); } // Let's open the update package: $path = $file->getPath(); $updater = new \Phar($path, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME); $updater->setAlias($this->pharAlias); $metadata = $updater->getMetadata(); // We need to do this while we're replacing files. $this->bringSiteDown(); if (isset($metadata['files'])) { foreach ($metadata['files'] as $fileName) { $this->replaceFile($fileName); } } if (isset($metadata['autoRun'])) { foreach ($metadata['autoRun'] as $autoRun) { $this->autoRunScript($autoRun); } } // If we included composer.lock, we need to install the dependencies. if (\in_array('composer.lock', $metadata['files'])) { $composers = ['/usr/bin/composer', '/usr/bin/composer.phar', \dirname(ROOT) . '/composer', \dirname(ROOT) . '/composer.phar']; $composerUpdated = false; foreach ($composers as $composer) { if (\file_exists($composer)) { $dir = \getcwd(); \chdir(\dirname(ROOT)); \shell_exec("{$composer} install"); \chdir($dir); $composerUpdated = true; break; } } if (!$composerUpdated) { self::$continuumLogger->store(LogLevel::INFO, 'Could not update dependencies. Please run Composer manually.', $this->getLogContext($info, $file)); } } // Free up the updater alias $garbageAlias = Base64UrlSafe::encode(\random_bytes(63)) . '.phar'; $updater->setAlias($garbageAlias); unset($updater); // Now bring it back up. $this->bringSiteBackUp(); self::$continuumLogger->store(LogLevel::INFO, 'CMS Airship core update installed', $this->getLogContext($info, $file)); }
/** * Install an updated version of a cabin * * If we get to this point: * * 1. We know the signature is signed by the supplier. * 2. The hash was checked into Keyggdrasil, which * was independently vouched for by our peers. * * @param UpdateInfo $info * @param UpdateFile $file * @throws CouldNotUpdate */ protected function install(UpdateInfo $info, UpdateFile $file) { if (!$file->hashMatches($info->getChecksum())) { throw new CouldNotUpdate(\__('Checksum mismatched')); } $path = $file->getPath(); $this->log('Begin Cabin updater', LogLevel::DEBUG, ['path' => $path, 'supplier' => $info->getSupplierName(), 'name' => $info->getPackageName()]); $updater = new \Phar($path, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME); $updater->setAlias($this->pharAlias); $ns = $this->makeNamespace($info->getSupplierName(), $info->getPackageName()); // We need to do this while we're replacing files. $this->bringCabinDown($ns); $oldMetadata = \Airship\loadJSON(ROOT . '/Cabin/' . $ns . '/manifest.json'); // Overwrite files $updater->extractTo(ROOT . '/Cabin/' . $ns, null, true); // Run the update trigger. Sandbox::safeInclude('phar://' . $this->pharAlias . '/update_trigger.php', $oldMetadata); // Free up the updater alias $garbageAlias = Base64UrlSafe::encode(\random_bytes(33)) . '.phar'; $updater->setAlias($garbageAlias); unset($updater); // Now bring it back up. $this->bringCabinBackUp($ns); // Make sure we update the version info. in the DB cache: $this->updateDBRecord('Cabin', $info); $this->log('Conclude Cabin updater', LogLevel::DEBUG, ['path' => $path, 'supplier' => $info->getSupplierName(), 'name' => $info->getPackageName()]); self::$continuumLogger->store(LogLevel::INFO, 'Cabin update installed', $this->getLogContext($info, $file)); }
<?php declare (strict_types=1); use ParagonIE\ConstantTime\Base64UrlSafe; /** * Used in automated deployment scripts. Generates, stores, then echos a random * PostgreSQL password, then sets up the web-based installer for step 2. */ require_once \dirname(__DIR__) . '/vendor/autoload.php'; $password = Base64UrlSafe::encode(\random_bytes(33)); \file_put_contents(\dirname(__DIR__) . '/src/tmp/installing.json', \json_encode(['step' => 2, 'database' => [[['driver' => 'pgsql', 'host' => $argv[1] ?? 'localhost', 'port' => $argv[2] ?? 5432, 'database' => 'airship', 'username' => 'airship', 'password' => $password]]]], JSON_PRETTY_PRINT)); echo $password;
/** * Compute an integer key for shared memory * * @param string $lookup * @return string */ public function getSHMKey(string $lookup) : string { return Base64UrlSafe::encode(\Sodium\crypto_shorthash($this->personalization . $lookup, $this->cacheKeyL) . \Sodium\crypto_shorthash($this->personalization . $lookup, $this->cacheKeyR)); }
/** * Generate, store, and return the index and token * * @param string $lockTo What URI endpoint this is valid for * @return array [string, string] */ protected function generateToken(string $lockTo = '') : array { // Create a distinct index: do { $index = Base64UrlSafe::encode(\random_bytes(18)); } while (isset($_SESSION[$this->sessionIndex][$index])); $token = Base64UrlSafe::encode(\random_bytes(33)); $_SESSION[$this->sessionIndex][$index] = ['created' => \intval(\date('YmdHis')), 'uri' => isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $_SERVER['SCRIPT_NAME'], 'token' => $token]; if (!empty($lockTo)) { // Get rid of trailing slashes. if (\preg_match('#/$#', $lockTo)) { $lockTo = Util::subString($lockTo, 0, Util::stringLength($lockTo) - 1); } $_SESSION[$this->sessionIndex][$index]['lockto'] = $lockTo; } $this->recycleTokens(); return [$index, $token]; }
/** * 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; }
/** * Handle user authentication * * @param array $post */ protected function processLogin(array $post = []) { $state = State::instance(); if (empty($post['username']) || empty($post['passphrase'])) { $this->lens('login', ['post_response' => ['message' => \__('Please fill out the form entirely'), 'status' => 'error']]); } $airBrake = Gears::get('AirBrake'); if (IDE_HACKS) { $airBrake = new AirBrake(); } if ($airBrake->failFast($post['username'], $_SERVER['REMOTE_ADDR'])) { $this->lens('login', ['post_response' => ['message' => \__('You are doing that too fast. Please wait a few seconds and try again.'), 'status' => 'error']]); } elseif (!$airBrake->getFastExit()) { $delay = $airBrake->getDelay($post['username'], $_SERVER['REMOTE_ADDR']); if ($delay > 0) { \usleep($delay * 1000); } } try { $userID = $this->airship_auth->login($post['username'], new HiddenString($post['passphrase'])); } catch (InvalidMessage $e) { $this->log('InvalidMessage Exception on Login; probable cause: password column was corrupted', LogLevel::CRITICAL, ['exception' => \Airship\throwableToArray($e)]); $this->lens('login', ['post_response' => ['message' => \__('Incorrect username or passphrase. Please try again.'), 'status' => 'error']]); } if (!empty($userID)) { $userID = (int) $userID; $user = $this->acct->getUserAccount($userID); if ($user['enable_2factor']) { if (empty($post['two_factor'])) { $post['two_factor'] = ''; } $gauth = $this->twoFactorPreamble($userID); $checked = $gauth->validateCode($post['two_factor'], \time()); if (!$checked) { $fails = $airBrake->getFailedLoginAttempts($post['username'], $_SERVER['REMOTE_ADDR']) + 1; // Instead of the password, seal a timestamped and // signed message saying the password was correct. // We use a signature with a key local to this Airship // so attackers can't just spam a string constant to // make the person decrypting these strings freak out // and assume the password was compromised. // // False positives are bad. This gives the sysadmin a // surefire way to reliably verify that a log entry is // due to two-factor authentication failing. $message = '**Note: The password was correct; ' . ' invalid 2FA token was provided.** ' . (new \DateTime('now'))->format(\AIRSHIP_DATE_FORMAT); $signed = Base64UrlSafe::encode(Asymmetric::sign($message, $state->keyring['notary.online_signing_key'], true)); $airBrake->registerLoginFailure($post['username'], $_SERVER['REMOTE_ADDR'], $fails, new HiddenString($signed . $message)); $this->lens('login', ['post_response' => ['message' => \__('Incorrect username or passphrase. Please try again.'), 'status' => 'error']]); } } if ($user['session_canary']) { $_SESSION['session_canary'] = $user['session_canary']; } elseif ($this->config('password-reset.logout')) { $_SESSION['session_canary'] = $this->acct->createSessionCanary($userID); } // Regenerate session ID: Session::regenerate(true); $_SESSION['userid'] = (int) $userID; if (!empty($post['remember'])) { $autoPilot = Gears::getName('AutoPilot'); if (IDE_HACKS) { $autoPilot = new AutoPilot(); } $httpsOnly = (bool) $autoPilot::isHTTPSConnection(); Cookie::setcookie('airship_token', Symmetric::encrypt($this->airship_auth->createAuthToken($userID), $state->keyring['cookie.encrypt_key']), \time() + ($state->universal['long-term-auth-expire'] ?? self::DEFAULT_LONGTERMAUTH_EXPIRE), '/', $state->universal['session_config']['cookie_domain'] ?? '', $httpsOnly ?? false, true); } \Airship\redirect($this->airship_cabin_prefix); } else { $fails = $airBrake->getFailedLoginAttempts($post['username'], $_SERVER['REMOTE_ADDR']) + 1; // If the server is setup (with an EncryptionPublicKey) and the // number of failures is above the log threshold, this will // encrypt the password guess with the public key so that only // the person in possession of the secret key can decrypt it. $airBrake->registerLoginFailure($post['username'], $_SERVER['REMOTE_ADDR'], $fails, new HiddenString($post['passphrase'])); $this->lens('login', ['post_response' => ['message' => \__('Incorrect username or passphrase. Please try again.'), 'status' => 'error']]); } }
/** * Get metadata from the Phar * * @param string $file * @return array */ public function getPharManifest(string $file) : array { $phar = new \Phar($file); $phar->setAlias(Base64UrlSafe::encode(\random_bytes(33))); $meta = $phar->getMetadata(); if (empty($meta)) { return []; } return $meta; }
/** * Create the admin user account */ protected function finalProcessAdminAccount() { if (!\array_key_exists('passphrase', $this->data['admin'])) { throw new \Exception(\__('Passphrase is not defined. This is a serious error.')); } $sessionCanary = Base64UrlSafe::encode(\random_bytes(33)); $userid = $this->db->insertGet('airship_users', ['username' => $this->data['admin']['username'], 'password' => $this->data['admin']['passphrase'], 'session_canary' => $sessionCanary, 'uniqueid' => \Airship\uniqueId()], 'userid'); $this->db->insert('airship_users_groups', ['userid' => $userid, 'groupid' => self::GROUP_ADMIN]); // Log in as the user $_SESSION['userid'] = $userid; $_SESSION['session_canary'] = $sessionCanary; }