Пример #1
0
 /**
  * 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);
     }
 }
Пример #2
0
 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));
         }
     }
 }
Пример #3
0
 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();
 }
Пример #4
0
 /**
  * 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;
 }
Пример #5
0
 /**
  * @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);
         }
     }
 }
Пример #7
0
 /**
  * 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;
         }
     }
 }
Пример #8
0
 /**
  * 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');
 }
Пример #9
0
 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));
 }
Пример #10
0
 /**
  * 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));
 }
Пример #11
0
 /**
  * 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;
 }
Пример #12
0
 /**
  * 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;
 }
Пример #13
0
 /**
  * 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']];
 }
Пример #14
0
    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.
Пример #15
0
 /**
  * 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);
 }
Пример #16
0
 /**
  * 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));
 }
Пример #17
0
 /**
  * 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));
 }
Пример #18
0
<?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;
Пример #19
0
 /**
  * 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));
 }
Пример #20
0
 /**
  * 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];
 }
Пример #21
0
 /**
  * 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;
 }
Пример #22
0
 /**
  * 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']]);
     }
 }
Пример #23
0
 /**
  * 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;
 }
Пример #24
0
 /**
  * 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;
 }