diff options
Diffstat (limited to 'lib/private')
6 files changed, 91 insertions, 36 deletions
diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 0f1767e845b..c7e29568383 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -34,14 +34,18 @@ use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Exceptions\TokenPasswordExpiredException; use OC\Authentication\Exceptions\PasswordlessTokenException; use OC\Authentication\Exceptions\WipeTokenException; +use OCP\AppFramework\Db\TTransactional; use OCP\Cache\CappedMemoryCache; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; +use OCP\IDBConnection; use OCP\Security\ICrypto; use Psr\Log\LoggerInterface; class PublicKeyTokenProvider implements IProvider { + use TTransactional; + /** @var PublicKeyTokenMapper */ private $mapper; @@ -51,6 +55,8 @@ class PublicKeyTokenProvider implements IProvider { /** @var IConfig */ private $config; + private IDBConnection $db; + /** @var LoggerInterface */ private $logger; @@ -63,11 +69,13 @@ class PublicKeyTokenProvider implements IProvider { public function __construct(PublicKeyTokenMapper $mapper, ICrypto $crypto, IConfig $config, + IDBConnection $db, LoggerInterface $logger, ITimeFactory $time) { $this->mapper = $mapper; $this->crypto = $crypto; $this->config = $config; + $this->db = $db; $this->logger = $logger; $this->time = $time; @@ -111,8 +119,14 @@ class PublicKeyTokenProvider implements IProvider { $token = $this->mapper->getToken($this->hashToken($tokenId)); $this->cache[$token->getToken()] = $token; } catch (DoesNotExistException $ex) { - $this->cache[$tokenHash] = $ex; - throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex); + try { + $token = $this->mapper->getToken($this->hashTokenWithEmptySecret($tokenId)); + $this->cache[$token->getToken()] = $token; + $this->rotate($token, $tokenId, $tokenId); + } catch (DoesNotExistException $ex2) { + $this->cache[$tokenHash] = $ex2; + throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex); + } } } @@ -158,37 +172,39 @@ class PublicKeyTokenProvider implements IProvider { public function renewSessionToken(string $oldSessionId, string $sessionId): IToken { $this->cache->clear(); - $token = $this->getToken($oldSessionId); - - if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); - } - - $password = null; - if (!is_null($token->getPassword())) { - $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId); - $password = $this->decryptPassword($token->getPassword(), $privateKey); - } - - $newToken = $this->generateToken( - $sessionId, - $token->getUID(), - $token->getLoginName(), - $password, - $token->getName(), - IToken::TEMPORARY_TOKEN, - $token->getRemember() - ); + return $this->atomic(function () use ($oldSessionId, $sessionId) { + $token = $this->getToken($oldSessionId); - $this->mapper->delete($token); + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException("Invalid token type"); + } - return $newToken; + $password = null; + if (!is_null($token->getPassword())) { + $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId); + $password = $this->decryptPassword($token->getPassword(), $privateKey); + } + $newToken = $this->generateToken( + $sessionId, + $token->getUID(), + $token->getLoginName(), + $password, + $token->getName(), + IToken::TEMPORARY_TOKEN, + $token->getRemember() + ); + + $this->mapper->delete($token); + + return $newToken; + }, $this->db); } public function invalidateToken(string $token) { $this->cache->clear(); $this->mapper->invalidate($this->hashToken($token)); + $this->mapper->invalidate($this->hashTokenWithEmptySecret($token)); } public function invalidateTokenById(string $uid, int $id) { @@ -305,9 +321,14 @@ class PublicKeyTokenProvider implements IProvider { try { return $this->crypto->decrypt($cipherText, $token . $secret); } catch (\Exception $ex) { - // Delete the invalid token - $this->invalidateToken($token); - throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex); + // Retry with empty secret as a fallback for instances where the secret might not have been set by accident + try { + return $this->crypto->decrypt($cipherText, $token); + } catch (\Exception $ex2) { + // Delete the invalid token + $this->invalidateToken($token); + throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2); + } } } @@ -331,6 +352,13 @@ class PublicKeyTokenProvider implements IProvider { } /** + * @deprecated Fallback for instances where the secret might not have been set by accident + */ + private function hashTokenWithEmptySecret(string $token): string { + return hash('sha512', $token); + } + + /** * @throws \RuntimeException when OpenSSL reports a problem */ private function newToken(string $token, diff --git a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php index e9aa15e11b6..19d80218562 100644 --- a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php +++ b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php @@ -47,7 +47,7 @@ class ProviderUserAssignmentDao { /** * Get all assigned provider IDs for the given user ID * - * @return string[] where the array key is the provider ID (string) and the + * @return array<string, bool> where the array key is the provider ID (string) and the * value is the enabled state (bool) */ public function getState(string $uid): array { @@ -59,7 +59,7 @@ class ProviderUserAssignmentDao { $result = $query->execute(); $providers = []; foreach ($result->fetchAll() as $row) { - $providers[$row['provider_id']] = 1 === (int)$row['enabled']; + $providers[(string)$row['provider_id']] = 1 === (int)$row['enabled']; } $result->closeCursor(); diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php index 66e7c090e42..37a9f03d073 100644 --- a/lib/private/Authentication/TwoFactorAuth/Manager.php +++ b/lib/private/Authentication/TwoFactorAuth/Manager.php @@ -170,10 +170,10 @@ class Manager { * * @todo remove in Nextcloud 17 as by then all providers should have been updated * - * @param string[] $providerStates + * @param array<string, bool> $providerStates * @param IProvider[] $providers * @param IUser $user - * @return string[] the updated $providerStates variable + * @return array<string, bool> the updated $providerStates variable */ private function fixMissingProviderStates(array $providerStates, array $providers, IUser $user): array { diff --git a/lib/private/Security/Crypto.php b/lib/private/Security/Crypto.php index e9ef4417925..aeeafcc271c 100644 --- a/lib/private/Security/Crypto.php +++ b/lib/private/Security/Crypto.php @@ -122,9 +122,22 @@ class Crypto implements ICrypto { * @throws Exception If the decryption failed */ public function decrypt(string $authenticatedCiphertext, string $password = ''): string { - if ($password === '') { - $password = $this->config->getSystemValue('secret'); + $secret = $this->config->getSystemValue('secret'); + try { + if ($password === '') { + return $this->decryptWithoutSecret($authenticatedCiphertext, $secret); + } + return $this->decryptWithoutSecret($authenticatedCiphertext, $password); + } catch (Exception $e) { + if ($password === '') { + // Retry with empty secret as a fallback for instances where the secret might not have been set by accident + return $this->decryptWithoutSecret($authenticatedCiphertext, ''); + } + throw $e; } + } + + private function decryptWithoutSecret(string $authenticatedCiphertext, string $password = ''): string { $hmacKey = $encryptionKey = $password; $parts = explode('|', $authenticatedCiphertext); diff --git a/lib/private/Security/Hasher.php b/lib/private/Security/Hasher.php index 5b3fc2b47a9..4731ba96bd3 100644 --- a/lib/private/Security/Hasher.php +++ b/lib/private/Security/Hasher.php @@ -137,6 +137,15 @@ class Hasher implements IHasher { return true; } + // Verify whether it matches a legacy PHPass or SHA1 string + // Retry with empty passwordsalt for cases where it was not set + $hashLength = \strlen($hash); + if (($hashLength === 60 && password_verify($message, $hash)) || + ($hashLength === 40 && hash_equals($hash, sha1($message)))) { + $newHash = $this->hash($message); + return true; + } + return false; } diff --git a/lib/private/Security/VerificationToken/VerificationToken.php b/lib/private/Security/VerificationToken/VerificationToken.php index c85e0e7b5a1..2d3f902b622 100644 --- a/lib/private/Security/VerificationToken/VerificationToken.php +++ b/lib/private/Security/VerificationToken/VerificationToken.php @@ -84,10 +84,15 @@ class VerificationToken implements IVerificationToken { try { $decryptedToken = $this->crypto->decrypt($encryptedToken, $passwordPrefix.$this->config->getSystemValue('secret')); } catch (\Exception $e) { - $this->throwInvalidTokenException(InvalidTokenException::TOKEN_DECRYPTION_ERROR); + // Retry with empty secret as a fallback for instances where the secret might not have been set by accident + try { + $decryptedToken = $this->crypto->decrypt($encryptedToken, $passwordPrefix); + } catch (\Exception $e2) { + $this->throwInvalidTokenException(InvalidTokenException::TOKEN_DECRYPTION_ERROR); + } } - $splitToken = explode(':', $decryptedToken ?? ''); + $splitToken = explode(':', $decryptedToken); if (count($splitToken) !== 2) { $this->throwInvalidTokenException(InvalidTokenException::TOKEN_INVALID_FORMAT); } |