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