diff options
-rw-r--r-- | classes/pref/prefs.php | 107 | ||||
-rw-r--r-- | js/PrefHelpers.js | 38 | ||||
-rw-r--r-- | plugins/auth_internal/init.php | 22 |
3 files changed, 163 insertions, 4 deletions
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 "</form>"; print "</div>"; # content pane - print "<div dojoType='dijit.layout.ContentPane' title=\"".__('App passwords')."\">"; - 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 "<div dojoType='dijit.layout.ContentPane' title=\"" . __('App passwords') . "\">"; + print_notice("You can create separate passwords for the API clients. Using one is required if you enable OTP."); - print "</div>"; # content pane + print "<div id='app_passwords_holder'>"; + $this->appPasswordList(); + print "</div>"; + + print "<hr>"; + + print "<button style='float : left' class='alt-primary' dojoType='dijit.form.Button' + onclick=\"Helpers.AppPasswords.generate()\">" . + __('Generate new password') . "</button> "; + + print "<button style='float : left' class='alt-danger' dojoType='dijit.form.Button' + onclick=\"Helpers.AppPasswords.removeSelected()\">" . + __('Remove selected passwords') . "</button>"; + + print "</div>"; # content pane + } print "<div dojoType='dijit.layout.ContentPane' title=\"".__('One time passwords / Authenticator')."\">"; @@ -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 "<h3>" . __("Scan the following code by the Authenticator application or copy the key manually:") . "</h3>"; @@ -1221,4 +1237,87 @@ class Pref_Prefs extends Handler_Protected { } return ""; } + + private function appPasswordList() { + print "<div dojoType='fox.Toolbar'>"; + print "<div dojoType='fox.form.DropDownButton'>" . + "<span>" . __('Select') . "</span>"; + print "<div dojoType='dijit.Menu' style='display: none'>"; + print "<div onclick=\"Tables.select('app-password-list', true)\" + dojoType=\"dijit.MenuItem\">" . __('All') . "</div>"; + print "<div onclick=\"Tables.select('app-password-list', false)\" + dojoType=\"dijit.MenuItem\">" . __('None') . "</div>"; + print "</div></div>"; + print "</div>"; #toolbar + + print "<div class='panel panel-scrollable'>"; + print "<table width='100%' id='app-password-list'>"; + print "<tr>"; + print "<th width='2%'></th>"; + print "<th align='left'>".__("Description")."</th>"; + print "<th align='right'>".__("Created")."</th>"; + print "<th align='right'>".__("Last used")."</th>"; + print "</tr>"; + + $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 "<tr data-row-id='$row_id'>"; + + print "<td align='center'> + <input onclick='Tables.onRowChecked(this)' dojoType='dijit.form.CheckBox' type='checkbox'></td>"; + print "<td>" . htmlspecialchars($row["title"]) . "</td>"; + + print "<td align='right' class='text-muted'>"; + print make_local_datetime($row['created'], false); + print "</td>"; + + print "<td align='right' class='text-muted'>"; + print make_local_datetime($row['last_used'], false); + print "</td>"; + + print "</tr>"; + } + + print "</table>"; + print "</div>"; + } + + 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 <strong>%s</strong> 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; } |