Project import generated by Copybara.

GitOrigin-RevId: 63746295f1a5ab5a619056791995793d65529e62
diff --git a/src/inc/secondFactor.php b/src/inc/secondFactor.php
new file mode 100644
index 0000000..1b0de75
--- /dev/null
+++ b/src/inc/secondFactor.php
@@ -0,0 +1,288 @@
+<?php
+require_once(__DIR__."/../lib/GoogleAuthenticator/GoogleAuthenticator.php");
+require_once(__DIR__."/../lib/WebAuthn/WebAuthn.php");
+
+class secondFactor {
+  public static function isAvailable() {
+    global $conf;
+    return (($conf["secondFactor"]["enabled"] ?? false) === true);
+  }
+
+  public static function checkAvailability() {
+    global $conf;
+
+    if (!self::isAvailable()) {
+      security::notFound();
+    }
+  }
+
+  public static function isEnabled($person = "ME") {
+    if (!self::isAvailable()) return false;
+    return (people::userData("secondfactor", $person) == 1);
+  }
+
+  public static function generateSecret() {
+    $authenticator = new GoogleAuthenticator();
+    return $authenticator->generateSecret();
+  }
+
+  public static function isValidSecret($secret) {
+    return (strlen($secret) === 32);
+  }
+
+  public static function checkCode($secret, $code) {
+    $authenticator = new GoogleAuthenticator();
+    return $authenticator->checkCode($secret, $code);
+  }
+
+  public static function checkPersonCode($person, $code) {
+    global $con;
+
+    $sperson = (int)$person;
+    $query = mysqli_query($con, "SELECT secret FROM totp WHERE person = $sperson LIMIT 1");
+    if ($query === false || !mysqli_num_rows($query)) return false;
+
+    $row = mysqli_fetch_assoc($query);
+
+    return self::checkCode($row["secret"], $code);
+  }
+
+  public static function disable($person = "ME", $personDisable = true) {
+    global $con;
+
+    if ($person == "ME") $person = people::userData("id");
+    $sperson = (int)$person;
+
+    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"));
+  }
+
+  public static function enable($secret, $person = "ME") {
+    global $con;
+
+    if (!self::isValidSecret($secret)) return false;
+
+    if ($person == "ME") $person = people::userData("id");
+
+    self::disable($person, false);
+
+    $sperson = (int)$person;
+    $ssecret = db::sanitize($secret);
+    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"));
+  }
+
+  private static function completeLogin($success) {
+    if ($success === true) {
+      $_SESSION["id"] = $_SESSION["firstfactorid"];
+      unset($_SESSION["firstfactorid"]);
+      return true;
+    }
+
+    if ($success === false) {
+      unset($_SESSION["firstfactorid"]);
+      return true;
+    }
+
+    return false;
+  }
+
+  public static function completeCodeChallenge($code) {
+    global $_SESSION;
+
+    $success = self::checkPersonCode($_SESSION["firstfactorid"], $code);
+    self::completeLogin($success);
+    return $success;
+  }
+
+  private static function newWebAuthn() {
+    global $conf;
+
+    if (!isset($conf["secondFactor"]) || !isset($conf["secondFactor"]["origin"])) {
+      throw new Exception('secondFactor is not enabled (or the origin is not set) in config.php.');
+    }
+
+    return new \lbuchs\WebAuthn\WebAuthn($conf["appName"], $conf["secondFactor"]["origin"], ["none"]);
+  }
+
+  public static function createRegistrationChallenge() {
+    global $_SESSION;
+
+    $WebAuthn = self::newWebAuthn();
+
+    $credentialIds = self::getCredentialIds();
+    $createArgs = $WebAuthn->getCreateArgs(people::userData("id"), people::userData("username"), people::userData("name"), 20, false, false, ($credentialIds === false ? [] : $credentialIds));
+    $_SESSION['webauthnchallenge'] = $WebAuthn->getChallenge();
+
+    return $createArgs;
+  }
+
+  public static function addSecurityKeyToDB($credentialId, $credentialPublicKey, $name) {
+    global $con;
+
+    $person = people::userData("id");
+    if ($person === false) return false;
+    $sperson = (int)$person;
+
+    $sname = db::sanitize($name);
+    $scredentialId = db::sanitize($credentialId);
+    $scredentialPublicKey = db::sanitize($credentialPublicKey);
+    $stime = (int)time();
+
+    return mysqli_query($con, "INSERT INTO securitykeys (person, name, credentialid, credentialpublickey, added) VALUES ($sperson, '$sname', '$scredentialId', '$scredentialPublicKey', $stime)");
+  }
+
+  public static function completeRegistrationChallenge($clientDataJSON, $attestationObject, $name) {
+    global $_SESSION;
+
+    $clientDataJSON = base64_decode($clientDataJSON);
+    $attestationObject = base64_decode($attestationObject);
+
+    if (!isset($_SESSION["webauthnchallenge"])) {
+      throw new Exception('The user didn\'t start the webauthn challenge.');
+    }
+    $challenge = $_SESSION["webauthnchallenge"];
+    unset($_SESSION["webauthnchallenge"]);
+
+    $WebAuthn = self::newWebAuthn();
+    $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge);
+
+    if (!self::addSecurityKeyToDB($data->credentialId, $data->credentialPublicKey, $name)) {
+      throw new Exception('Failed adding security key to DB.');
+    }
+
+    return ["status" => "ok"];
+  }
+
+  public static function getSecurityKeys($person = "ME") {
+    global $con;
+
+    if ($person == "ME") $person = people::userData("id");
+    if ($person === false) return false;
+    $sperson = (int)$person;
+
+    $query = mysqli_query($con, "SELECT * FROM securitykeys WHERE person = $sperson");
+    if ($query === false) return false;
+
+    $securityKeys = [];
+    while ($row = mysqli_fetch_assoc($query)) {
+      $securityKeys[] = $row;
+    }
+
+    return $securityKeys;
+  }
+
+  public static function getCredentialIds($person = "ME") {
+    $securityKeys = self::getSecurityKeys($person);
+    if ($securityKeys === false) return false;
+
+    $credentials = [];
+    foreach ($securityKeys as $s) $credentials[] = $s["credentialid"];
+
+    return $credentials;
+  }
+
+  public static function hasSecurityKeys($person) {
+    global $con;
+
+    $sperson = (int)$person;
+
+    $query = mysqli_query($con, "SELECT 1 FROM securitykeys WHERE person = $sperson LIMIT 1");
+    if ($query === false) return false;
+
+    return (mysqli_num_rows($query) > 0);
+  }
+
+  public static function createValidationChallenge() {
+    global $_SESSION;
+
+    $WebAuthn = self::newWebAuthn();
+
+    if (!isset($_SESSION["firstfactorid"])) {
+      throw new Exception('User didn\'t log in with the first factor.');
+    }
+    $credentialIds = self::getCredentialIds($_SESSION["firstfactorid"]);
+    if ($credentialIds === false || empty($credentialIds)) {
+      throw new Exception('The user credentials could not be obtained.');
+    }
+
+    $getArgs = $WebAuthn->getGetArgs($credentialIds);
+    $_SESSION['webauthnvalidationchallenge'] = $WebAuthn->getChallenge();
+
+    return $getArgs;
+  }
+
+  public static function getSecurityKey($credentialId, $person) {
+    $securityKeys = self::getSecurityKeys($person);
+
+    foreach ($securityKeys as $s) {
+      if ($s["credentialid"] == $credentialId) {
+        return $s;
+      }
+    }
+
+    return null;
+  }
+
+  public static function getSecurityKeyById($id) {
+    global $con;
+
+    $query = mysqli_query($con, "SELECT * FROM securitykeys WHERE id = $id");
+    if ($query === false || !mysqli_num_rows($query)) return false;
+
+    return mysqli_fetch_assoc($query);
+  }
+
+  public static function removeSecurityKey($id) {
+    global $con;
+
+    $sid = (int)$id;
+    $sperson = (int)people::userData("id");
+    if ($sperson === false) return false;
+
+    return mysqli_query($con, "DELETE FROM securitykeys WHERE id = $sid and PERSON = $sperson LIMIT 1");
+  }
+
+  private static function recordSecurityKeyUsageToDB($id) {
+    global $con;
+
+    $sid = (int)$id;
+    $stime = (int)time();
+
+    return mysqli_query($con, "UPDATE securitykeys SET lastused = $stime WHERE id = $sid LIMIT 1");
+  }
+
+  public static function completeValidationChallenge($id, $clientDataJSON, $authenticatorData, $signature) {
+    global $_SESSION;
+
+    $id = base64_decode($id);
+    $clientDataJSON = base64_decode($clientDataJSON);
+    $authenticatorData = base64_decode($authenticatorData);
+    $signature = base64_decode($signature);
+
+    if (!isset($_SESSION["webauthnvalidationchallenge"])) {
+      throw new Exception('The user didn\'t start the webauthn challenge.');
+    }
+    $challenge = $_SESSION["webauthnvalidationchallenge"];
+    unset($_SESSION["webauthnvalidationchallenge"]);
+
+    $securityKey = self::getSecurityKey($id, $_SESSION["firstfactorid"]);
+    $credentialPublicKey = $securityKey["credentialpublickey"] ?? null;
+    if ($credentialPublicKey === null) {
+      self::completeLogin(false);
+      throw new Exception('The security key could not be found.');
+    }
+
+    try {
+      $WebAuthn = self::newWebAuthn();
+
+      $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge);
+    } catch (Throwable $ex) {
+      self::completeLogin(false);
+      throw $ex;
+    }
+
+    self::recordSecurityKeyUsageToDB($securityKey["id"]);
+    self::completeLogin(true);
+
+    return ["status" => "ok"];
+  }
+}