| 
23 | 23 | 
 
  | 
24 | 24 | use OCP\Authentication\IApacheBackend;  | 
25 | 25 | use OCP\EventDispatcher\GenericEvent;  | 
 | 26 | +use OCP\Authentication\IProvideUserSecretBackend;  | 
26 | 27 | use OCP\EventDispatcher\IEventDispatcher;  | 
27 | 28 | use OCP\Files\NotPermittedException;  | 
28 | 29 | use OCP\IConfig;  | 
 | 
40 | 41 | use OCP\User\Events\UserChangedEvent;  | 
41 | 42 | use OCP\UserInterface;  | 
42 | 43 | 
 
  | 
43 |  | -class UserBackend implements IApacheBackend, UserInterface, IUserBackend, IGetDisplayNameBackend {  | 
 | 44 | +class UserBackend implements IApacheBackend, UserInterface, IUserBackend, IGetDisplayNameBackend, IProvideUserSecretBackend {  | 
44 | 45 | 	/** @var IConfig */  | 
45 | 46 | 	private $config;  | 
46 | 47 | 	/** @var IURLGenerator */  | 
@@ -148,10 +149,63 @@ public function createUserIfNotExists($uid, array $attributes = []) {  | 
148 | 149 | 			}  | 
149 | 150 | 			$qb->execute();  | 
150 | 151 | 
 
  | 
 | 152 | +			// If we use per-user encryption the keys must be initialized first  | 
 | 153 | +			$userSecret = $this->getUserSecret($uid, $attributes);  | 
 | 154 | +			if ($userSecret !== null) {  | 
 | 155 | +				$this->updateUserSecretHash($uid, $userSecret);  | 
 | 156 | +				// Emit a post login action to initialize the encryption module with the user secret provided by the idp.  | 
 | 157 | +				\OC_Hook::emit('OC_User', 'post_login', ['run' => true, 'uid' => $uid, 'password' => $userSecret, 'isTokenLogin' => false]);  | 
 | 158 | +			}  | 
151 | 159 | 			$this->initializeHomeDir($uid);  | 
152 | 160 | 		}  | 
153 | 161 | 	}  | 
154 | 162 | 
 
  | 
 | 163 | +	private function getUserSecretHash($uid) {  | 
 | 164 | +		$qb = $this->db->getQueryBuilder();  | 
 | 165 | +		$qb->select('token')  | 
 | 166 | +			->from('user_saml_auth_token')  | 
 | 167 | +			->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))  | 
 | 168 | +			->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash')))  | 
 | 169 | +			->setMaxResults(10);  | 
 | 170 | +		$result = $qb->execute();  | 
 | 171 | +		$data = $result->fetchAll();  | 
 | 172 | +		$result->closeCursor();  | 
 | 173 | +		return $data;  | 
 | 174 | +	}  | 
 | 175 | + | 
 | 176 | +	private function checkUserSecretHash($uid, $userSecret) {  | 
 | 177 | +		$data = $this->getUserSecretHash($uid);  | 
 | 178 | +		foreach($data as $row) {  | 
 | 179 | +			$storedHash = $row['token'];  | 
 | 180 | +			if (\OC::$server->getHasher()->verify($userSecret, $storedHash, $newHash)) {  | 
 | 181 | +				if (!empty($newHash)) {  | 
 | 182 | +					$this->updateUserSecretHash($uid, $userSecret, true);  | 
 | 183 | +				}  | 
 | 184 | +				return true;  | 
 | 185 | +			}  | 
 | 186 | +		}  | 
 | 187 | +		return false;  | 
 | 188 | +	}  | 
 | 189 | + | 
 | 190 | +	private function updateUserSecretHash($uid, $userSecret, $exists = false) {  | 
 | 191 | +		$qb = $this->db->getQueryBuilder();  | 
 | 192 | +		$hash = \OC::$server->getHasher()->hash($userSecret);  | 
 | 193 | +		if ($exists || count($this->getUserSecretHash($uid)) > 0) {  | 
 | 194 | +			$qb->update('user_saml_auth_token')  | 
 | 195 | +				->set('token', $qb->createNamedParameter($hash))  | 
 | 196 | +				->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))  | 
 | 197 | +				->andWhere($qb->expr()->eq('name', $qb->createNamedParameter('sso_secret_hash')));  | 
 | 198 | +		} else {  | 
 | 199 | +			$qb->insert('user_saml_auth_token')  | 
 | 200 | +				->values([  | 
 | 201 | +					'uid' => $qb->createNamedParameter($uid),  | 
 | 202 | +					'token' => $qb->createNamedParameter($hash),  | 
 | 203 | +					'name' => $qb->createNamedParameter('sso_secret_hash'),  | 
 | 204 | +				]);  | 
 | 205 | +		}  | 
 | 206 | +		return $qb->execute();  | 
 | 207 | +	}  | 
 | 208 | + | 
155 | 209 | 	/**  | 
156 | 210 | 	 * @param string $uid  | 
157 | 211 | 	 * @throws \OCP\Files\NotFoundException  | 
@@ -195,22 +249,15 @@ public function implementsActions($actions) {  | 
195 | 249 | 	 * @return string  | 
196 | 250 | 	 *  | 
197 | 251 | 	 * Check if the password is correct without logging in the user  | 
198 |  | -	 * returns the user id or false  | 
 | 252 | +	 * returns the user id or false.  | 
 | 253 | +	 *  | 
 | 254 | +	 * By default user_saml tokens are passwordless and this function  | 
 | 255 | +	 * is unused. It is only called if we have tokens with passwords,  | 
 | 256 | +	 * which happens if we have SSO provided user secrets.  | 
199 | 257 | 	 */  | 
200 | 258 | 	public function checkPassword($uid, $password) {  | 
201 |  | -		$qb = $this->db->getQueryBuilder();  | 
202 |  | -		$qb->select('token')  | 
203 |  | -			->from('user_saml_auth_token')  | 
204 |  | -			->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))  | 
205 |  | -			->setMaxResults(1000);  | 
206 |  | -		$result = $qb->execute();  | 
207 |  | -		$data = $result->fetchAll();  | 
208 |  | -		$result->closeCursor();  | 
209 |  | - | 
210 |  | -		foreach ($data as $passwords) {  | 
211 |  | -			if (password_verify($password, $passwords['token'])) {  | 
212 |  | -				return $uid;  | 
213 |  | -			}  | 
 | 259 | +		if ($this->checkUserSecretHash($uid, $password)) {  | 
 | 260 | +			return $uid;  | 
214 | 261 | 		}  | 
215 | 262 | 
 
  | 
216 | 263 | 		return false;  | 
@@ -508,6 +555,16 @@ public function getCurrentUserId() {  | 
508 | 555 | 		return '';  | 
509 | 556 | 	}  | 
510 | 557 | 
 
  | 
 | 558 | +	/**  | 
 | 559 | +	 * Optionally returns a stable per-user secret. This secret is for  | 
 | 560 | +	 * instance used to secure file encryption keys.  | 
 | 561 | +	 * @return string|null  | 
 | 562 | +	 * @since 26.0.0  | 
 | 563 | +	 */  | 
 | 564 | +	public function getCurrentUserSecret(): string {  | 
 | 565 | +		$samlData = $this->session->get('user_saml.samlUserData');  | 
 | 566 | +		return $this->getUserSecret($this->getCurrentUserId(), $samlData);  | 
 | 567 | +	}  | 
511 | 568 | 
 
  | 
512 | 569 | 	/**  | 
513 | 570 | 	 * Backend name to be shown in user management  | 
@@ -608,6 +665,21 @@ private function getAttributeArrayValue($name, array $attributes) {  | 
608 | 665 | 		return $value;  | 
609 | 666 | 	}  | 
610 | 667 | 
 
  | 
 | 668 | +	private function getUserSecret($uid, array $attributes) {  | 
 | 669 | +		try {  | 
 | 670 | +			$userSecret = $this->getAttributeValue('saml-attribute-mapping-user_secret_mapping', $attributes);  | 
 | 671 | +			if ($userSecret === '') {  | 
 | 672 | +				$this->logger->debug('Got no user_secret from idp', ['app' => 'user_saml']);  | 
 | 673 | +			} else {  | 
 | 674 | +				$this->logger->debug('Got user_secret from idp', ['app' => 'user_saml']);  | 
 | 675 | +				return $userSecret;  | 
 | 676 | +			}  | 
 | 677 | +		} catch (\InvalidArgumentException $e) {  | 
 | 678 | +			$this->logger->debug('No user_secret mapping configured', ['app' => 'user_saml']);  | 
 | 679 | +		}  | 
 | 680 | +		return null;  | 
 | 681 | +	}  | 
 | 682 | + | 
611 | 683 | 	public function updateAttributes($uid,  | 
612 | 684 | 		array $attributes) {  | 
613 | 685 | 		$user = $this->userManager->get($uid);  | 
@@ -679,11 +751,16 @@ public function updateAttributes($uid,  | 
679 | 751 | 					$groupManager->get($group)->removeUser($user);  | 
680 | 752 | 				}  | 
681 | 753 | 			}  | 
 | 754 | + | 
 | 755 | +			$userSecret = $this->getUserSecret($uid, $attributes);  | 
 | 756 | +			if ($userSecret !== null) {  | 
 | 757 | +				if (!$this->checkUserSecretHash($uid, $userSecret)) {  | 
 | 758 | +					$this->updateUserSecretHash($uid, $userSecret);  | 
 | 759 | +				}  | 
 | 760 | +			}  | 
682 | 761 | 		}  | 
683 | 762 | 	}  | 
684 | 763 | 
 
  | 
685 |  | - | 
686 |  | - | 
687 | 764 | 	public function countUsers() {  | 
688 | 765 | 		$query = $this->db->getQueryBuilder();  | 
689 | 766 | 		$query->select($query->func()->count('uid'))  | 
 | 
0 commit comments