diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 43e2b75aa6..7772988660 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -422,6 +422,9 @@ return array( 'OC\\Authentication\\Token\\IProvider' => $baseDir . '/lib/private/Authentication/Token/IProvider.php', 'OC\\Authentication\\Token\\IToken' => $baseDir . '/lib/private/Authentication/Token/IToken.php', 'OC\\Authentication\\Token\\Manager' => $baseDir . '/lib/private/Authentication/Token/Manager.php', + 'OC\\Authentication\\Token\\PublicKeyToken' => $baseDir . '/lib/private/Authentication/Token/PublicKeyToken.php', + 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', + 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => $baseDir . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Avatar' => $baseDir . '/lib/private/Avatar.php', 'OC\\AvatarManager' => $baseDir . '/lib/private/AvatarManager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 34d9f3a73e..be9c71d824 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -452,6 +452,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Authentication\\Token\\IProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IProvider.php', 'OC\\Authentication\\Token\\IToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/IToken.php', 'OC\\Authentication\\Token\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/Manager.php', + 'OC\\Authentication\\Token\\PublicKeyToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyToken.php', + 'OC\\Authentication\\Token\\PublicKeyTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenMapper.php', + 'OC\\Authentication\\Token\\PublicKeyTokenProvider' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/PublicKeyTokenProvider.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Manager.php', 'OC\\Avatar' => __DIR__ . '/../../..' . '/lib/private/Avatar.php', 'OC\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/AvatarManager.php', diff --git a/lib/private/Authentication/Token/DefaultToken.php b/lib/private/Authentication/Token/DefaultToken.php index 67aa89ea66..29c4b5a74a 100644 --- a/lib/private/Authentication/Token/DefaultToken.php +++ b/lib/private/Authentication/Token/DefaultToken.php @@ -37,6 +37,7 @@ use OCP\AppFramework\Db\Entity; * @method void setRemember(int $remember) * @method void setLastActivity(int $lastactivity) * @method int getLastActivity() + * @method void setVersion(int $version) */ class DefaultToken extends Entity implements IToken { @@ -73,6 +74,9 @@ class DefaultToken extends Entity implements IToken { /** @var int */ protected $expires; + /** @var int */ + protected $version; + public function __construct() { $this->addType('uid', 'string'); $this->addType('loginName', 'string'); @@ -85,6 +89,9 @@ class DefaultToken extends Entity implements IToken { $this->addType('lastCheck', 'int'); $this->addType('scope', 'string'); $this->addType('expires', 'int'); + $this->addType('version', 'int'); + + $this->setVersion(1); } public function getId(): int { diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index a67d7d151e..0a3f32ffb4 100644 --- a/lib/private/Authentication/Token/DefaultTokenMapper.php +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -50,8 +50,8 @@ class DefaultTokenMapper extends QBMapper { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->where($qb->expr()->eq('token', $qb->createParameter('token'))) - ->setParameter('token', $token) + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -66,6 +66,7 @@ class DefaultTokenMapper extends QBMapper { ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -79,9 +80,10 @@ class DefaultTokenMapper extends QBMapper { public function getToken(string $token): DefaultToken { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $result = $qb->select('*') + $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') ->from('authtoken') ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -102,9 +104,10 @@ class DefaultTokenMapper extends QBMapper { public function getTokenById(int $id): DefaultToken { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $result = $qb->select('*') + $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') ->from('authtoken') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))) ->execute(); $data = $result->fetch(); @@ -127,9 +130,10 @@ class DefaultTokenMapper extends QBMapper { public function getTokenByUser(IUser $user): array { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('*') + $qb->select('id', 'uid', 'login_name', 'password', 'name', 'token', 'type', 'remember', 'last_activity', 'last_check', 'scope', 'expires', 'version') ->from('authtoken') ->where($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); $result = $qb->execute(); $data = $result->fetchAll(); @@ -151,7 +155,8 @@ class DefaultTokenMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) - ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))); + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); $qb->execute(); } @@ -163,7 +168,8 @@ class DefaultTokenMapper extends QBMapper { public function deleteByName(string $name) { $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)); + ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))); $qb->execute(); } diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index 4465c288c8..85fe91cdf1 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -76,9 +76,9 @@ class Manager implements IProvider { public function updateToken(IToken $token) { if ($token instanceof DefaultToken) { $this->defaultTokenProvider->updateToken($token); + } else { + throw new InvalidTokenException(); } - - throw new InvalidTokenException(); } /** @@ -90,9 +90,9 @@ class Manager implements IProvider { public function updateTokenActivity(IToken $token) { if ($token instanceof DefaultToken) { $this->defaultTokenProvider->updateTokenActivity($token); + } else { + throw new InvalidTokenException(); } - - throw new InvalidTokenException(); } /** diff --git a/lib/private/Authentication/Token/PublicKeyToken.php b/lib/private/Authentication/Token/PublicKeyToken.php new file mode 100644 index 0000000000..18b2707577 --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyToken.php @@ -0,0 +1,216 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Authentication\Token; + +use OCP\AppFramework\Db\Entity; + +/** + * @method void setId(int $id) + * @method void setUid(string $uid); + * @method void setLoginName(string $loginname) + * @method void setName(string $name) + * @method string getToken() + * @method void setType(int $type) + * @method int getType() + * @method void setRemember(int $remember) + * @method void setLastActivity(int $lastactivity) + * @method int getLastActivity() + * @method string getPrivateKey() + * @method void setPrivateKey(string $key) + * @method string getPublicKey() + * @method void setPublicKey(string $key) + * @method void setVersion(int $version) + */ +class PublicKeyToken extends Entity implements IToken { + + /** @var string user UID */ + protected $uid; + + /** @var string login name used for generating the token */ + protected $loginName; + + /** @var string encrypted user password */ + protected $password; + + /** @var string token name (e.g. browser/OS) */ + protected $name; + + /** @var string */ + protected $token; + + /** @var int */ + protected $type; + + /** @var int */ + protected $remember; + + /** @var int */ + protected $lastActivity; + + /** @var int */ + protected $lastCheck; + + /** @var string */ + protected $scope; + + /** @var int */ + protected $expires; + + /** @var string */ + protected $privateKey; + + /** @var string */ + protected $publicKey; + + /** @var int */ + protected $version; + + public function __construct() { + $this->addType('uid', 'string'); + $this->addType('loginName', 'string'); + $this->addType('password', 'string'); + $this->addType('name', 'string'); + $this->addType('token', 'string'); + $this->addType('type', 'int'); + $this->addType('remember', 'int'); + $this->addType('lastActivity', 'int'); + $this->addType('lastCheck', 'int'); + $this->addType('scope', 'string'); + $this->addType('expires', 'int'); + $this->addType('publicKey', 'string'); + $this->addType('privateKey', 'string'); + $this->addType('version', 'int'); + + $this->setVersion(2); + } + + public function getId(): int { + return $this->id; + } + + public function getUID(): string { + return $this->uid; + } + + /** + * Get the login name used when generating the token + * + * @return string + */ + public function getLoginName(): string { + return parent::getLoginName(); + } + + /** + * Get the (encrypted) login password + * + * @return string|null + */ + public function getPassword() { + return parent::getPassword(); + } + + public function jsonSerialize() { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'lastActivity' => $this->lastActivity, + 'type' => $this->type, + 'scope' => $this->getScopeAsArray() + ]; + } + + /** + * Get the timestamp of the last password check + * + * @return int + */ + public function getLastCheck(): int { + return parent::getLastCheck(); + } + + /** + * Get the timestamp of the last password check + * + * @param int $time + */ + public function setLastCheck(int $time) { + parent::setLastCheck($time); + } + + public function getScope(): string { + $scope = parent::getScope(); + if ($scope === null) { + return ''; + } + + return $scope; + } + + public function getScopeAsArray(): array { + $scope = json_decode($this->getScope(), true); + if (!$scope) { + return [ + 'filesystem'=> true + ]; + } + return $scope; + } + + public function setScope($scope) { + if (\is_array($scope)) { + parent::setScope(json_encode($scope)); + } else { + parent::setScope((string)$scope); + } + } + + public function getName(): string { + return parent::getName(); + } + + public function getRemember(): int { + return parent::getRemember(); + } + + public function setToken(string $token) { + parent::setToken($token); + } + + public function setPassword(string $password = null) { + parent::setPassword($password); + } + + public function setExpires($expires) { + parent::setExpires($expires); + } + + /** + * @return int|null + */ + public function getExpires() { + return parent::getExpires(); + } +} diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php new file mode 100644 index 0000000000..0d5657cb58 --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -0,0 +1,167 @@ + + * + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Authentication\Token; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IUser; + +class PublicKeyTokenMapper extends QBMapper { + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'authtoken'); + } + + /** + * Invalidate (delete) a given token + * + * @param string $token + */ + public function invalidate(string $token) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->execute(); + } + + /** + * @param int $olderThan + * @param int $remember + */ + public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->execute(); + } + + /** + * Get the user UID for the given token + * + * @throws DoesNotExistException + */ + public function getToken(string $token): PublicKeyToken { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('*') + ->from('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->execute(); + + $data = $result->fetch(); + $result->closeCursor(); + if ($data === false) { + throw new DoesNotExistException('token does not exist'); + } + return PublicKeyToken::fromRow($data); + } + + /** + * Get the token for $id + * + * @throws DoesNotExistException + */ + public function getTokenById(int $id): PublicKeyToken { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('*') + ->from('authtoken') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->execute(); + + $data = $result->fetch(); + $result->closeCursor(); + if ($data === false) { + throw new DoesNotExistException('token does not exist'); + } + return PublicKeyToken::fromRow($data); + } + + /** + * Get all tokens of a user + * + * The provider may limit the number of result rows in case of an abuse + * where a high number of (session) tokens is generated + * + * @param IUser $user + * @return DefaultToken[] + */ + public function getTokenByUser(IUser $user): array { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('authtoken') + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))) + ->setMaxResults(1000); + $result = $qb->execute(); + $data = $result->fetchAll(); + $result->closeCursor(); + + $entities = array_map(function ($row) { + return PublicKeyToken::fromRow($row); + }, $data); + + return $entities; + } + + /** + * @param IUser $user + * @param int $id + */ + public function deleteById(IUser $user, int $id) { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))); + $qb->execute(); + } + + /** + * delete all auth token which belong to a specific client if the client was deleted + * + * @param string $name + */ + public function deleteByName(string $name) { + $qb = $this->db->getQueryBuilder(); + $qb->delete('authtoken') + ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT))); + $qb->execute(); + } + +} diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php new file mode 100644 index 0000000000..d7e9038a07 --- /dev/null +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -0,0 +1,265 @@ + + * + * @author Roeland Jago Douma + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OC\Authentication\Token; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\ILogger; +use OCP\IUser; +use OCP\Security\ICrypto; + +class PublicKeyTokenProvider implements IProvider { + /** @var PublicKeyTokenMapper */ + private $mapper; + + /** @var ICrypto */ + private $crypto; + + /** @var IConfig */ + private $config; + + /** @var ILogger $logger */ + private $logger; + + /** @var ITimeFactory $time */ + private $time; + + public function __construct(PublicKeyTokenMapper $mapper, + ICrypto $crypto, + IConfig $config, + ILogger $logger, + ITimeFactory $time) { + $this->mapper = $mapper; + $this->crypto = $crypto; + $this->config = $config; + $this->logger = $logger; + $this->time = $time; + } + + public function generateToken(string $token, + string $uid, + string $loginName, + $password, + string $name, + int $type = IToken::TEMPORARY_TOKEN, + int $remember = IToken::DO_NOT_REMEMBER): IToken { + $dbToken = new PublicKeyToken(); + $dbToken->setUid($uid); + $dbToken->setLoginName($loginName); + + $config = [ + 'digest_alg' => 'sha512', + 'private_key_bits' => 2048, + ]; + + // Generate new key + $res = openssl_pkey_new($config); + openssl_pkey_export($res, $privateKey); + + // 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)); + + if (!is_null($password)) { + $dbToken->setPassword($this->encryptPassword($password, $publicKey)); + } + + $dbToken->setName($name); + $dbToken->setToken($this->hashToken($token)); + $dbToken->setType($type); + $dbToken->setRemember($remember); + $dbToken->setLastActivity($this->time->getTime()); + $dbToken->setLastCheck($this->time->getTime()); + + $this->mapper->insert($dbToken); + + return $dbToken; + } + + public function getToken(string $tokenId): IToken { + try { + $token = $this->mapper->getToken($this->hashToken($tokenId)); + } catch (DoesNotExistException $ex) { + throw new InvalidTokenException(); + } + + if ($token->getExpires() !== null && $token->getExpires() < $this->time->getTime()) { + throw new ExpiredTokenException($token); + } + + return $token; + } + + public function getTokenById(int $tokenId): IToken { + try { + $token = $this->mapper->getTokenById($tokenId); + } catch (DoesNotExistException $ex) { + throw new InvalidTokenException(); + } + + if ($token->getExpires() !== null && $token->getExpires() < $this->time->getTime()) { + throw new ExpiredTokenException($token); + } + + return $token; + } + + public function renewSessionToken(string $oldSessionId, string $sessionId) { + $token = $this->getToken($oldSessionId); + + $password = null; + if (!is_null($token->getPassword())) { + $password = $this->decryptPassword($token->getPassword(), $oldSessionId); + } + + $this->generateToken( + $sessionId, + $token->getUID(), + $token->getLoginName(), + $password, + $token->getName(), + IToken::TEMPORARY_TOKEN, + $token->getRemember() + ); + + $this->mapper->delete($token); + } + + public function invalidateToken(string $token) { + $this->mapper->invalidate($this->hashToken($token)); + } + + public function invalidateTokenById(IUser $user, int $id) { + $this->mapper->deleteById($user, $id); + } + + public function invalidateOldTokens() { + $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24); + $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']); + $this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER); + $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); + $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']); + $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER); + } + + public function updateToken(IToken $token) { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + $this->mapper->update($token); + } + + public function updateTokenActivity(IToken $token) { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + /** @var DefaultToken $token */ + $now = $this->time->getTime(); + if ($token->getLastActivity() < ($now - 60)) { + // Update token only once per minute + $token->setLastActivity($now); + $this->mapper->update($token); + } + } + + public function getTokenByUser(IUser $user): array { + return $this->mapper->getTokenByUser($user); + } + + public function getPassword(IToken $token, string $tokenId): string { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + // Decrypt private key with tokenId + $privateKey = $this->decrypt($token->getPrivateKey(), $tokenId); + + // Decrypt password with private key + return $this->decryptPassword($token->getPassword(), $privateKey); + } + + public function setPassword(IToken $token, string $tokenId, string $password) { + // Kill all temp tokens except the current token + + // Update pass for all permanent tokens by rencrypting + } + + public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken { + if (!($token instanceof PublicKeyToken)) { + throw new InvalidTokenException(); + } + + // 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 { + $secret = $this->config->getSystemValue('secret'); + return $this->crypto->encrypt($plaintext, $token . $secret); + } + + /** + * @throws InvalidTokenException + */ + private function decrypt(string $cipherText, string $token): string { + $secret = $this->config->getSystemValue('secret'); + try { + return $this->crypto->decrypt($cipherText, $token . $secret); + } catch (\Exception $ex) { + // Delete the invalid token + $this->invalidateToken($token); + throw new InvalidTokenException(); + } + } + + private function encryptPassword(string $password, string $publicKey): string { + openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); + + return $encryptedPassword; + } + + private function decryptPassword(string $encryptedPassword, string $privateKey): string { + openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING); + + return $password; + } + + private function hashToken(string $token): string { + $secret = $this->config->getSystemValue('secret'); + return hash('sha512', $token . $secret); + } +}