Passed
Push — master ( d4a9da...674f4e )
by Julius
15:49 queued 12s
created
lib/private/Authentication/Token/PublicKeyTokenMapper.php 1 patch
Indentation   +199 added lines, -199 removed lines patch added patch discarded remove patch
@@ -36,224 +36,224 @@
 block discarded – undo
36 36
  * @template-extends QBMapper<PublicKeyToken>
37 37
  */
38 38
 class PublicKeyTokenMapper extends QBMapper {
39
-	public function __construct(IDBConnection $db) {
40
-		parent::__construct($db, 'authtoken');
41
-	}
39
+    public function __construct(IDBConnection $db) {
40
+        parent::__construct($db, 'authtoken');
41
+    }
42 42
 
43
-	/**
44
-	 * Invalidate (delete) a given token
45
-	 *
46
-	 * @param string $token
47
-	 */
48
-	public function invalidate(string $token) {
49
-		/* @var $qb IQueryBuilder */
50
-		$qb = $this->db->getQueryBuilder();
51
-		$qb->delete($this->tableName)
52
-			->where($qb->expr()->eq('token', $qb->createNamedParameter($token)))
53
-			->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
54
-			->execute();
55
-	}
43
+    /**
44
+     * Invalidate (delete) a given token
45
+     *
46
+     * @param string $token
47
+     */
48
+    public function invalidate(string $token) {
49
+        /* @var $qb IQueryBuilder */
50
+        $qb = $this->db->getQueryBuilder();
51
+        $qb->delete($this->tableName)
52
+            ->where($qb->expr()->eq('token', $qb->createNamedParameter($token)))
53
+            ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
54
+            ->execute();
55
+    }
56 56
 
57
-	/**
58
-	 * @param int $olderThan
59
-	 * @param int $remember
60
-	 */
61
-	public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) {
62
-		/* @var $qb IQueryBuilder */
63
-		$qb = $this->db->getQueryBuilder();
64
-		$qb->delete($this->tableName)
65
-			->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT)))
66
-			->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT)))
67
-			->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT)))
68
-			->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
69
-			->execute();
70
-	}
57
+    /**
58
+     * @param int $olderThan
59
+     * @param int $remember
60
+     */
61
+    public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) {
62
+        /* @var $qb IQueryBuilder */
63
+        $qb = $this->db->getQueryBuilder();
64
+        $qb->delete($this->tableName)
65
+            ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT)))
66
+            ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT)))
67
+            ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT)))
68
+            ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
69
+            ->execute();
70
+    }
71 71
 
72
-	/**
73
-	 * Get the user UID for the given token
74
-	 *
75
-	 * @throws DoesNotExistException
76
-	 */
77
-	public function getToken(string $token): PublicKeyToken {
78
-		/* @var $qb IQueryBuilder */
79
-		$qb = $this->db->getQueryBuilder();
80
-		$result = $qb->select('*')
81
-			->from($this->tableName)
82
-			->where($qb->expr()->eq('token', $qb->createNamedParameter($token)))
83
-			->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
84
-			->execute();
72
+    /**
73
+     * Get the user UID for the given token
74
+     *
75
+     * @throws DoesNotExistException
76
+     */
77
+    public function getToken(string $token): PublicKeyToken {
78
+        /* @var $qb IQueryBuilder */
79
+        $qb = $this->db->getQueryBuilder();
80
+        $result = $qb->select('*')
81
+            ->from($this->tableName)
82
+            ->where($qb->expr()->eq('token', $qb->createNamedParameter($token)))
83
+            ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
84
+            ->execute();
85 85
 
86
-		$data = $result->fetch();
87
-		$result->closeCursor();
88
-		if ($data === false) {
89
-			throw new DoesNotExistException('token does not exist');
90
-		}
91
-		return PublicKeyToken::fromRow($data);
92
-	}
86
+        $data = $result->fetch();
87
+        $result->closeCursor();
88
+        if ($data === false) {
89
+            throw new DoesNotExistException('token does not exist');
90
+        }
91
+        return PublicKeyToken::fromRow($data);
92
+    }
93 93
 
94
-	/**
95
-	 * Get the token for $id
96
-	 *
97
-	 * @throws DoesNotExistException
98
-	 */
99
-	public function getTokenById(int $id): PublicKeyToken {
100
-		/* @var $qb IQueryBuilder */
101
-		$qb = $this->db->getQueryBuilder();
102
-		$result = $qb->select('*')
103
-			->from($this->tableName)
104
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
105
-			->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
106
-			->execute();
94
+    /**
95
+     * Get the token for $id
96
+     *
97
+     * @throws DoesNotExistException
98
+     */
99
+    public function getTokenById(int $id): PublicKeyToken {
100
+        /* @var $qb IQueryBuilder */
101
+        $qb = $this->db->getQueryBuilder();
102
+        $result = $qb->select('*')
103
+            ->from($this->tableName)
104
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
105
+            ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
106
+            ->execute();
107 107
 
108
-		$data = $result->fetch();
109
-		$result->closeCursor();
110
-		if ($data === false) {
111
-			throw new DoesNotExistException('token does not exist');
112
-		}
113
-		return PublicKeyToken::fromRow($data);
114
-	}
108
+        $data = $result->fetch();
109
+        $result->closeCursor();
110
+        if ($data === false) {
111
+            throw new DoesNotExistException('token does not exist');
112
+        }
113
+        return PublicKeyToken::fromRow($data);
114
+    }
115 115
 
116
-	/**
117
-	 * Get all tokens of a user
118
-	 *
119
-	 * The provider may limit the number of result rows in case of an abuse
120
-	 * where a high number of (session) tokens is generated
121
-	 *
122
-	 * @param string $uid
123
-	 * @return PublicKeyToken[]
124
-	 */
125
-	public function getTokenByUser(string $uid): array {
126
-		/* @var $qb IQueryBuilder */
127
-		$qb = $this->db->getQueryBuilder();
128
-		$qb->select('*')
129
-			->from($this->tableName)
130
-			->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))
131
-			->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
132
-			->setMaxResults(1000);
133
-		$result = $qb->execute();
134
-		$data = $result->fetchAll();
135
-		$result->closeCursor();
116
+    /**
117
+     * Get all tokens of a user
118
+     *
119
+     * The provider may limit the number of result rows in case of an abuse
120
+     * where a high number of (session) tokens is generated
121
+     *
122
+     * @param string $uid
123
+     * @return PublicKeyToken[]
124
+     */
125
+    public function getTokenByUser(string $uid): array {
126
+        /* @var $qb IQueryBuilder */
127
+        $qb = $this->db->getQueryBuilder();
128
+        $qb->select('*')
129
+            ->from($this->tableName)
130
+            ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))
131
+            ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)))
132
+            ->setMaxResults(1000);
133
+        $result = $qb->execute();
134
+        $data = $result->fetchAll();
135
+        $result->closeCursor();
136 136
 
137
-		$entities = array_map(function ($row) {
138
-			return PublicKeyToken::fromRow($row);
139
-		}, $data);
137
+        $entities = array_map(function ($row) {
138
+            return PublicKeyToken::fromRow($row);
139
+        }, $data);
140 140
 
141
-		return $entities;
142
-	}
141
+        return $entities;
142
+    }
143 143
 
144
-	public function deleteById(string $uid, int $id) {
145
-		/* @var $qb IQueryBuilder */
146
-		$qb = $this->db->getQueryBuilder();
147
-		$qb->delete($this->tableName)
148
-			->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
149
-			->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))
150
-			->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)));
151
-		$qb->execute();
152
-	}
144
+    public function deleteById(string $uid, int $id) {
145
+        /* @var $qb IQueryBuilder */
146
+        $qb = $this->db->getQueryBuilder();
147
+        $qb->delete($this->tableName)
148
+            ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
149
+            ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))
150
+            ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)));
151
+        $qb->execute();
152
+    }
153 153
 
154
-	/**
155
-	 * delete all auth token which belong to a specific client if the client was deleted
156
-	 *
157
-	 * @param string $name
158
-	 */
159
-	public function deleteByName(string $name) {
160
-		$qb = $this->db->getQueryBuilder();
161
-		$qb->delete($this->tableName)
162
-			->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR))
163
-			->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)));
164
-		$qb->execute();
165
-	}
154
+    /**
155
+     * delete all auth token which belong to a specific client if the client was deleted
156
+     *
157
+     * @param string $name
158
+     */
159
+    public function deleteByName(string $name) {
160
+        $qb = $this->db->getQueryBuilder();
161
+        $qb->delete($this->tableName)
162
+            ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR))
163
+            ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)));
164
+        $qb->execute();
165
+    }
166 166
 
167
-	public function deleteTempToken(PublicKeyToken $except) {
168
-		$qb = $this->db->getQueryBuilder();
167
+    public function deleteTempToken(PublicKeyToken $except) {
168
+        $qb = $this->db->getQueryBuilder();
169 169
 
170
-		$qb->delete($this->tableName)
171
-			->where($qb->expr()->eq('uid', $qb->createNamedParameter($except->getUID())))
172
-			->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN)))
173
-			->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($except->getId())))
174
-			->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)));
170
+        $qb->delete($this->tableName)
171
+            ->where($qb->expr()->eq('uid', $qb->createNamedParameter($except->getUID())))
172
+            ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN)))
173
+            ->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($except->getId())))
174
+            ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT)));
175 175
 
176
-		$qb->execute();
177
-	}
176
+        $qb->execute();
177
+    }
178 178
 
179
-	public function hasExpiredTokens(string $uid): bool {
180
-		$qb = $this->db->getQueryBuilder();
181
-		$qb->select('*')
182
-			->from($this->tableName)
183
-			->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))
184
-			->andWhere($qb->expr()->eq('password_invalid', $qb->createNamedParameter(true), IQueryBuilder::PARAM_BOOL))
185
-			->setMaxResults(1);
179
+    public function hasExpiredTokens(string $uid): bool {
180
+        $qb = $this->db->getQueryBuilder();
181
+        $qb->select('*')
182
+            ->from($this->tableName)
183
+            ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)))
184
+            ->andWhere($qb->expr()->eq('password_invalid', $qb->createNamedParameter(true), IQueryBuilder::PARAM_BOOL))
185
+            ->setMaxResults(1);
186 186
 
187
-		$cursor = $qb->execute();
188
-		$data = $cursor->fetchAll();
189
-		$cursor->closeCursor();
187
+        $cursor = $qb->execute();
188
+        $data = $cursor->fetchAll();
189
+        $cursor->closeCursor();
190 190
 
191
-		return count($data) === 1;
192
-	}
191
+        return count($data) === 1;
192
+    }
193 193
 
194
-	/**
195
-	 * Update the last activity timestamp
196
-	 *
197
-	 * In highly concurrent setups it can happen that two parallel processes
198
-	 * trigger the update at (nearly) the same time. In that special case it's
199
-	 * not necessary to hit the database with two actual updates. Therefore the
200
-	 * target last activity is included in the WHERE clause with a few seconds
201
-	 * of tolerance.
202
-	 *
203
-	 * Example:
204
-	 * - process 1 (P1) reads the token at timestamp 1500
205
-	 * - process 1 (P2) reads the token at timestamp 1501
206
-	 * - activity update interval is 100
207
-	 *
208
-	 * This means
209
-	 *
210
-	 * - P1 will see a last_activity smaller than the current time and update
211
-	 *   the token row
212
-	 * - If P2 reads after P1 had written, it will see 1600 as last activity
213
-	 *   and the comparison on last_activity won't be truthy. This means no rows
214
-	 *   need to be updated a second time
215
-	 * - If P2 reads before P1 had written, it will see 1501 as last activity,
216
-	 *   but the comparison on last_activity will still not be truthy and the
217
-	 *   token row is not updated a second time
218
-	 *
219
-	 * @param IToken $token
220
-	 * @param int $now
221
-	 */
222
-	public function updateActivity(IToken $token, int $now): void {
223
-		$qb = $this->db->getQueryBuilder();
224
-		$update = $qb->update($this->getTableName())
225
-			->set('last_activity', $qb->createNamedParameter($now, IQueryBuilder::PARAM_INT))
226
-			->where(
227
-				$qb->expr()->eq('id', $qb->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
228
-				$qb->expr()->lt('last_activity', $qb->createNamedParameter($now - 15, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)
229
-			);
230
-		$update->executeStatement();
231
-	}
194
+    /**
195
+     * Update the last activity timestamp
196
+     *
197
+     * In highly concurrent setups it can happen that two parallel processes
198
+     * trigger the update at (nearly) the same time. In that special case it's
199
+     * not necessary to hit the database with two actual updates. Therefore the
200
+     * target last activity is included in the WHERE clause with a few seconds
201
+     * of tolerance.
202
+     *
203
+     * Example:
204
+     * - process 1 (P1) reads the token at timestamp 1500
205
+     * - process 1 (P2) reads the token at timestamp 1501
206
+     * - activity update interval is 100
207
+     *
208
+     * This means
209
+     *
210
+     * - P1 will see a last_activity smaller than the current time and update
211
+     *   the token row
212
+     * - If P2 reads after P1 had written, it will see 1600 as last activity
213
+     *   and the comparison on last_activity won't be truthy. This means no rows
214
+     *   need to be updated a second time
215
+     * - If P2 reads before P1 had written, it will see 1501 as last activity,
216
+     *   but the comparison on last_activity will still not be truthy and the
217
+     *   token row is not updated a second time
218
+     *
219
+     * @param IToken $token
220
+     * @param int $now
221
+     */
222
+    public function updateActivity(IToken $token, int $now): void {
223
+        $qb = $this->db->getQueryBuilder();
224
+        $update = $qb->update($this->getTableName())
225
+            ->set('last_activity', $qb->createNamedParameter($now, IQueryBuilder::PARAM_INT))
226
+            ->where(
227
+                $qb->expr()->eq('id', $qb->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
228
+                $qb->expr()->lt('last_activity', $qb->createNamedParameter($now - 15, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)
229
+            );
230
+        $update->executeStatement();
231
+    }
232 232
 
233
-	public function updateHashesForUser(string $userId, string $passwordHash): void {
234
-		$qb = $this->db->getQueryBuilder();
235
-		$update = $qb->update($this->getTableName())
236
-			->set('password_hash', $qb->createNamedParameter($passwordHash))
237
-			->where(
238
-				$qb->expr()->eq('uid', $qb->createNamedParameter($userId))
239
-			);
240
-		$update->executeStatement();
241
-	}
233
+    public function updateHashesForUser(string $userId, string $passwordHash): void {
234
+        $qb = $this->db->getQueryBuilder();
235
+        $update = $qb->update($this->getTableName())
236
+            ->set('password_hash', $qb->createNamedParameter($passwordHash))
237
+            ->where(
238
+                $qb->expr()->eq('uid', $qb->createNamedParameter($userId))
239
+            );
240
+        $update->executeStatement();
241
+    }
242 242
 
243
-	public function getFirstTokenForUser(string $userId): ?PublicKeyToken {
244
-		$qb = $this->db->getQueryBuilder();
245
-		$qb->select('*')
246
-			->from($this->getTableName())
247
-			->where($qb->expr()->eq('uid', $qb->createNamedParameter($userId)))
248
-			->setMaxResults(1)
249
-			->orderBy('id');
250
-		$result = $qb->executeQuery();
243
+    public function getFirstTokenForUser(string $userId): ?PublicKeyToken {
244
+        $qb = $this->db->getQueryBuilder();
245
+        $qb->select('*')
246
+            ->from($this->getTableName())
247
+            ->where($qb->expr()->eq('uid', $qb->createNamedParameter($userId)))
248
+            ->setMaxResults(1)
249
+            ->orderBy('id');
250
+        $result = $qb->executeQuery();
251 251
 
252
-		$data = $result->fetch();
253
-		$result->closeCursor();
254
-		if ($data === false) {
255
-			return null;
256
-		}
257
-		return PublicKeyToken::fromRow($data);
258
-	}
252
+        $data = $result->fetch();
253
+        $result->closeCursor();
254
+        if ($data === false) {
255
+            return null;
256
+        }
257
+        return PublicKeyToken::fromRow($data);
258
+    }
259 259
 }
Please login to merge, or discard this patch.
lib/private/Authentication/Token/PublicKeyTokenProvider.php 2 patches
Indentation   +466 added lines, -466 removed lines patch added patch discarded remove patch
@@ -46,470 +46,470 @@
 block discarded – undo
46 46
 use Psr\Log\LoggerInterface;
47 47
 
48 48
 class PublicKeyTokenProvider implements IProvider {
49
-	use TTransactional;
50
-
51
-	/** @var PublicKeyTokenMapper */
52
-	private $mapper;
53
-
54
-	/** @var ICrypto */
55
-	private $crypto;
56
-
57
-	/** @var IConfig */
58
-	private $config;
59
-
60
-	private IDBConnection $db;
61
-
62
-	/** @var LoggerInterface */
63
-	private $logger;
64
-
65
-	/** @var ITimeFactory */
66
-	private $time;
67
-
68
-	/** @var CappedMemoryCache */
69
-	private $cache;
70
-
71
-	private IHasher $hasher;
72
-
73
-	public function __construct(PublicKeyTokenMapper $mapper,
74
-								ICrypto $crypto,
75
-								IConfig $config,
76
-								IDBConnection $db,
77
-								LoggerInterface $logger,
78
-								ITimeFactory $time,
79
-								IHasher $hasher) {
80
-		$this->mapper = $mapper;
81
-		$this->crypto = $crypto;
82
-		$this->config = $config;
83
-		$this->db = $db;
84
-		$this->logger = $logger;
85
-		$this->time = $time;
86
-
87
-		$this->cache = new CappedMemoryCache();
88
-		$this->hasher = $hasher;
89
-	}
90
-
91
-	/**
92
-	 * {@inheritDoc}
93
-	 */
94
-	public function generateToken(string $token,
95
-								  string $uid,
96
-								  string $loginName,
97
-								  ?string $password,
98
-								  string $name,
99
-								  int $type = IToken::TEMPORARY_TOKEN,
100
-								  int $remember = IToken::DO_NOT_REMEMBER): IToken {
101
-		if (mb_strlen($name) > 128) {
102
-			$name = mb_substr($name, 0, 120) . '…';
103
-		}
104
-
105
-		// We need to check against one old token to see if there is a password
106
-		// hash that we can reuse for detecting outdated passwords
107
-		$randomOldToken = $this->mapper->getFirstTokenForUser($uid);
108
-		$oldTokenMatches = $randomOldToken && $this->hasher->verify(sha1($password) . $password, $randomOldToken->getPasswordHash());
109
-
110
-		$dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember);
111
-
112
-		if ($oldTokenMatches) {
113
-			$dbToken->setPasswordHash($randomOldToken->getPasswordHash());
114
-		}
115
-
116
-		$this->mapper->insert($dbToken);
117
-
118
-		if (!$oldTokenMatches && $password !== null) {
119
-			$this->updatePasswords($uid, $password);
120
-		}
121
-
122
-		// Add the token to the cache
123
-		$this->cache[$dbToken->getToken()] = $dbToken;
124
-
125
-		return $dbToken;
126
-	}
127
-
128
-	public function getToken(string $tokenId): IToken {
129
-		$tokenHash = $this->hashToken($tokenId);
130
-
131
-		if (isset($this->cache[$tokenHash])) {
132
-			if ($this->cache[$tokenHash] instanceof DoesNotExistException) {
133
-				$ex = $this->cache[$tokenHash];
134
-				throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
135
-			}
136
-			$token = $this->cache[$tokenHash];
137
-		} else {
138
-			try {
139
-				$token = $this->mapper->getToken($this->hashToken($tokenId));
140
-				$this->cache[$token->getToken()] = $token;
141
-			} catch (DoesNotExistException $ex) {
142
-				try {
143
-					$token = $this->mapper->getToken($this->hashTokenWithEmptySecret($tokenId));
144
-					$this->cache[$token->getToken()] = $token;
145
-					$this->rotate($token, $tokenId, $tokenId);
146
-				} catch (DoesNotExistException $ex2) {
147
-					$this->cache[$tokenHash] = $ex2;
148
-					throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
149
-				}
150
-			}
151
-		}
152
-
153
-		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
154
-			throw new ExpiredTokenException($token);
155
-		}
156
-
157
-		if ($token->getType() === IToken::WIPE_TOKEN) {
158
-			throw new WipeTokenException($token);
159
-		}
160
-
161
-		if ($token->getPasswordInvalid() === true) {
162
-			//The password is invalid we should throw an TokenPasswordExpiredException
163
-			throw new TokenPasswordExpiredException($token);
164
-		}
165
-
166
-		return $token;
167
-	}
168
-
169
-	public function getTokenById(int $tokenId): IToken {
170
-		try {
171
-			$token = $this->mapper->getTokenById($tokenId);
172
-		} catch (DoesNotExistException $ex) {
173
-			throw new InvalidTokenException("Token with ID $tokenId does not exist: " . $ex->getMessage(), 0, $ex);
174
-		}
175
-
176
-		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
177
-			throw new ExpiredTokenException($token);
178
-		}
179
-
180
-		if ($token->getType() === IToken::WIPE_TOKEN) {
181
-			throw new WipeTokenException($token);
182
-		}
183
-
184
-		if ($token->getPasswordInvalid() === true) {
185
-			//The password is invalid we should throw an TokenPasswordExpiredException
186
-			throw new TokenPasswordExpiredException($token);
187
-		}
188
-
189
-		return $token;
190
-	}
191
-
192
-	public function renewSessionToken(string $oldSessionId, string $sessionId): IToken {
193
-		$this->cache->clear();
194
-
195
-		return $this->atomic(function () use ($oldSessionId, $sessionId) {
196
-			$token = $this->getToken($oldSessionId);
197
-
198
-			if (!($token instanceof PublicKeyToken)) {
199
-				throw new InvalidTokenException("Invalid token type");
200
-			}
201
-
202
-			$password = null;
203
-			if (!is_null($token->getPassword())) {
204
-				$privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId);
205
-				$password = $this->decryptPassword($token->getPassword(), $privateKey);
206
-			}
207
-			$newToken = $this->generateToken(
208
-				$sessionId,
209
-				$token->getUID(),
210
-				$token->getLoginName(),
211
-				$password,
212
-				$token->getName(),
213
-				IToken::TEMPORARY_TOKEN,
214
-				$token->getRemember()
215
-			);
216
-
217
-			$this->mapper->delete($token);
218
-
219
-			return $newToken;
220
-		}, $this->db);
221
-	}
222
-
223
-	public function invalidateToken(string $token) {
224
-		$this->cache->clear();
225
-
226
-		$this->mapper->invalidate($this->hashToken($token));
227
-		$this->mapper->invalidate($this->hashTokenWithEmptySecret($token));
228
-	}
229
-
230
-	public function invalidateTokenById(string $uid, int $id) {
231
-		$this->cache->clear();
232
-
233
-		$this->mapper->deleteById($uid, $id);
234
-	}
235
-
236
-	public function invalidateOldTokens() {
237
-		$this->cache->clear();
238
-
239
-		$olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
240
-		$this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
241
-		$this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
242
-		$rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
243
-		$this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
244
-		$this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
245
-	}
246
-
247
-	public function updateToken(IToken $token) {
248
-		$this->cache->clear();
249
-
250
-		if (!($token instanceof PublicKeyToken)) {
251
-			throw new InvalidTokenException("Invalid token type");
252
-		}
253
-		$this->mapper->update($token);
254
-	}
255
-
256
-	public function updateTokenActivity(IToken $token) {
257
-		$this->cache->clear();
258
-
259
-		if (!($token instanceof PublicKeyToken)) {
260
-			throw new InvalidTokenException("Invalid token type");
261
-		}
262
-
263
-		$activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60);
264
-		$activityInterval = min(max($activityInterval, 0), 300);
265
-
266
-		/** @var PublicKeyToken $token */
267
-		$now = $this->time->getTime();
268
-		if ($token->getLastActivity() < ($now - $activityInterval)) {
269
-			$token->setLastActivity($now);
270
-			$this->mapper->updateActivity($token, $now);
271
-		}
272
-	}
273
-
274
-	public function getTokenByUser(string $uid): array {
275
-		return $this->mapper->getTokenByUser($uid);
276
-	}
277
-
278
-	public function getPassword(IToken $savedToken, string $tokenId): string {
279
-		if (!($savedToken instanceof PublicKeyToken)) {
280
-			throw new InvalidTokenException("Invalid token type");
281
-		}
282
-
283
-		if ($savedToken->getPassword() === null) {
284
-			throw new PasswordlessTokenException();
285
-		}
286
-
287
-		// Decrypt private key with tokenId
288
-		$privateKey = $this->decrypt($savedToken->getPrivateKey(), $tokenId);
289
-
290
-		// Decrypt password with private key
291
-		return $this->decryptPassword($savedToken->getPassword(), $privateKey);
292
-	}
293
-
294
-	public function setPassword(IToken $token, string $tokenId, string $password) {
295
-		$this->cache->clear();
296
-
297
-		if (!($token instanceof PublicKeyToken)) {
298
-			throw new InvalidTokenException("Invalid token type");
299
-		}
300
-
301
-		// When changing passwords all temp tokens are deleted
302
-		$this->mapper->deleteTempToken($token);
303
-
304
-		// Update the password for all tokens
305
-		$tokens = $this->mapper->getTokenByUser($token->getUID());
306
-		$hashedPassword = $this->hashPassword($password);
307
-		foreach ($tokens as $t) {
308
-			$publicKey = $t->getPublicKey();
309
-			$t->setPassword($this->encryptPassword($password, $publicKey));
310
-			$t->setPasswordHash($hashedPassword);
311
-			$this->updateToken($t);
312
-		}
313
-	}
314
-
315
-	private function hashPassword(string $password): string {
316
-		return $this->hasher->hash(sha1($password) . $password);
317
-	}
318
-
319
-	public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
320
-		$this->cache->clear();
321
-
322
-		if (!($token instanceof PublicKeyToken)) {
323
-			throw new InvalidTokenException("Invalid token type");
324
-		}
325
-
326
-		// Decrypt private key with oldTokenId
327
-		$privateKey = $this->decrypt($token->getPrivateKey(), $oldTokenId);
328
-		// Encrypt with the new token
329
-		$token->setPrivateKey($this->encrypt($privateKey, $newTokenId));
330
-
331
-		$token->setToken($this->hashToken($newTokenId));
332
-		$this->updateToken($token);
333
-
334
-		return $token;
335
-	}
336
-
337
-	private function encrypt(string $plaintext, string $token): string {
338
-		$secret = $this->config->getSystemValue('secret');
339
-		return $this->crypto->encrypt($plaintext, $token . $secret);
340
-	}
341
-
342
-	/**
343
-	 * @throws InvalidTokenException
344
-	 */
345
-	private function decrypt(string $cipherText, string $token): string {
346
-		$secret = $this->config->getSystemValue('secret');
347
-		try {
348
-			return $this->crypto->decrypt($cipherText, $token . $secret);
349
-		} catch (\Exception $ex) {
350
-			// Retry with empty secret as a fallback for instances where the secret might not have been set by accident
351
-			try {
352
-				return $this->crypto->decrypt($cipherText, $token);
353
-			} catch (\Exception $ex2) {
354
-				// Delete the invalid token
355
-				$this->invalidateToken($token);
356
-				throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2);
357
-			}
358
-		}
359
-	}
360
-
361
-	private function encryptPassword(string $password, string $publicKey): string {
362
-		openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING);
363
-		$encryptedPassword = base64_encode($encryptedPassword);
364
-
365
-		return $encryptedPassword;
366
-	}
367
-
368
-	private function decryptPassword(string $encryptedPassword, string $privateKey): string {
369
-		$encryptedPassword = base64_decode($encryptedPassword);
370
-		openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
371
-
372
-		return $password;
373
-	}
374
-
375
-	private function hashToken(string $token): string {
376
-		$secret = $this->config->getSystemValue('secret');
377
-		return hash('sha512', $token . $secret);
378
-	}
379
-
380
-	/**
381
-	 * @deprecated Fallback for instances where the secret might not have been set by accident
382
-	 */
383
-	private function hashTokenWithEmptySecret(string $token): string {
384
-		return hash('sha512', $token);
385
-	}
386
-
387
-	/**
388
-	 * @throws \RuntimeException when OpenSSL reports a problem
389
-	 */
390
-	private function newToken(string $token,
391
-							  string $uid,
392
-							  string $loginName,
393
-							  $password,
394
-							  string $name,
395
-							  int $type,
396
-							  int $remember): PublicKeyToken {
397
-		$dbToken = new PublicKeyToken();
398
-		$dbToken->setUid($uid);
399
-		$dbToken->setLoginName($loginName);
400
-
401
-		$config = array_merge([
402
-			'digest_alg' => 'sha512',
403
-			'private_key_bits' => $password !== null && strlen($password) > 250 ? 4096 : 2048,
404
-		], $this->config->getSystemValue('openssl', []));
405
-
406
-		// Generate new key
407
-		$res = openssl_pkey_new($config);
408
-		if ($res === false) {
409
-			$this->logOpensslError();
410
-			throw new \RuntimeException('OpenSSL reported a problem');
411
-		}
412
-
413
-		if (openssl_pkey_export($res, $privateKey, null, $config) === false) {
414
-			$this->logOpensslError();
415
-			throw new \RuntimeException('OpenSSL reported a problem');
416
-		}
417
-
418
-		// Extract the public key from $res to $pubKey
419
-		$publicKey = openssl_pkey_get_details($res);
420
-		$publicKey = $publicKey['key'];
421
-
422
-		$dbToken->setPublicKey($publicKey);
423
-		$dbToken->setPrivateKey($this->encrypt($privateKey, $token));
424
-
425
-		if (!is_null($password) && $this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
426
-			if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
427
-				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');
428
-			}
429
-			$dbToken->setPassword($this->encryptPassword($password, $publicKey));
430
-			$dbToken->setPasswordHash($this->hashPassword($password));
431
-		}
432
-
433
-		$dbToken->setName($name);
434
-		$dbToken->setToken($this->hashToken($token));
435
-		$dbToken->setType($type);
436
-		$dbToken->setRemember($remember);
437
-		$dbToken->setLastActivity($this->time->getTime());
438
-		$dbToken->setLastCheck($this->time->getTime());
439
-		$dbToken->setVersion(PublicKeyToken::VERSION);
440
-
441
-		return $dbToken;
442
-	}
443
-
444
-	public function markPasswordInvalid(IToken $token, string $tokenId) {
445
-		$this->cache->clear();
446
-
447
-		if (!($token instanceof PublicKeyToken)) {
448
-			throw new InvalidTokenException("Invalid token type");
449
-		}
450
-
451
-		$token->setPasswordInvalid(true);
452
-		$this->mapper->update($token);
453
-	}
454
-
455
-	public function updatePasswords(string $uid, string $password) {
456
-		$this->cache->clear();
457
-
458
-		// prevent setting an empty pw as result of pw-less-login
459
-		if ($password === '' || !$this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
460
-			return;
461
-		}
462
-
463
-		// Update the password for all tokens
464
-		$tokens = $this->mapper->getTokenByUser($uid);
465
-		$newPasswordHash = null;
466
-
467
-		/**
468
-		 * - true: The password hash could not be verified anymore
469
-		 *     and the token needs to be updated with the newly encrypted password
470
-		 * - false: The hash could still be verified
471
-		 * - missing: The hash needs to be verified
472
-		 */
473
-		$hashNeedsUpdate = [];
474
-
475
-		foreach ($tokens as $t) {
476
-			if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) {
477
-				if ($t->getPasswordHash() === null) {
478
-					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
479
-				} elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) {
480
-					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
481
-				} else {
482
-					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false;
483
-				}
484
-			}
485
-			$needsUpdating = $hashNeedsUpdate[$t->getPasswordHash() ?: ''] ?? true;
486
-
487
-			if ($needsUpdating) {
488
-				if ($newPasswordHash === null) {
489
-					$newPasswordHash = $this->hashPassword($password);
490
-				}
491
-
492
-				$publicKey = $t->getPublicKey();
493
-				$t->setPassword($this->encryptPassword($password, $publicKey));
494
-				$t->setPasswordHash($newPasswordHash);
495
-				$t->setPasswordInvalid(false);
496
-				$this->updateToken($t);
497
-			}
498
-		}
499
-
500
-		// If password hashes are different we update them all to be equal so
501
-		// that the next execution only needs to verify once
502
-		if (count($hashNeedsUpdate) > 1) {
503
-			$newPasswordHash = $this->hashPassword($password);
504
-			$this->mapper->updateHashesForUser($uid, $newPasswordHash);
505
-		}
506
-	}
507
-
508
-	private function logOpensslError() {
509
-		$errors = [];
510
-		while ($error = openssl_error_string()) {
511
-			$errors[] = $error;
512
-		}
513
-		$this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
514
-	}
49
+    use TTransactional;
50
+
51
+    /** @var PublicKeyTokenMapper */
52
+    private $mapper;
53
+
54
+    /** @var ICrypto */
55
+    private $crypto;
56
+
57
+    /** @var IConfig */
58
+    private $config;
59
+
60
+    private IDBConnection $db;
61
+
62
+    /** @var LoggerInterface */
63
+    private $logger;
64
+
65
+    /** @var ITimeFactory */
66
+    private $time;
67
+
68
+    /** @var CappedMemoryCache */
69
+    private $cache;
70
+
71
+    private IHasher $hasher;
72
+
73
+    public function __construct(PublicKeyTokenMapper $mapper,
74
+                                ICrypto $crypto,
75
+                                IConfig $config,
76
+                                IDBConnection $db,
77
+                                LoggerInterface $logger,
78
+                                ITimeFactory $time,
79
+                                IHasher $hasher) {
80
+        $this->mapper = $mapper;
81
+        $this->crypto = $crypto;
82
+        $this->config = $config;
83
+        $this->db = $db;
84
+        $this->logger = $logger;
85
+        $this->time = $time;
86
+
87
+        $this->cache = new CappedMemoryCache();
88
+        $this->hasher = $hasher;
89
+    }
90
+
91
+    /**
92
+     * {@inheritDoc}
93
+     */
94
+    public function generateToken(string $token,
95
+                                    string $uid,
96
+                                    string $loginName,
97
+                                  ?string $password,
98
+                                    string $name,
99
+                                    int $type = IToken::TEMPORARY_TOKEN,
100
+                                    int $remember = IToken::DO_NOT_REMEMBER): IToken {
101
+        if (mb_strlen($name) > 128) {
102
+            $name = mb_substr($name, 0, 120) . '…';
103
+        }
104
+
105
+        // We need to check against one old token to see if there is a password
106
+        // hash that we can reuse for detecting outdated passwords
107
+        $randomOldToken = $this->mapper->getFirstTokenForUser($uid);
108
+        $oldTokenMatches = $randomOldToken && $this->hasher->verify(sha1($password) . $password, $randomOldToken->getPasswordHash());
109
+
110
+        $dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember);
111
+
112
+        if ($oldTokenMatches) {
113
+            $dbToken->setPasswordHash($randomOldToken->getPasswordHash());
114
+        }
115
+
116
+        $this->mapper->insert($dbToken);
117
+
118
+        if (!$oldTokenMatches && $password !== null) {
119
+            $this->updatePasswords($uid, $password);
120
+        }
121
+
122
+        // Add the token to the cache
123
+        $this->cache[$dbToken->getToken()] = $dbToken;
124
+
125
+        return $dbToken;
126
+    }
127
+
128
+    public function getToken(string $tokenId): IToken {
129
+        $tokenHash = $this->hashToken($tokenId);
130
+
131
+        if (isset($this->cache[$tokenHash])) {
132
+            if ($this->cache[$tokenHash] instanceof DoesNotExistException) {
133
+                $ex = $this->cache[$tokenHash];
134
+                throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
135
+            }
136
+            $token = $this->cache[$tokenHash];
137
+        } else {
138
+            try {
139
+                $token = $this->mapper->getToken($this->hashToken($tokenId));
140
+                $this->cache[$token->getToken()] = $token;
141
+            } catch (DoesNotExistException $ex) {
142
+                try {
143
+                    $token = $this->mapper->getToken($this->hashTokenWithEmptySecret($tokenId));
144
+                    $this->cache[$token->getToken()] = $token;
145
+                    $this->rotate($token, $tokenId, $tokenId);
146
+                } catch (DoesNotExistException $ex2) {
147
+                    $this->cache[$tokenHash] = $ex2;
148
+                    throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
149
+                }
150
+            }
151
+        }
152
+
153
+        if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
154
+            throw new ExpiredTokenException($token);
155
+        }
156
+
157
+        if ($token->getType() === IToken::WIPE_TOKEN) {
158
+            throw new WipeTokenException($token);
159
+        }
160
+
161
+        if ($token->getPasswordInvalid() === true) {
162
+            //The password is invalid we should throw an TokenPasswordExpiredException
163
+            throw new TokenPasswordExpiredException($token);
164
+        }
165
+
166
+        return $token;
167
+    }
168
+
169
+    public function getTokenById(int $tokenId): IToken {
170
+        try {
171
+            $token = $this->mapper->getTokenById($tokenId);
172
+        } catch (DoesNotExistException $ex) {
173
+            throw new InvalidTokenException("Token with ID $tokenId does not exist: " . $ex->getMessage(), 0, $ex);
174
+        }
175
+
176
+        if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
177
+            throw new ExpiredTokenException($token);
178
+        }
179
+
180
+        if ($token->getType() === IToken::WIPE_TOKEN) {
181
+            throw new WipeTokenException($token);
182
+        }
183
+
184
+        if ($token->getPasswordInvalid() === true) {
185
+            //The password is invalid we should throw an TokenPasswordExpiredException
186
+            throw new TokenPasswordExpiredException($token);
187
+        }
188
+
189
+        return $token;
190
+    }
191
+
192
+    public function renewSessionToken(string $oldSessionId, string $sessionId): IToken {
193
+        $this->cache->clear();
194
+
195
+        return $this->atomic(function () use ($oldSessionId, $sessionId) {
196
+            $token = $this->getToken($oldSessionId);
197
+
198
+            if (!($token instanceof PublicKeyToken)) {
199
+                throw new InvalidTokenException("Invalid token type");
200
+            }
201
+
202
+            $password = null;
203
+            if (!is_null($token->getPassword())) {
204
+                $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId);
205
+                $password = $this->decryptPassword($token->getPassword(), $privateKey);
206
+            }
207
+            $newToken = $this->generateToken(
208
+                $sessionId,
209
+                $token->getUID(),
210
+                $token->getLoginName(),
211
+                $password,
212
+                $token->getName(),
213
+                IToken::TEMPORARY_TOKEN,
214
+                $token->getRemember()
215
+            );
216
+
217
+            $this->mapper->delete($token);
218
+
219
+            return $newToken;
220
+        }, $this->db);
221
+    }
222
+
223
+    public function invalidateToken(string $token) {
224
+        $this->cache->clear();
225
+
226
+        $this->mapper->invalidate($this->hashToken($token));
227
+        $this->mapper->invalidate($this->hashTokenWithEmptySecret($token));
228
+    }
229
+
230
+    public function invalidateTokenById(string $uid, int $id) {
231
+        $this->cache->clear();
232
+
233
+        $this->mapper->deleteById($uid, $id);
234
+    }
235
+
236
+    public function invalidateOldTokens() {
237
+        $this->cache->clear();
238
+
239
+        $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
240
+        $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
241
+        $this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
242
+        $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
243
+        $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
244
+        $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
245
+    }
246
+
247
+    public function updateToken(IToken $token) {
248
+        $this->cache->clear();
249
+
250
+        if (!($token instanceof PublicKeyToken)) {
251
+            throw new InvalidTokenException("Invalid token type");
252
+        }
253
+        $this->mapper->update($token);
254
+    }
255
+
256
+    public function updateTokenActivity(IToken $token) {
257
+        $this->cache->clear();
258
+
259
+        if (!($token instanceof PublicKeyToken)) {
260
+            throw new InvalidTokenException("Invalid token type");
261
+        }
262
+
263
+        $activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60);
264
+        $activityInterval = min(max($activityInterval, 0), 300);
265
+
266
+        /** @var PublicKeyToken $token */
267
+        $now = $this->time->getTime();
268
+        if ($token->getLastActivity() < ($now - $activityInterval)) {
269
+            $token->setLastActivity($now);
270
+            $this->mapper->updateActivity($token, $now);
271
+        }
272
+    }
273
+
274
+    public function getTokenByUser(string $uid): array {
275
+        return $this->mapper->getTokenByUser($uid);
276
+    }
277
+
278
+    public function getPassword(IToken $savedToken, string $tokenId): string {
279
+        if (!($savedToken instanceof PublicKeyToken)) {
280
+            throw new InvalidTokenException("Invalid token type");
281
+        }
282
+
283
+        if ($savedToken->getPassword() === null) {
284
+            throw new PasswordlessTokenException();
285
+        }
286
+
287
+        // Decrypt private key with tokenId
288
+        $privateKey = $this->decrypt($savedToken->getPrivateKey(), $tokenId);
289
+
290
+        // Decrypt password with private key
291
+        return $this->decryptPassword($savedToken->getPassword(), $privateKey);
292
+    }
293
+
294
+    public function setPassword(IToken $token, string $tokenId, string $password) {
295
+        $this->cache->clear();
296
+
297
+        if (!($token instanceof PublicKeyToken)) {
298
+            throw new InvalidTokenException("Invalid token type");
299
+        }
300
+
301
+        // When changing passwords all temp tokens are deleted
302
+        $this->mapper->deleteTempToken($token);
303
+
304
+        // Update the password for all tokens
305
+        $tokens = $this->mapper->getTokenByUser($token->getUID());
306
+        $hashedPassword = $this->hashPassword($password);
307
+        foreach ($tokens as $t) {
308
+            $publicKey = $t->getPublicKey();
309
+            $t->setPassword($this->encryptPassword($password, $publicKey));
310
+            $t->setPasswordHash($hashedPassword);
311
+            $this->updateToken($t);
312
+        }
313
+    }
314
+
315
+    private function hashPassword(string $password): string {
316
+        return $this->hasher->hash(sha1($password) . $password);
317
+    }
318
+
319
+    public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
320
+        $this->cache->clear();
321
+
322
+        if (!($token instanceof PublicKeyToken)) {
323
+            throw new InvalidTokenException("Invalid token type");
324
+        }
325
+
326
+        // Decrypt private key with oldTokenId
327
+        $privateKey = $this->decrypt($token->getPrivateKey(), $oldTokenId);
328
+        // Encrypt with the new token
329
+        $token->setPrivateKey($this->encrypt($privateKey, $newTokenId));
330
+
331
+        $token->setToken($this->hashToken($newTokenId));
332
+        $this->updateToken($token);
333
+
334
+        return $token;
335
+    }
336
+
337
+    private function encrypt(string $plaintext, string $token): string {
338
+        $secret = $this->config->getSystemValue('secret');
339
+        return $this->crypto->encrypt($plaintext, $token . $secret);
340
+    }
341
+
342
+    /**
343
+     * @throws InvalidTokenException
344
+     */
345
+    private function decrypt(string $cipherText, string $token): string {
346
+        $secret = $this->config->getSystemValue('secret');
347
+        try {
348
+            return $this->crypto->decrypt($cipherText, $token . $secret);
349
+        } catch (\Exception $ex) {
350
+            // Retry with empty secret as a fallback for instances where the secret might not have been set by accident
351
+            try {
352
+                return $this->crypto->decrypt($cipherText, $token);
353
+            } catch (\Exception $ex2) {
354
+                // Delete the invalid token
355
+                $this->invalidateToken($token);
356
+                throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2);
357
+            }
358
+        }
359
+    }
360
+
361
+    private function encryptPassword(string $password, string $publicKey): string {
362
+        openssl_public_encrypt($password, $encryptedPassword, $publicKey, OPENSSL_PKCS1_OAEP_PADDING);
363
+        $encryptedPassword = base64_encode($encryptedPassword);
364
+
365
+        return $encryptedPassword;
366
+    }
367
+
368
+    private function decryptPassword(string $encryptedPassword, string $privateKey): string {
369
+        $encryptedPassword = base64_decode($encryptedPassword);
370
+        openssl_private_decrypt($encryptedPassword, $password, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
371
+
372
+        return $password;
373
+    }
374
+
375
+    private function hashToken(string $token): string {
376
+        $secret = $this->config->getSystemValue('secret');
377
+        return hash('sha512', $token . $secret);
378
+    }
379
+
380
+    /**
381
+     * @deprecated Fallback for instances where the secret might not have been set by accident
382
+     */
383
+    private function hashTokenWithEmptySecret(string $token): string {
384
+        return hash('sha512', $token);
385
+    }
386
+
387
+    /**
388
+     * @throws \RuntimeException when OpenSSL reports a problem
389
+     */
390
+    private function newToken(string $token,
391
+                                string $uid,
392
+                                string $loginName,
393
+                                $password,
394
+                                string $name,
395
+                                int $type,
396
+                                int $remember): PublicKeyToken {
397
+        $dbToken = new PublicKeyToken();
398
+        $dbToken->setUid($uid);
399
+        $dbToken->setLoginName($loginName);
400
+
401
+        $config = array_merge([
402
+            'digest_alg' => 'sha512',
403
+            'private_key_bits' => $password !== null && strlen($password) > 250 ? 4096 : 2048,
404
+        ], $this->config->getSystemValue('openssl', []));
405
+
406
+        // Generate new key
407
+        $res = openssl_pkey_new($config);
408
+        if ($res === false) {
409
+            $this->logOpensslError();
410
+            throw new \RuntimeException('OpenSSL reported a problem');
411
+        }
412
+
413
+        if (openssl_pkey_export($res, $privateKey, null, $config) === false) {
414
+            $this->logOpensslError();
415
+            throw new \RuntimeException('OpenSSL reported a problem');
416
+        }
417
+
418
+        // Extract the public key from $res to $pubKey
419
+        $publicKey = openssl_pkey_get_details($res);
420
+        $publicKey = $publicKey['key'];
421
+
422
+        $dbToken->setPublicKey($publicKey);
423
+        $dbToken->setPrivateKey($this->encrypt($privateKey, $token));
424
+
425
+        if (!is_null($password) && $this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
426
+            if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
427
+                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');
428
+            }
429
+            $dbToken->setPassword($this->encryptPassword($password, $publicKey));
430
+            $dbToken->setPasswordHash($this->hashPassword($password));
431
+        }
432
+
433
+        $dbToken->setName($name);
434
+        $dbToken->setToken($this->hashToken($token));
435
+        $dbToken->setType($type);
436
+        $dbToken->setRemember($remember);
437
+        $dbToken->setLastActivity($this->time->getTime());
438
+        $dbToken->setLastCheck($this->time->getTime());
439
+        $dbToken->setVersion(PublicKeyToken::VERSION);
440
+
441
+        return $dbToken;
442
+    }
443
+
444
+    public function markPasswordInvalid(IToken $token, string $tokenId) {
445
+        $this->cache->clear();
446
+
447
+        if (!($token instanceof PublicKeyToken)) {
448
+            throw new InvalidTokenException("Invalid token type");
449
+        }
450
+
451
+        $token->setPasswordInvalid(true);
452
+        $this->mapper->update($token);
453
+    }
454
+
455
+    public function updatePasswords(string $uid, string $password) {
456
+        $this->cache->clear();
457
+
458
+        // prevent setting an empty pw as result of pw-less-login
459
+        if ($password === '' || !$this->config->getSystemValueBool('auth.storeCryptedPassword', true)) {
460
+            return;
461
+        }
462
+
463
+        // Update the password for all tokens
464
+        $tokens = $this->mapper->getTokenByUser($uid);
465
+        $newPasswordHash = null;
466
+
467
+        /**
468
+         * - true: The password hash could not be verified anymore
469
+         *     and the token needs to be updated with the newly encrypted password
470
+         * - false: The hash could still be verified
471
+         * - missing: The hash needs to be verified
472
+         */
473
+        $hashNeedsUpdate = [];
474
+
475
+        foreach ($tokens as $t) {
476
+            if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) {
477
+                if ($t->getPasswordHash() === null) {
478
+                    $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
479
+                } elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) {
480
+                    $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
481
+                } else {
482
+                    $hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false;
483
+                }
484
+            }
485
+            $needsUpdating = $hashNeedsUpdate[$t->getPasswordHash() ?: ''] ?? true;
486
+
487
+            if ($needsUpdating) {
488
+                if ($newPasswordHash === null) {
489
+                    $newPasswordHash = $this->hashPassword($password);
490
+                }
491
+
492
+                $publicKey = $t->getPublicKey();
493
+                $t->setPassword($this->encryptPassword($password, $publicKey));
494
+                $t->setPasswordHash($newPasswordHash);
495
+                $t->setPasswordInvalid(false);
496
+                $this->updateToken($t);
497
+            }
498
+        }
499
+
500
+        // If password hashes are different we update them all to be equal so
501
+        // that the next execution only needs to verify once
502
+        if (count($hashNeedsUpdate) > 1) {
503
+            $newPasswordHash = $this->hashPassword($password);
504
+            $this->mapper->updateHashesForUser($uid, $newPasswordHash);
505
+        }
506
+    }
507
+
508
+    private function logOpensslError() {
509
+        $errors = [];
510
+        while ($error = openssl_error_string()) {
511
+            $errors[] = $error;
512
+        }
513
+        $this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
514
+    }
515 515
 }
Please login to merge, or discard this patch.
Spacing   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -99,13 +99,13 @@  discard block
 block discarded – undo
99 99
 								  int $type = IToken::TEMPORARY_TOKEN,
100 100
 								  int $remember = IToken::DO_NOT_REMEMBER): IToken {
101 101
 		if (mb_strlen($name) > 128) {
102
-			$name = mb_substr($name, 0, 120) . '…';
102
+			$name = mb_substr($name, 0, 120).'…';
103 103
 		}
104 104
 
105 105
 		// We need to check against one old token to see if there is a password
106 106
 		// hash that we can reuse for detecting outdated passwords
107 107
 		$randomOldToken = $this->mapper->getFirstTokenForUser($uid);
108
-		$oldTokenMatches = $randomOldToken && $this->hasher->verify(sha1($password) . $password, $randomOldToken->getPasswordHash());
108
+		$oldTokenMatches = $randomOldToken && $this->hasher->verify(sha1($password).$password, $randomOldToken->getPasswordHash());
109 109
 
110 110
 		$dbToken = $this->newToken($token, $uid, $loginName, $password, $name, $type, $remember);
111 111
 
@@ -131,7 +131,7 @@  discard block
 block discarded – undo
131 131
 		if (isset($this->cache[$tokenHash])) {
132 132
 			if ($this->cache[$tokenHash] instanceof DoesNotExistException) {
133 133
 				$ex = $this->cache[$tokenHash];
134
-				throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
134
+				throw new InvalidTokenException("Token does not exist: ".$ex->getMessage(), 0, $ex);
135 135
 			}
136 136
 			$token = $this->cache[$tokenHash];
137 137
 		} else {
@@ -145,12 +145,12 @@  discard block
 block discarded – undo
145 145
 					$this->rotate($token, $tokenId, $tokenId);
146 146
 				} catch (DoesNotExistException $ex2) {
147 147
 					$this->cache[$tokenHash] = $ex2;
148
-					throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex);
148
+					throw new InvalidTokenException("Token does not exist: ".$ex->getMessage(), 0, $ex);
149 149
 				}
150 150
 			}
151 151
 		}
152 152
 
153
-		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
153
+		if ((int) $token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
154 154
 			throw new ExpiredTokenException($token);
155 155
 		}
156 156
 
@@ -170,10 +170,10 @@  discard block
 block discarded – undo
170 170
 		try {
171 171
 			$token = $this->mapper->getTokenById($tokenId);
172 172
 		} catch (DoesNotExistException $ex) {
173
-			throw new InvalidTokenException("Token with ID $tokenId does not exist: " . $ex->getMessage(), 0, $ex);
173
+			throw new InvalidTokenException("Token with ID $tokenId does not exist: ".$ex->getMessage(), 0, $ex);
174 174
 		}
175 175
 
176
-		if ((int)$token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
176
+		if ((int) $token->getExpires() !== 0 && $token->getExpires() < $this->time->getTime()) {
177 177
 			throw new ExpiredTokenException($token);
178 178
 		}
179 179
 
@@ -192,7 +192,7 @@  discard block
 block discarded – undo
192 192
 	public function renewSessionToken(string $oldSessionId, string $sessionId): IToken {
193 193
 		$this->cache->clear();
194 194
 
195
-		return $this->atomic(function () use ($oldSessionId, $sessionId) {
195
+		return $this->atomic(function() use ($oldSessionId, $sessionId) {
196 196
 			$token = $this->getToken($oldSessionId);
197 197
 
198 198
 			if (!($token instanceof PublicKeyToken)) {
@@ -237,10 +237,10 @@  discard block
 block discarded – undo
237 237
 		$this->cache->clear();
238 238
 
239 239
 		$olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24);
240
-		$this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']);
240
+		$this->logger->debug('Invalidating session tokens older than '.date('c', $olderThan), ['app' => 'cron']);
241 241
 		$this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER);
242 242
 		$rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15);
243
-		$this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']);
243
+		$this->logger->debug('Invalidating remembered session tokens older than '.date('c', $rememberThreshold), ['app' => 'cron']);
244 244
 		$this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER);
245 245
 	}
246 246
 
@@ -313,7 +313,7 @@  discard block
 block discarded – undo
313 313
 	}
314 314
 
315 315
 	private function hashPassword(string $password): string {
316
-		return $this->hasher->hash(sha1($password) . $password);
316
+		return $this->hasher->hash(sha1($password).$password);
317 317
 	}
318 318
 
319 319
 	public function rotate(IToken $token, string $oldTokenId, string $newTokenId): IToken {
@@ -336,7 +336,7 @@  discard block
 block discarded – undo
336 336
 
337 337
 	private function encrypt(string $plaintext, string $token): string {
338 338
 		$secret = $this->config->getSystemValue('secret');
339
-		return $this->crypto->encrypt($plaintext, $token . $secret);
339
+		return $this->crypto->encrypt($plaintext, $token.$secret);
340 340
 	}
341 341
 
342 342
 	/**
@@ -345,7 +345,7 @@  discard block
 block discarded – undo
345 345
 	private function decrypt(string $cipherText, string $token): string {
346 346
 		$secret = $this->config->getSystemValue('secret');
347 347
 		try {
348
-			return $this->crypto->decrypt($cipherText, $token . $secret);
348
+			return $this->crypto->decrypt($cipherText, $token.$secret);
349 349
 		} catch (\Exception $ex) {
350 350
 			// Retry with empty secret as a fallback for instances where the secret might not have been set by accident
351 351
 			try {
@@ -353,7 +353,7 @@  discard block
 block discarded – undo
353 353
 			} catch (\Exception $ex2) {
354 354
 				// Delete the invalid token
355 355
 				$this->invalidateToken($token);
356
-				throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2);
356
+				throw new InvalidTokenException("Could not decrypt token password: ".$ex->getMessage(), 0, $ex2);
357 357
 			}
358 358
 		}
359 359
 	}
@@ -374,7 +374,7 @@  discard block
 block discarded – undo
374 374
 
375 375
 	private function hashToken(string $token): string {
376 376
 		$secret = $this->config->getSystemValue('secret');
377
-		return hash('sha512', $token . $secret);
377
+		return hash('sha512', $token.$secret);
378 378
 	}
379 379
 
380 380
 	/**
@@ -476,7 +476,7 @@  discard block
 block discarded – undo
476 476
 			if (!isset($hashNeedsUpdate[$t->getPasswordHash()])) {
477 477
 				if ($t->getPasswordHash() === null) {
478 478
 					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
479
-				} elseif (!$this->hasher->verify(sha1($password) . $password, $t->getPasswordHash())) {
479
+				} elseif (!$this->hasher->verify(sha1($password).$password, $t->getPasswordHash())) {
480 480
 					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = true;
481 481
 				} else {
482 482
 					$hashNeedsUpdate[$t->getPasswordHash() ?: ''] = false;
@@ -510,6 +510,6 @@  discard block
 block discarded – undo
510 510
 		while ($error = openssl_error_string()) {
511 511
 			$errors[] = $error;
512 512
 		}
513
-		$this->logger->critical('Something is wrong with your openssl setup: ' . implode(', ', $errors));
513
+		$this->logger->critical('Something is wrong with your openssl setup: '.implode(', ', $errors));
514 514
 	}
515 515
 }
Please login to merge, or discard this patch.