<?php declare (strict_types=1); use Airship\Engine\LedgerStorage\{DBStore, FileStore}; 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) {
/** * 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']]); } }
<?php declare (strict_types=1); use Airship\Engine\{AutoPilot, Gears, State}; /** * This loads the Cabin configuration, and selects the active cabin. * * @global State $state */ $ap = Gears::getName('AutoPilot'); // Needed for IDE code completion: if (IDE_HACKS) { $ap = new AutoPilot(); } /** * Cache the cabin configuration */ $cabinDisabled = false; if (\file_exists(ROOT . '/tmp/cache/cabin_data.json')) { // Load the cabins from cache $config = \Airship\loadJSON(ROOT . '/tmp/cache/cabin_data.json'); foreach ($config['cabins'] as $key => $cabin) { if ($ap::isActiveCabinKey($key, !empty($cabin['https']))) { $state->active_cabin = $key; if ($cabin['enabled']) { $cabinDisabled = true; } break; } } $state->cabins = $config['cabins'];
<?php declare (strict_types=1); use Airship\Engine\{Database, Gears, State}; /** * Set up the Database objects from our database.json file * * @global State $state */ $dbgear = Gears::getName('Database'); $databases = \Airship\loadJSON(ROOT . '/config/databases.json'); $dbPool = []; $dbCount = 0; // Needed for IDE code completion: if (IDE_HACKS) { $dbgear = new Database(new \PDO('sqlite::memory:')); } /** * Initialize all of our database connections: */ foreach ($databases as $label => $dbs) { $dbPool[$label] = []; foreach ($dbs as $dbConf) { if (isset($dbConf['driver']) || isset($dbConf['dsn'])) { $conf = [isset($dbConf['dsn']) ? $dbConf['dsn'] : $dbConf]; if (isset($dbConf['username']) && isset($dbConf['password'])) { $conf[] = $dbConf['username']; $conf[] = $dbConf['password']; if (isset($dbConf['options'])) { $conf[] = $dbConf['options']; }
/** * Find probable collisions between patterns and cabin names, as well as hard-coded paths * in the current cabin. It does NOT look for collisions in custom pages, nor in page collisions * in foreign Cabins (outside of the Cabin itself). * * @param string $uri * @param string $cabin * @return bool * @throws \Airship\Alerts\GearNotFound * @throws \TypeError */ protected function detectCollisions(string $uri, string $cabin) : bool { $state = State::instance(); $ap = Gears::getName('AutoPilot'); if (!$ap instanceof AutoPilot) { throw new \TypeError(\__('AutoPilot Blueprint')); } $nop = []; foreach ($state->cabins as $pattern => $cab) { if ($cab === $cabin) { // Let's check each existing route in the current cabin for a collision foreach ($cab['data']['routes'] as $route => $landing) { $test = $ap::testLanding($ap::$patternPrefix . $route . '$', $uri, $nop, true); if ($test) { return true; } } } else { // Let's check each cabin route for a pattern $test = $ap::testLanding($ap::$patternPrefix . $pattern, $uri, $nop, true); if ($test) { return true; } } } return \preg_match('#^(static|js|img|fonts|css)/#', $uri) === 0; }
/** * Let's do an automatic login * * @param string $token * @param string $uid_idx * @param string $token_idx * @return bool * @throws LongTermAuthAlert (only in debug mode) * @throws \TypeError */ protected function doAutoLogin(string $token, string $uid_idx, string $token_idx) : bool { if (!$this->airship_auth instanceof Authentication) { $this->tightenSecurityBolt(); } $state = State::instance(); try { $userId = $this->airship_auth->loginByToken($token); \Sodium\memzero($token); if (!$this->verifySessionCanary($userId, false)) { return false; } // Regenerate session ID: Session::regenerate(true); // Set session variable $_SESSION[$uid_idx] = $userId; $autoPilot = Gears::getName('AutoPilot'); if (IDE_HACKS) { // We're using getName(), this is just to fool IDEs. $autoPilot = new AutoPilot(); } $httpsOnly = (bool) $autoPilot::isHTTPSConnection(); // Rotate the authentication token: Cookie::setcookie($token_idx, Symmetric::encrypt($this->airship_auth->rotateToken($token, $userId), $state->keyring['cookie.encrypt_key']), \time() + ($state->universal['long-term-auth-expire'] ?? self::DEFAULT_LONGTERMAUTH_EXPIRE), '/', '', $httpsOnly ?? false, true); return true; } catch (LongTermAuthAlert $e) { $state = State::instance(); // Let's wipe our long-term authentication cookies Cookie::setcookie($token_idx, null, 0, '/', '', $httpsOnly ?? false, true); // Let's log this incident if (\property_exists($this, 'log')) { $this->log($e->getMessage(), LogLevel::CRITICAL, ['exception' => \Airship\throwableToArray($e)]); } else { $state->logger->log(LogLevel::CRITICAL, $e->getMessage(), ['exception' => \Airship\throwableToArray($e)]); } // In debug mode, re-throw the exception: if ($state->universal['debug']) { throw $e; } } return false; }