2018-05-18 17:48:08 +00:00
< ? php
2019-12-03 18:57:53 +00:00
2018-05-18 17:48:08 +00:00
declare ( strict_types = 1 );
/**
2024-05-23 07:26:56 +00:00
* SPDX - FileCopyrightText : 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2018-05-18 17:48:08 +00:00
*/
namespace OC\Authentication\Token ;
2018-10-30 12:18:41 +00:00
use OC\Authentication\Exceptions\ExpiredTokenException ;
2018-05-18 17:48:08 +00:00
use OC\Authentication\Exceptions\InvalidTokenException ;
use OC\Authentication\Exceptions\PasswordlessTokenException ;
2019-10-07 12:05:57 +00:00
use OC\Authentication\Exceptions\TokenPasswordExpiredException ;
2019-04-03 14:00:46 +00:00
use OC\Authentication\Exceptions\WipeTokenException ;
2018-05-18 17:48:08 +00:00
use OCP\AppFramework\Db\DoesNotExistException ;
2022-10-02 12:11:41 +00:00
use OCP\AppFramework\Db\TTransactional ;
2018-05-18 17:48:08 +00:00
use OCP\AppFramework\Utility\ITimeFactory ;
2024-01-11 14:42:19 +00:00
use OCP\Authentication\Token\IToken as OCPIToken ;
2024-02-28 09:27:00 +00:00
use OCP\ICache ;
use OCP\ICacheFactory ;
2018-05-18 17:48:08 +00:00
use OCP\IConfig ;
2022-10-02 12:11:41 +00:00
use OCP\IDBConnection ;
2023-01-04 10:23:43 +00:00
use OCP\IUserManager ;
2018-05-18 17:48:08 +00:00
use OCP\Security\ICrypto ;
2022-12-01 17:06:58 +00:00
use OCP\Security\IHasher ;
2020-10-12 15:14:25 +00:00
use Psr\Log\LoggerInterface ;
2018-05-18 17:48:08 +00:00
class PublicKeyTokenProvider implements IProvider {
2023-02-08 21:59:18 +00:00
public const TOKEN_MIN_LENGTH = 22 ;
2024-02-28 09:27:00 +00:00
/** Token cache TTL in seconds */
private const TOKEN_CACHE_TTL = 10 ;
2023-02-08 21:59:18 +00:00
2022-10-02 12:11:41 +00:00
use TTransactional ;
2018-05-18 17:48:08 +00:00
/** @var PublicKeyTokenMapper */
private $mapper ;
/** @var ICrypto */
private $crypto ;
/** @var IConfig */
private $config ;
2022-10-02 12:11:41 +00:00
private IDBConnection $db ;
2020-10-12 15:14:25 +00:00
/** @var LoggerInterface */
2018-05-18 17:48:08 +00:00
private $logger ;
2020-10-12 15:14:25 +00:00
/** @var ITimeFactory */
2018-05-18 17:48:08 +00:00
private $time ;
2024-02-28 09:27:00 +00:00
/** @var ICache */
2019-10-08 11:57:36 +00:00
private $cache ;
2024-02-28 09:27:00 +00:00
/** @var IHasher */
private $hasher ;
2022-12-01 17:06:58 +00:00
2018-05-18 17:48:08 +00:00
public function __construct ( PublicKeyTokenMapper $mapper ,
ICrypto $crypto ,
IConfig $config ,
2022-10-02 12:11:41 +00:00
IDBConnection $db ,
2020-10-12 15:14:25 +00:00
LoggerInterface $logger ,
2022-12-01 17:06:58 +00:00
ITimeFactory $time ,
2024-02-28 09:27:00 +00:00
IHasher $hasher ,
ICacheFactory $cacheFactory ) {
2018-05-18 17:48:08 +00:00
$this -> mapper = $mapper ;
$this -> crypto = $crypto ;
$this -> config = $config ;
2022-10-02 12:11:41 +00:00
$this -> db = $db ;
2018-05-18 17:48:08 +00:00
$this -> logger = $logger ;
$this -> time = $time ;
2019-10-08 11:57:36 +00:00
2024-02-28 09:27:00 +00:00
$this -> cache = $cacheFactory -> isLocalCacheAvailable ()
? $cacheFactory -> createLocal ( 'authtoken_' )
: $cacheFactory -> createInMemory ();
2022-12-01 17:06:58 +00:00
$this -> hasher = $hasher ;
2018-05-18 17:48:08 +00:00
}
2019-07-18 09:36:50 +00:00
/**
* { @ inheritDoc }
*/
2018-05-18 17:48:08 +00:00
public function generateToken ( string $token ,
string $uid ,
string $loginName ,
2021-10-13 08:54:44 +00:00
? string $password ,
2018-05-18 17:48:08 +00:00
string $name ,
2024-01-11 14:42:19 +00:00
int $type = OCPIToken :: TEMPORARY_TOKEN ,
2024-07-19 13:53:46 +00:00
int $remember = OCPIToken :: DO_NOT_REMEMBER ,
? array $scope = null ,
) : OCPIToken {
2023-02-08 21:59:18 +00:00
if ( strlen ( $token ) < self :: TOKEN_MIN_LENGTH ) {
$exception = new InvalidTokenException ( 'Token is too short, minimum of ' . self :: TOKEN_MIN_LENGTH . ' characters is required, ' . strlen ( $token ) . ' characters given' );
$this -> logger -> error ( 'Invalid token provided when generating new token' , [ 'exception' => $exception ]);
throw $exception ;
}
2022-03-22 09:51:54 +00:00
if ( mb_strlen ( $name ) > 128 ) {
2022-05-09 06:36:34 +00:00
$name = mb_substr ( $name , 0 , 120 ) . '…' ;
2022-03-22 09:51:54 +00:00
}
2023-02-04 21:35:35 +00:00
// We need to check against one old token to see if there is a password
// hash that we can reuse for detecting outdated passwords
$randomOldToken = $this -> mapper -> getFirstTokenForUser ( $uid );
2023-03-13 09:32:53 +00:00
$oldTokenMatches = $randomOldToken && $randomOldToken -> getPasswordHash () && $password !== null && $this -> hasher -> verify ( sha1 ( $password ) . $password , $randomOldToken -> getPasswordHash ());
2023-02-04 21:35:35 +00:00
2018-05-31 19:56:17 +00:00
$dbToken = $this -> newToken ( $token , $uid , $loginName , $password , $name , $type , $remember );
2023-02-04 21:35:35 +00:00
if ( $oldTokenMatches ) {
$dbToken -> setPasswordHash ( $randomOldToken -> getPasswordHash ());
}
2024-07-19 13:53:46 +00:00
if ( $scope !== null ) {
$dbToken -> setScope ( $scope );
}
2018-05-18 17:48:08 +00:00
$this -> mapper -> insert ( $dbToken );
2023-02-04 21:35:35 +00:00
if ( ! $oldTokenMatches && $password !== null ) {
$this -> updatePasswords ( $uid , $password );
}
2019-10-08 11:57:36 +00:00
// Add the token to the cache
2024-02-28 09:27:00 +00:00
$this -> cacheToken ( $dbToken );
2019-10-08 11:57:36 +00:00
2018-05-18 17:48:08 +00:00
return $dbToken ;
}
2024-01-11 14:42:19 +00:00
public function getToken ( string $tokenId ) : OCPIToken {
2023-02-08 21:45:23 +00:00
/**
* Token length : 72
* @ see \OC\Core\Controller\ClientFlowLoginController :: generateAppPassword
* @ see \OC\Core\Controller\AppPasswordController :: getAppPassword
* @ see \OC\Core\Command\User\AddAppPassword :: execute
* @ see \OC\Core\Service\LoginFlowV2Service :: flowDone
* @ see \OCA\Talk\MatterbridgeManager :: generatePassword
* @ see \OCA\Preferred_Providers\Controller\PasswordController :: generateAppPassword
* @ see \OCA\GlobalSiteSelector\TokenHandler :: generateAppPassword
*
2023-02-08 21:59:18 +00:00
* Token length : 22 - 256 - https :// www . php . net / manual / en / session . configuration . php #ini.session.sid-length
2023-02-08 21:45:23 +00:00
* @ see \OC\User\Session :: createSessionToken
*
* Token length : 29
* @ see \OCA\Settings\Controller\AuthSettingsController :: generateRandomDeviceToken
* @ see \OCA\Registration\Service\RegistrationService :: generateAppPassword
*/
2023-02-08 21:59:18 +00:00
if ( strlen ( $tokenId ) < self :: TOKEN_MIN_LENGTH ) {
2023-02-08 21:45:23 +00:00
throw new InvalidTokenException ( 'Token is too short for a generated token, should be the password during basic auth' );
}
2019-10-08 11:57:36 +00:00
$tokenHash = $this -> hashToken ( $tokenId );
2024-02-28 09:27:00 +00:00
if ( $token = $this -> getTokenFromCache ( $tokenHash )) {
$this -> checkToken ( $token );
return $token ;
}
2019-10-08 11:57:36 +00:00
2024-02-28 09:27:00 +00:00
try {
$token = $this -> mapper -> getToken ( $tokenHash );
$this -> cacheToken ( $token );
} catch ( DoesNotExistException $ex ) {
2019-10-08 11:57:36 +00:00
try {
2024-02-28 09:27:00 +00:00
$token = $this -> mapper -> getToken ( $this -> hashTokenWithEmptySecret ( $tokenId ));
$this -> rotate ( $token , $tokenId , $tokenId );
} catch ( DoesNotExistException ) {
$this -> cacheInvalidHash ( $tokenHash );
throw new InvalidTokenException ( 'Token does not exist: ' . $ex -> getMessage (), 0 , $ex );
2019-10-08 11:57:36 +00:00
}
2018-05-18 17:48:08 +00:00
}
2024-02-28 09:27:00 +00:00
$this -> checkToken ( $token );
2018-05-18 17:48:08 +00:00
2024-02-28 09:27:00 +00:00
return $token ;
}
2019-04-03 14:00:46 +00:00
2024-02-28 09:27:00 +00:00
/**
* @ throws InvalidTokenException when token doesn ' t exist
*/
private function getTokenFromCache ( string $tokenHash ) : ? PublicKeyToken {
$serializedToken = $this -> cache -> get ( $tokenHash );
2024-04-29 10:45:44 +00:00
if ( $serializedToken === false ) {
2024-07-10 11:15:20 +00:00
return null ;
2024-04-29 10:45:44 +00:00
}
2024-02-28 09:27:00 +00:00
2024-04-29 10:45:44 +00:00
if ( $serializedToken === null ) {
2024-02-28 09:27:00 +00:00
return null ;
2019-10-07 12:05:57 +00:00
}
2024-02-28 09:27:00 +00:00
$token = unserialize ( $serializedToken , [
'allowed_classes' => [ PublicKeyToken :: class ],
]);
return $token instanceof PublicKeyToken ? $token : null ;
}
private function cacheToken ( PublicKeyToken $token ) : void {
$this -> cache -> set ( $token -> getToken (), serialize ( $token ), self :: TOKEN_CACHE_TTL );
}
2024-04-29 10:45:44 +00:00
private function cacheInvalidHash ( string $tokenHash ) : void {
2024-02-28 09:27:00 +00:00
// Invalid entries can be kept longer in cache since it’ s unlikely to reuse them
2024-04-29 10:45:44 +00:00
$this -> cache -> set ( $tokenHash , false , self :: TOKEN_CACHE_TTL * 2 );
2018-05-18 17:48:08 +00:00
}
2024-01-11 14:42:19 +00:00
public function getTokenById ( int $tokenId ) : OCPIToken {
2018-05-18 17:48:08 +00:00
try {
$token = $this -> mapper -> getTokenById ( $tokenId );
} catch ( DoesNotExistException $ex ) {
2020-05-27 07:21:47 +00:00
throw new InvalidTokenException ( " Token with ID $tokenId does not exist: " . $ex -> getMessage (), 0 , $ex );
2018-05-18 17:48:08 +00:00
}
2024-02-28 09:27:00 +00:00
$this -> checkToken ( $token );
return $token ;
}
private function checkToken ( $token ) : void {
2018-09-20 07:54:27 +00:00
if (( int ) $token -> getExpires () !== 0 && $token -> getExpires () < $this -> time -> getTime ()) {
2018-05-18 17:48:08 +00:00
throw new ExpiredTokenException ( $token );
}
2024-01-11 14:42:19 +00:00
if ( $token -> getType () === OCPIToken :: WIPE_TOKEN ) {
2019-04-03 14:00:46 +00:00
throw new WipeTokenException ( $token );
}
2019-10-07 12:05:57 +00:00
if ( $token -> getPasswordInvalid () === true ) {
//The password is invalid we should throw an TokenPasswordExpiredException
throw new TokenPasswordExpiredException ( $token );
}
2018-05-18 17:48:08 +00:00
}
2024-01-11 14:42:19 +00:00
public function renewSessionToken ( string $oldSessionId , string $sessionId ) : OCPIToken {
2022-10-02 12:11:41 +00:00
return $this -> atomic ( function () use ( $oldSessionId , $sessionId ) {
$token = $this -> getToken ( $oldSessionId );
2018-05-18 17:48:08 +00:00
2022-10-02 12:11:41 +00:00
if ( ! ( $token instanceof PublicKeyToken )) {
throw new InvalidTokenException ( 'Invalid token type' );
}
2019-10-08 09:01:53 +00:00
2022-10-02 12:11:41 +00:00
$password = null ;
if ( ! is_null ( $token -> getPassword ())) {
$privateKey = $this -> decrypt ( $token -> getPrivateKey (), $oldSessionId );
$password = $this -> decryptPassword ( $token -> getPassword (), $privateKey );
}
2024-07-19 13:53:46 +00:00
$scope = $token -> getScope () === '' ? null : $token -> getScopeAsArray ();
2022-10-02 12:11:41 +00:00
$newToken = $this -> generateToken (
$sessionId ,
$token -> getUID (),
$token -> getLoginName (),
$password ,
$token -> getName (),
2024-01-11 14:42:19 +00:00
OCPIToken :: TEMPORARY_TOKEN ,
2024-07-19 13:53:46 +00:00
$token -> getRemember (),
$scope ,
2022-10-02 12:11:41 +00:00
);
2024-02-28 09:27:00 +00:00
$this -> cacheToken ( $newToken );
2022-10-02 12:11:41 +00:00
2024-02-28 09:27:00 +00:00
$this -> cacheInvalidHash ( $token -> getToken ());
2022-10-02 12:11:41 +00:00
$this -> mapper -> delete ( $token );
return $newToken ;
}, $this -> db );
2018-05-18 17:48:08 +00:00
}
public function invalidateToken ( string $token ) {
2024-02-28 09:27:00 +00:00
$tokenHash = $this -> hashToken ( $token );
2018-05-18 17:48:08 +00:00
$this -> mapper -> invalidate ( $this -> hashToken ( $token ));
2022-03-09 09:52:27 +00:00
$this -> mapper -> invalidate ( $this -> hashTokenWithEmptySecret ( $token ));
2024-02-28 09:27:00 +00:00
$this -> cacheInvalidHash ( $tokenHash );
2018-05-18 17:48:08 +00:00
}
2018-05-29 07:29:29 +00:00
public function invalidateTokenById ( string $uid , int $id ) {
2024-02-28 09:27:00 +00:00
$token = $this -> mapper -> getTokenById ( $id );
if ( $token -> getUID () !== $uid ) {
return ;
}
$this -> mapper -> invalidate ( $token -> getToken ());
$this -> cacheInvalidHash ( $token -> getToken ());
2019-10-08 11:57:36 +00:00
2018-05-18 17:48:08 +00:00
}
public function invalidateOldTokens () {
2023-04-05 10:50:08 +00:00
$olderThan = $this -> time -> getTime () - $this -> config -> getSystemValueInt ( 'session_lifetime' , 60 * 60 * 24 );
2018-05-18 17:48:08 +00:00
$this -> logger -> debug ( 'Invalidating session tokens older than ' . date ( 'c' , $olderThan ), [ 'app' => 'cron' ]);
2024-05-07 17:30:11 +00:00
$this -> mapper -> invalidateOld ( $olderThan , OCPIToken :: TEMPORARY_TOKEN , OCPIToken :: DO_NOT_REMEMBER );
2023-04-05 10:50:08 +00:00
$rememberThreshold = $this -> time -> getTime () - $this -> config -> getSystemValueInt ( 'remember_login_cookie_lifetime' , 60 * 60 * 24 * 15 );
2018-05-18 17:48:08 +00:00
$this -> logger -> debug ( 'Invalidating remembered session tokens older than ' . date ( 'c' , $rememberThreshold ), [ 'app' => 'cron' ]);
2024-05-07 17:30:11 +00:00
$this -> mapper -> invalidateOld ( $rememberThreshold , OCPIToken :: TEMPORARY_TOKEN , OCPIToken :: REMEMBER );
$wipeThreshold = $this -> time -> getTime () - $this -> config -> getSystemValueInt ( 'token_auth_wipe_token_retention' , 60 * 60 * 24 * 60 );
$this -> logger -> debug ( 'Invalidating auth tokens marked for remote wipe older than ' . date ( 'c' , $wipeThreshold ), [ 'app' => 'cron' ]);
$this -> mapper -> invalidateOld ( $wipeThreshold , OCPIToken :: WIPE_TOKEN );
$authTokenThreshold = $this -> time -> getTime () - $this -> config -> getSystemValueInt ( 'token_auth_token_retention' , 60 * 60 * 24 * 365 );
$this -> logger -> debug ( 'Invalidating auth tokens older than ' . date ( 'c' , $authTokenThreshold ), [ 'app' => 'cron' ]);
$this -> mapper -> invalidateOld ( $authTokenThreshold , OCPIToken :: PERMANENT_TOKEN );
2018-05-18 17:48:08 +00:00
}
2023-08-25 05:07:57 +00:00
public function invalidateLastUsedBefore ( string $uid , int $before ) : void {
$this -> mapper -> invalidateLastUsedBefore ( $uid , $before );
}
2024-01-11 14:42:19 +00:00
public function updateToken ( OCPIToken $token ) {
2018-05-18 17:48:08 +00:00
if ( ! ( $token instanceof PublicKeyToken )) {
2020-05-27 07:21:47 +00:00
throw new InvalidTokenException ( 'Invalid token type' );
2018-05-18 17:48:08 +00:00
}
$this -> mapper -> update ( $token );
2024-02-28 09:27:00 +00:00
$this -> cacheToken ( $token );
2018-05-18 17:48:08 +00:00
}
2024-01-11 14:42:19 +00:00
public function updateTokenActivity ( OCPIToken $token ) {
2018-05-18 17:48:08 +00:00
if ( ! ( $token instanceof PublicKeyToken )) {
2020-05-27 07:21:47 +00:00
throw new InvalidTokenException ( 'Invalid token type' );
2018-05-18 17:48:08 +00:00
}
2020-09-18 10:34:43 +00:00
$activityInterval = $this -> config -> getSystemValueInt ( 'token_auth_activity_update' , 60 );
$activityInterval = min ( max ( $activityInterval , 0 ), 300 );
2020-10-09 12:33:17 +00:00
/** @var PublicKeyToken $token */
2018-05-18 17:48:08 +00:00
$now = $this -> time -> getTime ();
2020-09-18 10:34:43 +00:00
if ( $token -> getLastActivity () < ( $now - $activityInterval )) {
2018-05-18 17:48:08 +00:00
$token -> setLastActivity ( $now );
2021-10-21 12:12:36 +00:00
$this -> mapper -> updateActivity ( $token , $now );
2024-02-28 09:27:00 +00:00
$this -> cacheToken ( $token );
2018-05-18 17:48:08 +00:00
}
}
2018-05-29 07:29:29 +00:00
public function getTokenByUser ( string $uid ) : array {
return $this -> mapper -> getTokenByUser ( $uid );
2018-05-18 17:48:08 +00:00
}
2024-01-11 14:42:19 +00:00
public function getPassword ( OCPIToken $savedToken , string $tokenId ) : string {
2020-08-19 15:54:00 +00:00
if ( ! ( $savedToken instanceof PublicKeyToken )) {
2020-05-27 07:21:47 +00:00
throw new InvalidTokenException ( 'Invalid token type' );
2018-05-18 17:48:08 +00:00
}
2020-08-19 15:54:00 +00:00
if ( $savedToken -> getPassword () === null ) {
2018-05-29 07:24:20 +00:00
throw new PasswordlessTokenException ();
}
2018-05-18 17:48:08 +00:00
// Decrypt private key with tokenId
2020-08-19 15:54:00 +00:00
$privateKey = $this -> decrypt ( $savedToken -> getPrivateKey (), $tokenId );
2018-05-18 17:48:08 +00:00
// Decrypt password with private key
2020-08-19 15:54:00 +00:00
return $this -> decryptPassword ( $savedToken -> getPassword (), $privateKey );
2018-05-18 17:48:08 +00:00
}
2024-01-11 14:42:19 +00:00
public function setPassword ( OCPIToken $token , string $tokenId , string $password ) {
2018-05-29 10:18:10 +00:00
if ( ! ( $token instanceof PublicKeyToken )) {
2020-05-27 07:21:47 +00:00
throw new InvalidTokenException ( 'Invalid token type' );
2018-05-29 10:18:10 +00:00
}
2023-04-11 15:27:57 +00:00
$this -> atomic ( function () use ( $password , $token ) {
// When changing passwords all temp tokens are deleted
$this -> mapper -> deleteTempToken ( $token );
// Update the password for all tokens
$tokens = $this -> mapper -> getTokenByUser ( $token -> getUID ());
$hashedPassword = $this -> hashPassword ( $password );
foreach ( $tokens as $t ) {
$publicKey = $t -> getPublicKey ();
$t -> setPassword ( $this -> encryptPassword ( $password , $publicKey ));
$t -> setPasswordHash ( $hashedPassword );
$this -> updateToken ( $t );
}
}, $this -> db );
2018-05-18 17:48:08 +00:00
}
2022-12-01 17:06:58 +00:00
private function hashPassword ( string $password ) : string {
return $this -> hasher -> hash ( sha1 ( $password ) . $password );
}
2024-01-11 14:42:19 +00:00
public function rotate ( OCPIToken $token , string $oldTokenId , string $newTokenId ) : OCPIToken {
2018-05-18 17:48:08 +00:00
if ( ! ( $token instanceof PublicKeyToken )) {
2020-05-27 07:21:47 +00:00
throw new InvalidTokenException ( 'Invalid token type' );
2018-05-18 17:48:08 +00:00
}
// Decrypt private key with oldTokenId
$privateKey = $this -> decrypt ( $token -> getPrivateKey (), $oldTokenId );
// Encrypt with the new token
$token -> setPrivateKey ( $this -> encrypt ( $privateKey , $newTokenId ));
$token -> setToken ( $this -> hashToken ( $newTokenId ));
$this -> updateToken ( $token );
return $token ;
}
private function encrypt ( string $plaintext , string $token ) : string {
2023-04-05 10:50:08 +00:00
$secret = $this -> config -> getSystemValueString ( 'secret' );
2018-05-18 17:48:08 +00:00
return $this -> crypto -> encrypt ( $plaintext , $token . $secret );
}
/**
* @ throws InvalidTokenException
*/
private function decrypt ( string $cipherText , string $token ) : string {
2023-04-05 10:50:08 +00:00
$secret = $this -> config -> getSystemValueString ( 'secret' );
2018-05-18 17:48:08 +00:00
try {
return $this -> crypto -> decrypt ( $cipherText , $token . $secret );
} catch ( \Exception $ex ) {
2022-03-09 09:52:27 +00:00
// 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 );
}
2018-05-18 17:48:08 +00:00
}
}
private function encryptPassword ( string $password , string $publicKey ) : string {
openssl_public_encrypt ( $password , $encryptedPassword , $publicKey , OPENSSL_PKCS1_OAEP_PADDING );
2018-05-31 19:56:17 +00:00
$encryptedPassword = base64_encode ( $encryptedPassword );
2018-05-18 17:48:08 +00:00
return $encryptedPassword ;
}
private function decryptPassword ( string $encryptedPassword , string $privateKey ) : string {
2018-05-31 19:56:17 +00:00
$encryptedPassword = base64_decode ( $encryptedPassword );
2018-05-18 17:48:08 +00:00
openssl_private_decrypt ( $encryptedPassword , $password , $privateKey , OPENSSL_PKCS1_OAEP_PADDING );
return $password ;
}
private function hashToken ( string $token ) : string {
2023-04-05 10:50:08 +00:00
$secret = $this -> config -> getSystemValueString ( 'secret' );
2018-05-18 17:48:08 +00:00
return hash ( 'sha512' , $token . $secret );
}
2018-05-31 19:56:17 +00:00
2022-03-09 09:52:27 +00:00
/**
2024-09-18 21:51:06 +00:00
* @ deprecated 26.0 . 0 Fallback for instances where the secret might not have been set by accident
2022-03-09 09:52:27 +00:00
*/
private function hashTokenWithEmptySecret ( string $token ) : string {
return hash ( 'sha512' , $token );
}
2019-07-18 09:36:50 +00:00
/**
* @ throws \RuntimeException when OpenSSL reports a problem
*/
2018-05-31 19:56:17 +00:00
private function newToken ( string $token ,
string $uid ,
string $loginName ,
$password ,
string $name ,
int $type ,
int $remember ) : PublicKeyToken {
$dbToken = new PublicKeyToken ();
$dbToken -> setUid ( $uid );
$dbToken -> setLoginName ( $loginName );
2018-09-16 09:51:15 +00:00
$config = array_merge ([
2018-05-31 19:56:17 +00:00
'digest_alg' => 'sha512' ,
2022-07-05 09:37:14 +00:00
'private_key_bits' => $password !== null && strlen ( $password ) > 250 ? 4096 : 2048 ,
2018-09-16 09:51:15 +00:00
], $this -> config -> getSystemValue ( 'openssl' , []));
2018-05-31 19:56:17 +00:00
// Generate new key
$res = openssl_pkey_new ( $config );
2018-12-06 20:27:57 +00:00
if ( $res === false ) {
$this -> logOpensslError ();
2019-07-18 09:36:50 +00:00
throw new \RuntimeException ( 'OpenSSL reported a problem' );
2018-12-06 20:27:57 +00:00
}
2019-07-21 20:21:59 +00:00
if ( openssl_pkey_export ( $res , $privateKey , null , $config ) === false ) {
$this -> logOpensslError ();
throw new \RuntimeException ( 'OpenSSL reported a problem' );
}
2018-05-31 19:56:17 +00:00
// Extract the public key from $res to $pubKey
$publicKey = openssl_pkey_get_details ( $res );
$publicKey = $publicKey [ 'key' ];
$dbToken -> setPublicKey ( $publicKey );
$dbToken -> setPrivateKey ( $this -> encrypt ( $privateKey , $token ));
2022-07-05 09:25:44 +00:00
if ( ! is_null ( $password ) && $this -> config -> getSystemValueBool ( 'auth.storeCryptedPassword' , true )) {
2023-01-04 10:23:43 +00:00
if ( strlen ( $password ) > IUserManager :: MAX_PASSWORD_LENGTH ) {
2022-07-05 09:37:14 +00:00
throw new \RuntimeException ( 'Trying to save a password with more than 469 characters is not supported. If you want to use big passwords, disable the auth.storeCryptedPassword option in config.php' );
}
2018-05-31 19:56:17 +00:00
$dbToken -> setPassword ( $this -> encryptPassword ( $password , $publicKey ));
2022-12-01 17:06:58 +00:00
$dbToken -> setPasswordHash ( $this -> hashPassword ( $password ));
2018-05-31 19:56:17 +00:00
}
$dbToken -> setName ( $name );
$dbToken -> setToken ( $this -> hashToken ( $token ));
$dbToken -> setType ( $type );
$dbToken -> setRemember ( $remember );
$dbToken -> setLastActivity ( $this -> time -> getTime ());
$dbToken -> setLastCheck ( $this -> time -> getTime ());
2018-06-04 07:51:13 +00:00
$dbToken -> setVersion ( PublicKeyToken :: VERSION );
2018-05-31 19:56:17 +00:00
return $dbToken ;
}
2018-09-26 11:10:17 +00:00
2024-01-11 14:42:19 +00:00
public function markPasswordInvalid ( OCPIToken $token , string $tokenId ) {
2018-09-26 11:10:17 +00:00
if ( ! ( $token instanceof PublicKeyToken )) {
2020-05-27 07:21:47 +00:00
throw new InvalidTokenException ( 'Invalid token type' );
2018-09-26 11:10:17 +00:00
}
$token -> setPasswordInvalid ( true );
$this -> mapper -> update ( $token );
2024-02-28 09:27:00 +00:00
$this -> cacheToken ( $token );
2018-09-26 11:10:17 +00:00
}
2018-09-26 11:36:04 +00:00
public function updatePasswords ( string $uid , string $password ) {
2021-07-09 07:35:12 +00:00
// prevent setting an empty pw as result of pw-less-login
2022-07-13 13:27:55 +00:00
if ( $password === '' || ! $this -> config -> getSystemValueBool ( 'auth.storeCryptedPassword' , true )) {
2021-07-09 07:35:12 +00:00
return ;
}
2023-04-11 15:27:57 +00:00
$this -> atomic ( function () use ( $password , $uid ) {
// Update the password for all tokens
$tokens = $this -> mapper -> getTokenByUser ( $uid );
$newPasswordHash = null ;
/**
* - true : The password hash could not be verified anymore
* and the token needs to be updated with the newly encrypted password
* - false : The hash could still be verified
* - missing : The hash needs to be verified
*/
$hashNeedsUpdate = [];
foreach ( $tokens as $t ) {
if ( ! isset ( $hashNeedsUpdate [ $t -> getPasswordHash ()])) {
if ( $t -> getPasswordHash () === null ) {
$hashNeedsUpdate [ $t -> getPasswordHash () ? : '' ] = true ;
} elseif ( ! $this -> hasher -> verify ( sha1 ( $password ) . $password , $t -> getPasswordHash ())) {
$hashNeedsUpdate [ $t -> getPasswordHash () ? : '' ] = true ;
} else {
$hashNeedsUpdate [ $t -> getPasswordHash () ? : '' ] = false ;
}
2023-01-09 15:12:01 +00:00
}
2023-04-11 15:27:57 +00:00
$needsUpdating = $hashNeedsUpdate [ $t -> getPasswordHash () ? : '' ] ? ? true ;
if ( $needsUpdating ) {
if ( $newPasswordHash === null ) {
$newPasswordHash = $this -> hashPassword ( $password );
}
$publicKey = $t -> getPublicKey ();
$t -> setPassword ( $this -> encryptPassword ( $password , $publicKey ));
$t -> setPasswordHash ( $newPasswordHash );
$t -> setPasswordInvalid ( false );
$this -> updateToken ( $t );
2023-01-09 14:58:26 +00:00
}
2022-08-08 15:41:52 +00:00
}
2023-02-04 21:35:35 +00:00
2023-04-11 15:27:57 +00:00
// If password hashes are different we update them all to be equal so
// that the next execution only needs to verify once
if ( count ( $hashNeedsUpdate ) > 1 ) {
$newPasswordHash = $this -> hashPassword ( $password );
$this -> mapper -> updateHashesForUser ( $uid , $newPasswordHash );
}
}, $this -> db );
2018-09-26 11:36:04 +00:00
}
2018-12-06 20:27:57 +00:00
private function logOpensslError () {
$errors = [];
while ( $error = openssl_error_string ()) {
$errors [] = $error ;
}
$this -> logger -> critical ( 'Something is wrong with your openssl setup: ' . implode ( ', ' , $errors ));
}
2018-05-18 17:48:08 +00:00
}