blob: bcf893782c73d3e330405fea7e250fd41cac756a [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001<?php
Adrià Vilanova Martínez5af86512023-12-02 20:44:16 +01002/*
3 * hores
4 * Copyright (c) 2023 Adrià Vilanova Martínez
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, either version 3 of the
9 * License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
15 *
16 * You should have received a copy of the GNU Affero General Public
17 * License along with this program.
18 * If not, see http://www.gnu.org/licenses/.
19 */
20
Copybara botbe50d492023-11-30 00:16:42 +010021require_once(__DIR__."/../lib/GoogleAuthenticator/GoogleAuthenticator.php");
22require_once(__DIR__."/../lib/WebAuthn/WebAuthn.php");
23
24class secondFactor {
25 public static function isAvailable() {
26 global $conf;
27 return (($conf["secondFactor"]["enabled"] ?? false) === true);
28 }
29
30 public static function checkAvailability() {
31 global $conf;
32
33 if (!self::isAvailable()) {
34 security::notFound();
35 }
36 }
37
38 public static function isEnabled($person = "ME") {
39 if (!self::isAvailable()) return false;
40 return (people::userData("secondfactor", $person) == 1);
41 }
42
43 public static function generateSecret() {
44 $authenticator = new GoogleAuthenticator();
45 return $authenticator->generateSecret();
46 }
47
48 public static function isValidSecret($secret) {
49 return (strlen($secret) === 32);
50 }
51
52 public static function checkCode($secret, $code) {
53 $authenticator = new GoogleAuthenticator();
54 return $authenticator->checkCode($secret, $code);
55 }
56
57 public static function checkPersonCode($person, $code) {
58 global $con;
59
60 $sperson = (int)$person;
61 $query = mysqli_query($con, "SELECT secret FROM totp WHERE person = $sperson LIMIT 1");
62 if ($query === false || !mysqli_num_rows($query)) return false;
63
64 $row = mysqli_fetch_assoc($query);
65
66 return self::checkCode($row["secret"], $code);
67 }
68
69 public static function disable($person = "ME", $personDisable = true) {
70 global $con;
71
72 if ($person == "ME") $person = people::userData("id");
73 $sperson = (int)$person;
74
75 return (mysqli_query($con, "DELETE FROM totp WHERE person = $sperson") && (!$personDisable || mysqli_query($con, "UPDATE people SET secondfactor = 0 WHERE id = $sperson LIMIT 1")) && mysqli_query($con, "DELETE FROM securitykeys WHERE person = $sperson"));
76 }
77
78 public static function enable($secret, $person = "ME") {
79 global $con;
80
81 if (!self::isValidSecret($secret)) return false;
82
83 if ($person == "ME") $person = people::userData("id");
84
85 self::disable($person, false);
86
87 $sperson = (int)$person;
88 $ssecret = db::sanitize($secret);
89 return (mysqli_query($con, "INSERT INTO totp (person, secret) VALUES ($sperson, '$ssecret')") && mysqli_query($con, "UPDATE people SET secondfactor = 1 WHERE id = $sperson LIMIT 1"));
90 }
91
92 private static function completeLogin($success) {
93 if ($success === true) {
94 $_SESSION["id"] = $_SESSION["firstfactorid"];
95 unset($_SESSION["firstfactorid"]);
96 return true;
97 }
98
99 if ($success === false) {
100 unset($_SESSION["firstfactorid"]);
101 return true;
102 }
103
104 return false;
105 }
106
107 public static function completeCodeChallenge($code) {
108 global $_SESSION;
109
110 $success = self::checkPersonCode($_SESSION["firstfactorid"], $code);
111 self::completeLogin($success);
112 return $success;
113 }
114
115 private static function newWebAuthn() {
116 global $conf;
117
118 if (!isset($conf["secondFactor"]) || !isset($conf["secondFactor"]["origin"])) {
119 throw new Exception('secondFactor is not enabled (or the origin is not set) in config.php.');
120 }
121
122 return new \lbuchs\WebAuthn\WebAuthn($conf["appName"], $conf["secondFactor"]["origin"], ["none"]);
123 }
124
125 public static function createRegistrationChallenge() {
126 global $_SESSION;
127
128 $WebAuthn = self::newWebAuthn();
129
130 $credentialIds = self::getCredentialIds();
131 $createArgs = $WebAuthn->getCreateArgs(people::userData("id"), people::userData("username"), people::userData("name"), 20, false, false, ($credentialIds === false ? [] : $credentialIds));
132 $_SESSION['webauthnchallenge'] = $WebAuthn->getChallenge();
133
134 return $createArgs;
135 }
136
137 public static function addSecurityKeyToDB($credentialId, $credentialPublicKey, $name) {
138 global $con;
139
140 $person = people::userData("id");
141 if ($person === false) return false;
142 $sperson = (int)$person;
143
144 $sname = db::sanitize($name);
145 $scredentialId = db::sanitize($credentialId);
146 $scredentialPublicKey = db::sanitize($credentialPublicKey);
147 $stime = (int)time();
148
149 return mysqli_query($con, "INSERT INTO securitykeys (person, name, credentialid, credentialpublickey, added) VALUES ($sperson, '$sname', '$scredentialId', '$scredentialPublicKey', $stime)");
150 }
151
152 public static function completeRegistrationChallenge($clientDataJSON, $attestationObject, $name) {
153 global $_SESSION;
154
155 $clientDataJSON = base64_decode($clientDataJSON);
156 $attestationObject = base64_decode($attestationObject);
157
158 if (!isset($_SESSION["webauthnchallenge"])) {
159 throw new Exception('The user didn\'t start the webauthn challenge.');
160 }
161 $challenge = $_SESSION["webauthnchallenge"];
162 unset($_SESSION["webauthnchallenge"]);
163
164 $WebAuthn = self::newWebAuthn();
165 $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge);
166
167 if (!self::addSecurityKeyToDB($data->credentialId, $data->credentialPublicKey, $name)) {
168 throw new Exception('Failed adding security key to DB.');
169 }
170
171 return ["status" => "ok"];
172 }
173
174 public static function getSecurityKeys($person = "ME") {
175 global $con;
176
177 if ($person == "ME") $person = people::userData("id");
178 if ($person === false) return false;
179 $sperson = (int)$person;
180
181 $query = mysqli_query($con, "SELECT * FROM securitykeys WHERE person = $sperson");
182 if ($query === false) return false;
183
184 $securityKeys = [];
185 while ($row = mysqli_fetch_assoc($query)) {
186 $securityKeys[] = $row;
187 }
188
189 return $securityKeys;
190 }
191
192 public static function getCredentialIds($person = "ME") {
193 $securityKeys = self::getSecurityKeys($person);
194 if ($securityKeys === false) return false;
195
196 $credentials = [];
197 foreach ($securityKeys as $s) $credentials[] = $s["credentialid"];
198
199 return $credentials;
200 }
201
202 public static function hasSecurityKeys($person) {
203 global $con;
204
205 $sperson = (int)$person;
206
207 $query = mysqli_query($con, "SELECT 1 FROM securitykeys WHERE person = $sperson LIMIT 1");
208 if ($query === false) return false;
209
210 return (mysqli_num_rows($query) > 0);
211 }
212
213 public static function createValidationChallenge() {
214 global $_SESSION;
215
216 $WebAuthn = self::newWebAuthn();
217
218 if (!isset($_SESSION["firstfactorid"])) {
219 throw new Exception('User didn\'t log in with the first factor.');
220 }
221 $credentialIds = self::getCredentialIds($_SESSION["firstfactorid"]);
222 if ($credentialIds === false || empty($credentialIds)) {
223 throw new Exception('The user credentials could not be obtained.');
224 }
225
226 $getArgs = $WebAuthn->getGetArgs($credentialIds);
227 $_SESSION['webauthnvalidationchallenge'] = $WebAuthn->getChallenge();
228
229 return $getArgs;
230 }
231
232 public static function getSecurityKey($credentialId, $person) {
233 $securityKeys = self::getSecurityKeys($person);
234
235 foreach ($securityKeys as $s) {
236 if ($s["credentialid"] == $credentialId) {
237 return $s;
238 }
239 }
240
241 return null;
242 }
243
244 public static function getSecurityKeyById($id) {
245 global $con;
246
247 $query = mysqli_query($con, "SELECT * FROM securitykeys WHERE id = $id");
248 if ($query === false || !mysqli_num_rows($query)) return false;
249
250 return mysqli_fetch_assoc($query);
251 }
252
253 public static function removeSecurityKey($id) {
254 global $con;
255
256 $sid = (int)$id;
257 $sperson = (int)people::userData("id");
258 if ($sperson === false) return false;
259
260 return mysqli_query($con, "DELETE FROM securitykeys WHERE id = $sid and PERSON = $sperson LIMIT 1");
261 }
262
263 private static function recordSecurityKeyUsageToDB($id) {
264 global $con;
265
266 $sid = (int)$id;
267 $stime = (int)time();
268
269 return mysqli_query($con, "UPDATE securitykeys SET lastused = $stime WHERE id = $sid LIMIT 1");
270 }
271
272 public static function completeValidationChallenge($id, $clientDataJSON, $authenticatorData, $signature) {
273 global $_SESSION;
274
275 $id = base64_decode($id);
276 $clientDataJSON = base64_decode($clientDataJSON);
277 $authenticatorData = base64_decode($authenticatorData);
278 $signature = base64_decode($signature);
279
280 if (!isset($_SESSION["webauthnvalidationchallenge"])) {
281 throw new Exception('The user didn\'t start the webauthn challenge.');
282 }
283 $challenge = $_SESSION["webauthnvalidationchallenge"];
284 unset($_SESSION["webauthnvalidationchallenge"]);
285
286 $securityKey = self::getSecurityKey($id, $_SESSION["firstfactorid"]);
287 $credentialPublicKey = $securityKey["credentialpublickey"] ?? null;
288 if ($credentialPublicKey === null) {
289 self::completeLogin(false);
290 throw new Exception('The security key could not be found.');
291 }
292
293 try {
294 $WebAuthn = self::newWebAuthn();
295
296 $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge);
297 } catch (Throwable $ex) {
298 self::completeLogin(false);
299 throw $ex;
300 }
301
302 self::recordSecurityKeyUsageToDB($securityKey["id"]);
303 self::completeLogin(true);
304
305 return ["status" => "ok"];
306 }
307}