diff --git a/classes/pref/prefs.php b/classes/pref/prefs.php index d9482b966..76dc526ab 100644 --- a/classes/pref/prefs.php +++ b/classes/pref/prefs.php @@ -395,13 +395,29 @@ class Pref_Prefs extends Handler_Protected { print ""; print ""; # content pane - print "
"; - print_notice("You can create separate passwords for API clients. Using one is required if you enable OTP."); + if ($_SESSION["auth_module"] == "auth_internal") { + print "
"; + print_notice("You can create separate passwords for the API clients. Using one is required if you enable OTP."); - print "
"; # content pane + print "
"; + $this->appPasswordList(); + print "
"; + + print "
"; + + print " "; + + print ""; + + print "
"; # content pane + } print "
"; @@ -450,7 +466,7 @@ class Pref_Prefs extends Handler_Protected { } else { print_warning("You will need a compatible Authenticator to use this. Changing your password would automatically disable OTP."); - print_notice("You will also need to create a separate App password for API clients if you enable OTP."); + print_notice("You will need to use a separate password for the API clients if you enable OTP."); if (function_exists("imagecreatefromstring")) { print "

" . __("Scan the following code by the Authenticator application or copy the key manually:") . "

"; @@ -1221,4 +1237,87 @@ class Pref_Prefs extends Handler_Protected { } return ""; } + + private function appPasswordList() { + print "
"; + print "
" . + "" . __('Select') . ""; + print "
"; + print "
" . __('All') . "
"; + print "
" . __('None') . "
"; + print "
"; + print "
"; #toolbar + + print "
"; + print ""; + print ""; + print ""; + print ""; + print ""; + print ""; + print ""; + + $sth = $this->pdo->prepare("SELECT id, title, created, last_used + FROM ttrss_app_passwords WHERE owner_uid = ?"); + $sth->execute([$_SESSION['uid']]); + + while ($row = $sth->fetch()) { + + $row_id = $row["id"]; + + print ""; + + print ""; + print ""; + + print ""; + + print ""; + + print ""; + } + + print "
".__("Description")."".__("Created")."".__("Last used")."
+ " . htmlspecialchars($row["title"]) . ""; + print make_local_datetime($row['created'], false); + print ""; + print make_local_datetime($row['last_used'], false); + print "
"; + print "
"; + } + + private function encryptAppPassword($password) { + $salt = substr(bin2hex(get_random_bytes(24)), 0, 24); + + return "SSHA-512:".hash('sha512', $salt . $password). ":$salt"; + } + + function deleteAppPassword() { + $ids = explode(",", clean($_REQUEST['ids'])); + $ids_qmarks = arr_qmarks($ids); + + $sth = $this->pdo->prepare("DELETE FROM ttrss_app_passwords WHERE id IN ($ids_qmarks) AND owner_uid = ?"); + $sth->execute(array_merge($ids, [$_SESSION['uid']])); + + $this->appPasswordList(); + } + + function generateAppPassword() { + $title = clean($_REQUEST['title']); + $new_password = make_password(16); + $new_password_hash = $this->encryptAppPassword($new_password); + + print_warning(T_sprintf("Generated password %s for %s. Please remember it for future reference.", $new_password, $title)); + + $sth = $this->pdo->prepare("INSERT INTO ttrss_app_passwords + (title, pwd_hash, service, created, owner_uid) + VALUES + (?, ?, ?, NOW(), ?)"); + + $sth->execute([$title, $new_password_hash, Auth_Base::AUTH_SERVICE_API, $_SESSION['uid']]); + + $this->appPasswordList(); + } } diff --git a/js/PrefHelpers.js b/js/PrefHelpers.js index a3d122029..6a62cb593 100644 --- a/js/PrefHelpers.js +++ b/js/PrefHelpers.js @@ -1,5 +1,43 @@ define(["dojo/_base/declare"], function (declare) { Helpers = { + AppPasswords: { + getSelected: function() { + return Tables.getSelected("app-password-list"); + }, + updateContent: function(data) { + $("app_passwords_holder").innerHTML = data; + dojo.parser.parse("app_passwords_holder"); + }, + removeSelected: function() { + const rows = this.getSelected(); + + if (rows.length == 0) { + alert("No passwords selected."); + } else { + if (confirm(__("Remove selected app passwords?"))) { + + xhrPost("backend.php", {op: "pref-prefs", method: "deleteAppPassword", ids: rows.toString()}, (transport) => { + this.updateContent(transport.responseText); + Notify.close(); + }); + + Notify.progress("Loading, please wait..."); + } + } + }, + generate: function() { + const title = prompt("Password description:") + + if (title) { + xhrPost("backend.php", {op: "pref-prefs", method: "generateAppPassword", title: title}, (transport) => { + this.updateContent(transport.responseText); + Notify.close(); + }); + + Notify.progress("Loading, please wait..."); + } + }, + }, clearFeedAccessKeys: function() { if (confirm(__("This will invalidate all previously generated feed URLs. Continue?"))) { Notify.progress("Clearing URLs..."); diff --git a/plugins/auth_internal/init.php b/plugins/auth_internal/init.php index 576f8ef05..a374c0948 100644 --- a/plugins/auth_internal/init.php +++ b/plugins/auth_internal/init.php @@ -258,6 +258,28 @@ } private function check_app_password($login, $password, $service) { + $sth = $this->pdo->prepare("SELECT p.id, p.pwd_hash, u.id AS uid + FROM ttrss_app_passwords p, ttrss_users u + WHERE p.owner_uid = u.id AND u.login = ? AND service = ?"); + $sth->execute([$login, $service]); + + while ($row = $sth->fetch()) { + list ($algo, $hash, $salt) = explode(":", $row["pwd_hash"]); + + if ($algo == "SSHA-512") { + $test_hash = hash('sha512', $salt . $password); + + if ($test_hash == $hash) { + $usth = $this->pdo->prepare("UPDATE ttrss_app_passwords SET last_used = NOW() WHERE id = ?"); + $usth->execute([$row['id']]); + + return $row['uid']; + } + } else { + user_error("Got unknown algo of app password for user $login: $algo"); + } + } + return false; }