public function testVerify() { $value = 'value'; $signature = 'signature'; $publicKey = 'public_key'; $encryptedValue = 'encrypted_value'; $this->encryption->setPublicKey($publicKey); $this->algorithm->expects($this->once())->method('verify')->with($value, $signature, $publicKey)->will($this->returnValue($encryptedValue)); $this->encryption->verify($value, $signature); }
/** * This method stores the necessary bits of data in this object. * * @param Channel $chan * @param array $updateData * @return void * @throws CouldNotUpdate * @throws NoSupplier */ protected function unpackMessageUpdate(Channel $chan, array $updateData) { // This is the JSON message from the tree node, stored as an array: $this->updateMessage = $updateData; if ($this->isPackageUpdate() || $this->isAirshipUpdate()) { // These aren't signed for updating the tree. return; } // We need a precise format: $dateGen = (new \DateTime($this->stored['date_generated']))->format(\AIRSHIP_DATE_FORMAT); $messageToSign = ['action' => $this->action, 'date_generated' => $dateGen, 'public_key' => $updateData['public_key'], 'supplier' => $updateData['supplier'], 'type' => $updateData['type']]; try { $this->supplier = $this->loadSupplier($chan, $updateData); } catch (NoSupplier $ex) { if (!$this->isNewSupplier) { throw $ex; } } // If this isn't a new supplier, we need to verify the key if ($this->isNewSupplier) { return; } if ($updateData['master'] === null) { throw new CouldNotUpdate(\__('The master data is NULL, but the supplier exists.')); } $master = \json_decode($updateData['master'], true); foreach ($this->supplier->getSigningKeys() as $supKey) { // Yes, this is (in fact) a SignaturePublicKey: if (IDE_HACKS) { $supKey['key'] = new SignaturePublicKey(); } if ($supKey['type'] !== 'master') { continue; } $pub = \Sodium\bin2hex($supKey['key']->getRawKeyMaterial()); // Is this the key we're looking for? if (\hash_equals($pub, $master['public_key'])) { // Store the public key $this->supplierMasterKeyUsed = $supKey['key']; break; } } if (empty($this->supplierMasterKeyUsed)) { throw new CouldNotUpdate(\__('The provided public key does not match any known master key.')); } $encoded = \json_encode($messageToSign); if (!Asymmetric::verify($encoded, $this->supplierMasterKeyUsed, $master['signature'])) { throw new CouldNotUpdate(\__('Invalid signature for this master key.')); } }
/** * Register a failed login attempt * * @param string $username * @param string $ip * @param int $numFailures * @param HiddenString|null $password * @return bool */ public function registerLoginFailure(string $username, string $ip, int $numFailures = 0, HiddenString $password = null) : bool { $logAfter = $this->config['log-after'] ?? null; if (!\is_null($logAfter)) { $logAfter = (int) $logAfter; } $publicKey = (string) ($this->config['log-public-key'] ?? ''); $this->db->beginTransaction(); $inserts = ['action' => self::ACTION_LOGIN, 'occurred' => (new \DateTime())->format(\AIRSHIP_DATE_FORMAT), 'username' => $username, 'ipaddress' => $ip, 'subnet' => $this->getSubnet($ip)]; if (\is_int($logAfter) && !empty($publicKey)) { if ($numFailures >= $logAfter) { // Encrypt the password guess with the admin's public key $inserts['sealed_password'] = Asymmetric::seal($password->getString(), $this->getLogPublicKey($publicKey)); } } $this->db->insert('airship_failed_logins', $inserts); return $this->db->commit(); }
/** * 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/verify/parse a JSON response * * The _server_ is the one that signs the message. * We're just verifying the Ed25519 signature. * * @param string $url * @param SignaturePublicKey $publicKey * @param array $args * @param array $options * @return array * @throws \Exception */ public static function postSignedJSON(string $url, SignaturePublicKey $publicKey, array $args = [], array $options = []) : array { $body = self::post($url, $args, $options); if (empty($body)) { throw new \Exception('Empty response from ' . $url); } if (self::$debug) { \var_dump($body); } $firstNewLine = \strpos($body, "\n"); // There should be a newline immediately after the base64urlsafe-encoded signature if ($firstNewLine !== self::ENCODED_SIGNATURE_LENGTH) { throw new \Exception('Invalid Signature'); } $sig = Base64UrlSafe::decode(Binary::safeSubstr($body, 0, self::ENCODED_SIGNATURE_LENGTH)); $msg = Binary::safeSubstr($body, self::ENCODED_SIGNATURE_LENGTH + 1); if (!Asymmetric::verify($msg, $publicKey, $sig, true)) { throw new \Exception('Invalid Signature'); } return \json_decode($msg, true); }
/** * Parse a signed JSON response * * @param Response $response * @param SignaturePublicKey $publicKey * @return mixed * @throws SignatureFailed * @throws TransferException */ public function parseSignedJSON(Response $response, SignaturePublicKey $publicKey) { $code = $response->getStatusCode(); if ($code >= 200 && $code < 300) { $body = (string) $response->getBody(); $firstNewLine = \strpos($body, "\n"); // There should be a newline immediately after the base64urlsafe-encoded signature if ($firstNewLine !== self::ENCODED_SIGNATURE_LENGTH) { throw new SignatureFailed(\sprintf("First newline found at position %s, expected %d.\n%s", \print_r($firstNewLine, true), \print_r(self::ENCODED_SIGNATURE_LENGTH, true), Base64::encode($body))); } $sig = Base64UrlSafe::decode(Binary::safeSubstr($body, 0, 88)); $msg = Binary::safeSubstr($body, 89); if (!Asymmetric::verify($msg, $publicKey, $sig, true)) { throw new SignatureFailed(); } return \Airship\parseJSON($msg, true); } throw new TransferException(); }
/** * @covers Asymmetric::sign() * @covers Asymmetric::verify() */ public function testSignFail() { $alice = KeyFactory::generateSignatureKeyPair(); $message = 'test message'; $signature = Asymmetric::sign($message, $alice->getSecretKey(), true); $this->assertFalse(Asymmetric::verify('wrongmessage', $alice->getPublicKey(), $signature, true)); $_signature = $signature; // Let's flip one bit, randomly: $r = \Sodium\randombytes_uniform(\mb_strlen($_signature, '8bit')); $_signature[$r] = \chr(\ord($_signature[$r]) ^ 1 << \Sodium\randombytes_uniform(8)); $this->assertFalse(Asymmetric::verify($message, $alice->getPublicKey(), $_signature, true)); }