From 6857b65a9147a61ef0e4c1286beb6d6c3f2f5404 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Felix=20D=C3=B6rre?= Date: Fri, 13 Nov 2015 18:58:57 +0100 Subject: [PATCH] add: internal api for password reset (with assurance) --- src/org/cacert/gigi/Gigi.java | 6 +- .../gigi/database/DatabaseConnection.java | 2 +- .../cacert/gigi/database/tableStructure.sql | 14 ++- .../cacert/gigi/database/upgrade/from_5.sql | 11 +++ src/org/cacert/gigi/dbObjects/User.java | 41 ++++++++ .../cacert/gigi/pages/PasswordResetForm.templ | 27 ++++++ .../cacert/gigi/pages/PasswordResetPage.java | 93 +++++++++++++++++++ tests/org/cacert/gigi/TestPasswordReset.java | 56 +++++++++++ 8 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 src/org/cacert/gigi/database/upgrade/from_5.sql create mode 100644 src/org/cacert/gigi/pages/PasswordResetForm.templ create mode 100644 src/org/cacert/gigi/pages/PasswordResetPage.java create mode 100644 tests/org/cacert/gigi/TestPasswordReset.java diff --git a/src/org/cacert/gigi/Gigi.java b/src/org/cacert/gigi/Gigi.java index f558a6e4..59273a32 100644 --- a/src/org/cacert/gigi/Gigi.java +++ b/src/org/cacert/gigi/Gigi.java @@ -36,14 +36,15 @@ import org.cacert.gigi.pages.LoginPage; import org.cacert.gigi.pages.LogoutPage; import org.cacert.gigi.pages.MainPage; import org.cacert.gigi.pages.Page; +import org.cacert.gigi.pages.PasswordResetPage; import org.cacert.gigi.pages.PolicyIndex; import org.cacert.gigi.pages.RootCertPage; import org.cacert.gigi.pages.StaticPage; import org.cacert.gigi.pages.TestSecure; import org.cacert.gigi.pages.Verify; import org.cacert.gigi.pages.account.ChangePasswordPage; -import org.cacert.gigi.pages.account.MyDetails; import org.cacert.gigi.pages.account.History; +import org.cacert.gigi.pages.account.MyDetails; import org.cacert.gigi.pages.account.UserTrainings; import org.cacert.gigi.pages.account.certs.CertificateAdd; import org.cacert.gigi.pages.account.certs.Certificates; @@ -153,6 +154,9 @@ public class Gigi extends HttpServlet { putPage(History.SUPPORT_PATH, new History(true), null); putPage(UserTrainings.PATH, new UserTrainings(false), "My Account"); putPage(UserTrainings.SUPPORT_PATH, new UserTrainings(true), null); + + putPage(PasswordResetPage.PATH, new PasswordResetPage(), null); + if (testing) { try { Class manager = Class.forName("org.cacert.gigi.pages.Manager"); diff --git a/src/org/cacert/gigi/database/DatabaseConnection.java b/src/org/cacert/gigi/database/DatabaseConnection.java index 67364d0a..b8c09e5a 100644 --- a/src/org/cacert/gigi/database/DatabaseConnection.java +++ b/src/org/cacert/gigi/database/DatabaseConnection.java @@ -17,7 +17,7 @@ import org.cacert.gigi.database.SQLFileManager.ImportType; public class DatabaseConnection { - public static final int CURRENT_SCHEMA_VERSION = 5; + public static final int CURRENT_SCHEMA_VERSION = 6; public static final int CONNECTION_TIMEOUT = 24 * 60 * 60; diff --git a/src/org/cacert/gigi/database/tableStructure.sql b/src/org/cacert/gigi/database/tableStructure.sql index c4e51737..301dabd2 100644 --- a/src/org/cacert/gigi/database/tableStructure.sql +++ b/src/org/cacert/gigi/database/tableStructure.sql @@ -373,4 +373,16 @@ CREATE TABLE "schemeVersion" ( "version" smallint NOT NULL, PRIMARY KEY ("version") ); -INSERT INTO "schemeVersion" (version) VALUES(5); +INSERT INTO "schemeVersion" (version) VALUES(6); + +DROP TABLE IF EXISTS `passwordResetTickets`; +CREATE TABLE `passwordResetTickets` ( + `id` serial NOT NULL, + `memid` int NOT NULL, + `creator` int NOT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `used` timestamp NULL DEFAULT NULL, + `token` varchar(32) NOT NULL, + `private_token` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +); diff --git a/src/org/cacert/gigi/database/upgrade/from_5.sql b/src/org/cacert/gigi/database/upgrade/from_5.sql new file mode 100644 index 00000000..6f9d8f7b --- /dev/null +++ b/src/org/cacert/gigi/database/upgrade/from_5.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS `passwordResetTickets`; +CREATE TABLE `passwordResetTickets` ( + `id` serial NOT NULL, + `memid` int NOT NULL, + `creator` int NOT NULL, + `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `used` timestamp NULL DEFAULT NULL, + `token` varchar(32) NOT NULL, + `private_token` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +); diff --git a/src/org/cacert/gigi/dbObjects/User.java b/src/org/cacert/gigi/dbObjects/User.java index 8d1d40fc..fc7b1c3f 100644 --- a/src/org/cacert/gigi/dbObjects/User.java +++ b/src/org/cacert/gigi/dbObjects/User.java @@ -109,7 +109,11 @@ public class User extends CertificateOwner { throw new GigiApiException("Old password does not match."); } } + setPassword(newPass); + } + private void setPassword(String newPass) throws GigiApiException { + GigiPreparedStatement ps; PasswordStrengthChecker.assertStrongPassword(newPass, getName(), getEmail()); ps = DatabaseConnection.getInstance().prepare("UPDATE users SET `password`=? WHERE id=?"); ps.setString(1, PasswordHash.hash(newPass)); @@ -479,4 +483,41 @@ public class User extends CertificateOwner { return entries.toArray(new String[0]); } + + public int generatePasswordResetTicket(User actor, String token, String privateToken) { + GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("INSERT INTO `passwordResetTickets` SET `memid`=?, `creator`=?, `token`=?, `private_token`=?"); + ps.setInt(1, getId()); + ps.setInt(2, getId()); + ps.setString(3, token); + ps.setString(4, PasswordHash.hash(privateToken)); + ps.execute(); + return ps.lastInsertId(); + } + + public static User getResetWithToken(int id, String token) { + GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("SELECT `memid` FROM `passwordResetTickets` WHERE `id`=? AND `token`=?"); + ps.setInt(1, id); + ps.setString(2, token); + GigiResultSet res = ps.executeQuery(); + if ( !res.next()) { + return null; + } + return User.getById(res.getInt(1)); + } + + public void consumePasswordResetTicket(int id, String private_token, String newPassword) throws GigiApiException { + GigiPreparedStatement ps = DatabaseConnection.getInstance().prepare("SELECT `private_token` FROM `passwordResetTickets` WHERE `id`=? AND `memid`=?"); + ps.setInt(1, id); + ps.setInt(2, getId()); + try (GigiResultSet rs = ps.executeQuery()) { + if ( !rs.next()) { + throw new GigiApiException("Token not found... very bad."); + } + if (PasswordHash.verifyHash(private_token, rs.getString(1)) == null) { + throw new GigiApiException("Private token does not match."); + } + setPassword(newPassword); + } + } + } diff --git a/src/org/cacert/gigi/pages/PasswordResetForm.templ b/src/org/cacert/gigi/pages/PasswordResetForm.templ new file mode 100644 index 00000000..aa5cce53 --- /dev/null +++ b/src/org/cacert/gigi/pages/PasswordResetForm.templ @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
:
*:
*:
*
diff --git a/src/org/cacert/gigi/pages/PasswordResetPage.java b/src/org/cacert/gigi/pages/PasswordResetPage.java new file mode 100644 index 00000000..8faaf826 --- /dev/null +++ b/src/org/cacert/gigi/pages/PasswordResetPage.java @@ -0,0 +1,93 @@ +package org.cacert.gigi.pages; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.cacert.gigi.GigiApiException; +import org.cacert.gigi.dbObjects.User; +import org.cacert.gigi.localisation.Language; +import org.cacert.gigi.output.template.Form; +import org.cacert.gigi.output.template.Template; +import org.cacert.gigi.util.AuthorizationContext; + +public class PasswordResetPage extends Page { + + public static final String PATH = "/passwordReset"; + + public PasswordResetPage() { + super("Password Reset"); + } + + public static class PasswordResetForm extends Form { + + private static Template t = new Template(PasswordResetForm.class.getResource("PasswordResetForm.templ")); + + private User u; + + private int id; + + public PasswordResetForm(HttpServletRequest hsr) throws GigiApiException { + super(hsr, PATH); + id = Integer.parseInt(hsr.getParameter("id")); + u = User.getResetWithToken(id, hsr.getParameter("token")); + if (u == null) { + throw new GigiApiException("User missing or token invalid"); + } + + } + + @Override + public boolean submit(PrintWriter out, HttpServletRequest req) throws GigiApiException { + String p1 = req.getParameter("pword1"); + String p2 = req.getParameter("pword2"); + String tok = req.getParameter("private_token"); + if (p1 == null || p2 == null || tok == null) { + throw new GigiApiException("Missing form parameter."); + } + if ( !p1.equals(p2)) { + throw new GigiApiException("New passwords differ."); + } + u.consumePasswordResetTicket(id, tok, p1); + return true; + } + + @Override + protected void outputContent(PrintWriter out, Language l, Map vars) { + + t.output(out, l, vars); + } + + } + + @Override + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + PasswordResetForm form = Form.getForm(req, PasswordResetForm.class); + try { + form.submit(resp.getWriter(), req); + resp.getWriter().println(getLanguage(req).getTranslation("Password reset successful.")); + return; + } catch (GigiApiException e) { + e.format(resp.getWriter(), getLanguage(req)); + } + form.output(resp.getWriter(), getLanguage(req), new HashMap()); + } + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + try { + new PasswordResetForm(req).output(resp.getWriter(), getLanguage(req), new HashMap()); + } catch (GigiApiException e) { + e.format(resp.getWriter(), getLanguage(req)); + } + } + + @Override + public boolean isPermitted(AuthorizationContext ac) { + return true; + } +} diff --git a/tests/org/cacert/gigi/TestPasswordReset.java b/tests/org/cacert/gigi/TestPasswordReset.java new file mode 100644 index 00000000..384ec31d --- /dev/null +++ b/tests/org/cacert/gigi/TestPasswordReset.java @@ -0,0 +1,56 @@ +package org.cacert.gigi; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import org.cacert.gigi.dbObjects.User; +import org.cacert.gigi.testUtils.ClientTest; +import org.cacert.gigi.util.RandomToken; +import org.junit.Test; + +public class TestPasswordReset extends ClientTest { + + String pub = RandomToken.generateToken(32); + + String priv = RandomToken.generateToken(32); + + int id = u.generatePasswordResetTicket(u, pub, priv); + + @Test + public void testInternal() throws IOException, GigiApiException { + User u2 = User.getResetWithToken(id, pub); + assertSame(u, u2); + assertNotNull(login(u.getEmail(), TEST_PASSWORD)); + u2.consumePasswordResetTicket(id, priv, TEST_PASSWORD + "'"); + assertEquals("", login(u.getEmail(), TEST_PASSWORD)); + assertNotNull(login(u.getEmail(), TEST_PASSWORD + "'")); + } + + @Test + public void testInternalWrongTk() throws IOException, GigiApiException { + User u2 = User.getResetWithToken(id, pub + "'"); + assertNull(u2); + } + + @Test + public void testInternalWrongId() throws IOException, GigiApiException { + User u2 = User.getResetWithToken(id + 1, pub); + assertNull(u2); + } + + @Test(expected = GigiApiException.class) + public void testInternalWeak() throws IOException, GigiApiException { + u.consumePasswordResetTicket(id, priv, ""); + } + + @Test(expected = GigiApiException.class) + public void testInternalWrongPriv() throws IOException, GigiApiException { + u.consumePasswordResetTicket(id, priv + "'", TEST_PASSWORD); + } + + @Test(expected = GigiApiException.class) + public void testInternalWrongIdSetting() throws IOException, GigiApiException { + u.consumePasswordResetTicket(id + 1, priv, TEST_PASSWORD); + } +} -- 2.39.2