diff --git a/api/actions/auth.php b/api/actions/auth.php index ede643b..d1cfe9c 100644 --- a/api/actions/auth.php +++ b/api/actions/auth.php @@ -7,7 +7,16 @@ */ $user = User::byUsername($VARS['username']); -if ($user->checkPassword($VARS['password'])) { + +$ok = false; +if (empty($VARS['apppass']) && ($user->checkPassword($VARS['password']) || $user->checkAppPassword($VARS['password']))) { + $ok = true; +} else { + if ((!$user->has2fa() && $user->checkPassword($VARS['password'])) || $user->checkAppPassword($VARS['password'])) { + $ok = true; + } +} +if ($ok) { Log::insert(LogType::API_AUTH_OK, null, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey()); sendJsonResp($Strings->get("login successful", false), "OK"); } else { diff --git a/api/actions/login.php b/api/actions/login.php index 6372b16..c7ed41b 100644 --- a/api/actions/login.php +++ b/api/actions/login.php @@ -8,7 +8,7 @@ engageRateLimit(); $user = User::byUsername($VARS['username']); -if ($user->checkPassword($VARS['password'])) { +if ((!$user->has2fa() && $user->checkPassword($VARS['password'])) || $user->checkAppPassword($VARS['password'])) { switch ($user->getStatus()->getString()) { case "LOCKED_OR_DISABLED": Log::insert(LogType::API_LOGIN_FAILED, $uid, "Username: " . strtolower($VARS['username']) . ", Key: " . getCensoredKey()); diff --git a/api/apisettings.php b/api/apisettings.php index 32ce97f..8acb6f8 100644 --- a/api/apisettings.php +++ b/api/apisettings.php @@ -19,7 +19,8 @@ $APIS = [ "load" => "auth.php", "vars" => [ "username" => "string", - "password" => "string" + "password" => "string", + "apppass (optional)" => "/[0-1]/" ], "keytype" => "AUTH" ], diff --git a/api/functions.php b/api/functions.php index 551f34f..e77b507 100644 --- a/api/functions.php +++ b/api/functions.php @@ -90,11 +90,13 @@ function checkVars($vars, $or = false) { continue; } } - $checkmethod = "is_$val"; - if ($checkmethod($VARS[$key]) !== true) { - $ok[$key] = false; + + if (strpos($val, "/") === 0) { + // regex + $ok[$key] = preg_match($val, $VARS[$key]) === 1; } else { - $ok[$key] = true; + $checkmethod = "is_$val"; + $ok[$key] = !($checkmethod($VARS[$key]) !== true); } } if ($or) { diff --git a/database.mwb b/database.mwb index a7fea3b..873a326 100644 Binary files a/database.mwb and b/database.mwb differ diff --git a/database.sql b/database.sql index 8e52928..b15b8d8 100644 --- a/database.sql +++ b/database.sql @@ -1,5 +1,5 @@ -- MySQL Script generated by MySQL Workbench --- Mon 11 Feb 2019 02:58:22 PM MST +-- Mon 11 Feb 2019 04:07:57 PM MST -- Model: New Model Version: 1.0 -- MySQL Workbench Forward Engineering @@ -335,6 +335,25 @@ CREATE TABLE IF NOT EXISTS `userloginkeys` ( ENGINE = InnoDB; +-- ----------------------------------------------------- +-- Table `apppasswords` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `apppasswords` ( + `passid` INT NOT NULL AUTO_INCREMENT, + `hash` VARCHAR(255) NOT NULL, + `uid` INT NOT NULL, + `description` VARCHAR(255) NOT NULL, + PRIMARY KEY (`passid`, `uid`), + UNIQUE INDEX `passid_UNIQUE` (`passid` ASC), + INDEX `fk_apppasswords_accounts1_idx` (`uid` ASC), + CONSTRAINT `fk_apppasswords_accounts1` + FOREIGN KEY (`uid`) + REFERENCES `accounts` (`uid`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB; + + SET SQL_MODE=@OLD_SQL_MODE; SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS; diff --git a/database_upgrade/2.1_2.2.sql b/database_upgrade/2.1_2.2.sql index 486fbd2..7cfb910 100644 --- a/database_upgrade/2.1_2.2.sql +++ b/database_upgrade/2.1_2.2.sql @@ -29,4 +29,20 @@ ADD COLUMN `appname` VARCHAR(255) NOT NULL AFTER `uid`; ALTER TABLE `userloginkeys` ADD COLUMN `appicon` TINYTEXT NULL DEFAULT NULL AFTER `appname`; ALTER TABLE `apikeys` -ADD COLUMN `type` VARCHAR(45) NOT NULL DEFAULT 'FULL' AFTER `notes`; \ No newline at end of file +ADD COLUMN `type` VARCHAR(45) NOT NULL DEFAULT 'FULL' AFTER `notes`; + +CREATE TABLE IF NOT EXISTS `apppasswords` ( + `passid` INT(11) NOT NULL AUTO_INCREMENT, + `hash` VARCHAR(255) NOT NULL, + `uid` INT(11) NOT NULL, + `description` VARCHAR(255) NOT NULL, + PRIMARY KEY (`passid`, `uid`), + UNIQUE INDEX `passid_UNIQUE` (`passid` ASC), + INDEX `fk_apppasswords_accounts1_idx` (`uid` ASC), + CONSTRAINT `fk_apppasswords_accounts1` + FOREIGN KEY (`uid`) + REFERENCES `accounthub`.`accounts` (`uid`) + ON DELETE NO ACTION + ON UPDATE NO ACTION) +ENGINE = InnoDB +DEFAULT CHARACTER SET = utf8; \ No newline at end of file diff --git a/langs/en/apppasswords.json b/langs/en/apppasswords.json new file mode 100644 index 0000000..9eb1cc0 --- /dev/null +++ b/langs/en/apppasswords.json @@ -0,0 +1,11 @@ +{ + "App Passwords": "App Passwords", + "app passwords explained": "Use app passwords instead of your actual password when logging into apps with your {site_name} login. App passwords are required in some places when you have 2-factor authentication enabled.", + "app password setup instructions": "Use the username and password below to log in to {app_name}. You'll only be shown this password one time.", + "App name": "App name", + "Generate password": "Generate password", + "Revoke password": "Revoke password", + "You don't have any app passwords.": "You don't have any app passwords.", + "Done": "Done", + "App passwords are not allowed here.": "App passwords are not allowed here." +} diff --git a/lib/User.lib.php b/lib/User.lib.php index edc3127..273c97c 100644 --- a/lib/User.lib.php +++ b/lib/User.lib.php @@ -19,6 +19,7 @@ class User { private $authsecret; private $has2fa = false; private $exists = false; + private $apppasswords = []; public function __construct(int $uid, string $username = "") { global $database; @@ -32,6 +33,7 @@ class User { $this->authsecret = $user['authsecret']; $this->has2fa = !empty($user['authsecret']); $this->exists = true; + $this->apppasswords = $database->select('apppasswords', 'hash', ['uid' => $this->uid]); } else { $this->uid = $uid; $this->username = $username; @@ -107,6 +109,20 @@ class User { return password_verify($password, $this->passhash); } + /** + * Check the given password against the user's app passwords. + * @param string $apppassword + * @return bool + */ + function checkAppPassword(string $apppassword): bool { + foreach ($this->apppasswords as $hash) { + if (password_verify($apppassword, $hash)) { + return true; + } + } + return false; + } + /** * Change the user's password. * @global $database $database diff --git a/login/index.php b/login/index.php index 55c63f7..8b8ffc1 100644 --- a/login/index.php +++ b/login/index.php @@ -93,6 +93,9 @@ if (!empty($_SESSION['check'])) { } } else { $error = $Strings->get("Password incorrect.", false); + if ($user->checkAppPassword($_POST['password'])) { + $error = $Strings->get("App passwords are not allowed here.", false); + } Log::insert(LogType::LOGIN_FAILED, $user); } break; diff --git a/pages/security.php b/pages/security.php index d1bb78e..d0a95ab 100644 --- a/pages/security.php +++ b/pages/security.php @@ -10,6 +10,12 @@ use Endroid\QrCode\ErrorCorrectionLevel; use Endroid\QrCode\QrCode; $user = new User($_SESSION['uid']); + +if (!empty($_GET['delpass'])) { + if ($database->has("apppasswords", ["AND" => ["uid" => $_SESSION['uid'], "passid" => $_GET['delpass']]])) { + $database->delete("apppasswords", ["AND" => ["uid" => $_SESSION['uid'], "passid" => $_GET['delpass']]]); + } +} ?>
@@ -138,5 +144,87 @@ $user = new User($_SESSION['uid']); ?>
- + +
+
+ insert('apppasswords', ['uid' => $_SESSION['uid'], 'hash' => password_hash($chunk_code, PASSWORD_DEFAULT), 'description' => $desc]); + ?> +
+
get("App Passwords"); ?>
+
+ + build("app password setup instructions", ["app_name" => $desc]); ?> +
+
+
+
get("username"); ?>:
+
+
+
+
get("password"); ?>
+
+
+
+ + select("apppasswords", ["passid", "description"], ["uid" => $_SESSION['uid']]); + ?> +
+
get("App Passwords"); ?>
+
+

+ build("app passwords explained", ["site_name" => $SETTINGS['site_title']]); ?> +

+
+ " required /> + +
+
+
+
+ get("App Passwords"); ?> +
+ 0) { + foreach ($activecodes as $c) { + ?> + + +
+ get("You don't have any app passwords."); ?> +
+ +
+ +
+ +
\ No newline at end of file